@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.
- package/README.md +287 -0
- package/lib/cdp/capture.d.ts +22 -0
- package/lib/cdp/capture.js +159 -0
- package/lib/cdp/client.d.ts +9 -0
- package/lib/cdp/client.js +11 -0
- package/lib/cdp/eviction.d.ts +16 -0
- package/lib/cdp/eviction.js +38 -0
- package/lib/cdp/launcher.d.ts +14 -0
- package/lib/cdp/launcher.js +102 -0
- package/lib/cdp/tabs.d.ts +6 -0
- package/lib/cdp/tabs.js +10 -0
- package/lib/cli/args.d.ts +14 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands.d.ts +1 -0
- package/lib/cli/commands.js +19 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +7 -0
- package/lib/config/store.d.ts +45 -0
- package/lib/config/store.js +46 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +7 -0
- package/lib/logger.d.ts +2 -0
- package/lib/logger.js +7 -0
- package/lib/mcp/api/operations.gen.d.ts +152 -0
- package/lib/mcp/api/operations.gen.js +2300 -0
- package/lib/mcp/api/tools.d.ts +3 -0
- package/lib/mcp/api/tools.js +36 -0
- package/lib/mcp/remap_errors.d.ts +2 -0
- package/lib/mcp/remap_errors.js +27 -0
- package/lib/mcp/server.d.ts +38 -0
- package/lib/mcp/server.js +76 -0
- package/lib/mcp/start.d.ts +1 -0
- package/lib/mcp/start.js +63 -0
- package/lib/mcp/tools/agent/analyze.d.ts +3 -0
- package/lib/mcp/tools/agent/analyze.js +26 -0
- package/lib/mcp/tools/agent/attach.d.ts +11 -0
- package/lib/mcp/tools/agent/attach.js +72 -0
- package/lib/mcp/tools/agent/build.d.ts +33 -0
- package/lib/mcp/tools/agent/build.js +74 -0
- package/lib/mcp/tools/agent/capture.d.ts +10 -0
- package/lib/mcp/tools/agent/capture.js +142 -0
- package/lib/mcp/tools/agent/eval.d.ts +5 -0
- package/lib/mcp/tools/agent/eval.js +127 -0
- package/lib/mcp/tools/agent/find.d.ts +3 -0
- package/lib/mcp/tools/agent/find.js +60 -0
- package/lib/mcp/tools/agent/proof.d.ts +3 -0
- package/lib/mcp/tools/agent/proof.js +214 -0
- package/lib/mcp/tools/agent/propose.d.ts +9 -0
- package/lib/mcp/tools/agent/propose.js +75 -0
- package/lib/mcp/tools/agent/publish.d.ts +3 -0
- package/lib/mcp/tools/agent/publish.js +122 -0
- package/lib/mcp/tools/agent/replay.d.ts +3 -0
- package/lib/mcp/tools/agent/replay.js +46 -0
- package/lib/mcp/tools/agent/session.d.ts +10 -0
- package/lib/mcp/tools/agent/session.js +29 -0
- package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
- package/lib/mcp/tools/credentials/authenticate.js +108 -0
- package/lib/mcp/tools/credentials/build.d.ts +16 -0
- package/lib/mcp/tools/credentials/build.js +33 -0
- package/lib/mcp/tools/credentials/cache.d.ts +70 -0
- package/lib/mcp/tools/credentials/cache.js +114 -0
- package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
- package/lib/mcp/tools/credentials/env-file.js +71 -0
- package/lib/mcp/tools/credentials/import.d.ts +2 -0
- package/lib/mcp/tools/credentials/import.js +52 -0
- package/lib/mcp/tools/credentials/issue.d.ts +2 -0
- package/lib/mcp/tools/credentials/issue.js +84 -0
- package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
- package/lib/mcp/tools/credentials/list-cached.js +55 -0
- package/lib/mcp/tools/credentials/register.d.ts +18 -0
- package/lib/mcp/tools/credentials/register.js +30 -0
- package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
- package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
- package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
- package/lib/mcp/tools/credentials/session-cache.js +19 -0
- package/lib/mcp/tools/credentials/status.d.ts +3 -0
- package/lib/mcp/tools/credentials/status.js +62 -0
- package/lib/mcp-bin.d.ts +1 -0
- package/lib/mcp-bin.js +7 -0
- package/lib/old/client.d.ts +67 -0
- package/lib/old/client.js +146 -0
- package/lib/old/flag.d.ts +11 -0
- package/lib/old/flag.js +13 -0
- package/lib/old/identity.d.ts +22 -0
- package/lib/old/identity.js +47 -0
- package/lib/old/instructions.d.ts +7 -0
- package/lib/old/instructions.js +29 -0
- package/lib/old/login-cdp.d.ts +23 -0
- package/lib/old/login-cdp.js +125 -0
- package/lib/old/to-register.d.ts +47 -0
- package/lib/old/to-register.js +133 -0
- package/lib/old/tools/authenticate.d.ts +18 -0
- package/lib/old/tools/authenticate.js +127 -0
- package/lib/old/tools/build.d.ts +9 -0
- package/lib/old/tools/build.js +16 -0
- package/lib/old/tools/me-providers.d.ts +10 -0
- package/lib/old/tools/me-providers.js +32 -0
- package/lib/old/tools/publish.d.ts +16 -0
- package/lib/old/tools/publish.js +97 -0
- package/lib/paths.d.ts +11 -0
- package/lib/paths.js +32 -0
- package/lib/pkg.d.ts +3 -0
- package/lib/pkg.js +13 -0
- package/lib/proof/attestor.d.ts +39 -0
- package/lib/proof/attestor.js +207 -0
- package/lib/proof/replay.d.ts +8 -0
- package/lib/proof/replay.js +73 -0
- package/lib/provider/constraints.d.ts +2 -0
- package/lib/provider/constraints.js +122 -0
- package/lib/provider/draft.d.ts +6 -0
- package/lib/provider/draft.js +68 -0
- package/lib/provider/graphql.d.ts +2 -0
- package/lib/provider/graphql.js +35 -0
- package/lib/provider/matcher.d.ts +36 -0
- package/lib/provider/matcher.js +436 -0
- package/lib/provider/normalize.d.ts +4 -0
- package/lib/provider/normalize.js +51 -0
- package/lib/provider/schema.d.ts +76 -0
- package/lib/provider/schema.js +1 -0
- package/lib/provider/secrets.d.ts +5 -0
- package/lib/provider/secrets.js +22 -0
- package/lib/provider/to-version.d.ts +22 -0
- package/lib/provider/to-version.js +68 -0
- package/lib/types/openapi.d.ts +4 -0
- package/lib/types/openapi.js +1 -0
- package/package.json +88 -0
- package/scripts/download-zk-files.ts +42 -0
- package/scripts/prepend-shebang.ts +17 -0
- package/skill/SKILL.md +248 -0
- package/src/cdp/capture.ts +215 -0
- package/src/cdp/client.ts +20 -0
- package/src/cdp/eviction.ts +52 -0
- package/src/cdp/launcher.ts +133 -0
- package/src/cdp/tabs.ts +22 -0
- package/src/cli/args.ts +20 -0
- package/src/cli/commands.ts +27 -0
- package/src/cli.ts +7 -0
- package/src/config/store.ts +86 -0
- package/src/index.ts +12 -0
- package/src/logger.ts +8 -0
- package/src/mcp/api/operations.gen.ts +2397 -0
- package/src/mcp/api/tools.ts +45 -0
- package/src/mcp/remap_errors.ts +35 -0
- package/src/mcp/server.ts +119 -0
- package/src/mcp/start.ts +78 -0
- package/src/mcp/tools/agent/analyze.ts +34 -0
- package/src/mcp/tools/agent/attach.ts +95 -0
- package/src/mcp/tools/agent/build.ts +99 -0
- package/src/mcp/tools/agent/capture.ts +202 -0
- package/src/mcp/tools/agent/eval.ts +161 -0
- package/src/mcp/tools/agent/find.ts +73 -0
- package/src/mcp/tools/agent/proof.ts +272 -0
- package/src/mcp/tools/agent/propose.ts +111 -0
- package/src/mcp/tools/agent/publish.ts +155 -0
- package/src/mcp/tools/agent/replay.ts +58 -0
- package/src/mcp/tools/agent/session.ts +46 -0
- package/src/mcp/tools/credentials/authenticate.ts +135 -0
- package/src/mcp/tools/credentials/build.ts +37 -0
- package/src/mcp/tools/credentials/cache.ts +161 -0
- package/src/mcp/tools/credentials/env-file.ts +101 -0
- package/src/mcp/tools/credentials/import.ts +72 -0
- package/src/mcp/tools/credentials/issue.ts +107 -0
- package/src/mcp/tools/credentials/list-cached.ts +66 -0
- package/src/mcp/tools/credentials/register.ts +49 -0
- package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
- package/src/mcp/tools/credentials/session-cache.ts +31 -0
- package/src/mcp/tools/credentials/status.ts +82 -0
- package/src/mcp-bin.ts +7 -0
- package/src/old/client.ts +236 -0
- package/src/old/flag.ts +13 -0
- package/src/old/identity.ts +56 -0
- package/src/old/instructions.ts +30 -0
- package/src/old/login-cdp.ts +153 -0
- package/src/old/to-register.ts +234 -0
- package/src/old/tools/authenticate.ts +152 -0
- package/src/old/tools/build.ts +21 -0
- package/src/old/tools/me-providers.ts +42 -0
- package/src/old/tools/publish.ts +135 -0
- package/src/paths.ts +37 -0
- package/src/pkg.ts +24 -0
- package/src/proof/attestor.ts +286 -0
- package/src/proof/replay.ts +102 -0
- package/src/provider/constraints.ts +150 -0
- package/src/provider/draft.ts +105 -0
- package/src/provider/graphql.ts +42 -0
- package/src/provider/matcher.ts +616 -0
- package/src/provider/normalize.ts +65 -0
- package/src/provider/schema.ts +92 -0
- package/src/provider/secrets.ts +29 -0
- package/src/provider/to-version.ts +90 -0
- 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
|
+
}
|
package/src/cdp/tabs.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/args.ts
ADDED
|
@@ -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,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