@reclaimprotocol/agent 0.1.17 → 0.1.20
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 +6 -2
- package/lib/authoring/backend.d.ts +4 -0
- package/lib/authoring/tools.js +4 -1
- package/lib/cdp/capture.d.ts +2 -2
- package/lib/cdp/capture.js +25 -21
- package/lib/cdp/client.d.ts +39 -1
- package/lib/cdp/client.js +125 -1
- package/lib/consts.d.ts +7 -0
- package/lib/consts.js +15 -0
- package/lib/mcp/api/operations.gen.js +33 -9
- package/lib/mcp/instructions.js +27 -3
- package/lib/mcp/start.js +12 -8
- package/lib/mcp/tools/agent/attach.d.ts +18 -4
- package/lib/mcp/tools/agent/attach.js +142 -52
- package/lib/mcp/tools/agent/build.d.ts +1 -1
- package/lib/mcp/tools/agent/build.js +5 -3
- package/lib/mcp/tools/agent/capture.d.ts +1 -0
- package/lib/mcp/tools/agent/capture.js +8 -1
- package/lib/mcp/tools/agent/eval.js +2 -3
- package/lib/mcp/tools/agent/how-it-works.d.ts +1 -1
- package/lib/mcp/tools/agent/how-it-works.js +1 -1
- package/lib/mcp/tools/agent/publish.js +55 -1
- package/lib/mcp/tools/agent/session.d.ts +2 -2
- package/lib/mcp/tools/agent/session.js +21 -5
- package/lib/mcp/tools/agent/shared.js +4 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
- package/lib/mcp/tools/authenticate/authenticate.js +10 -4
- package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -0
- package/lib/mcp/tools/authenticate/builder-access.js +31 -0
- package/lib/mcp/tools/authenticate/index.d.ts +1 -0
- package/lib/mcp/tools/authenticate/index.js +1 -0
- package/lib/mcp/tools/mode/mode.d.ts +3 -2
- package/lib/mcp/tools/mode/mode.js +24 -4
- package/lib/old/login-cdp.js +11 -3
- package/lib/old/to-register.d.ts +16 -8
- package/lib/old/to-register.js +29 -21
- package/lib/old/tools/publish.js +109 -9
- package/lib/provider/draft.d.ts +20 -0
- package/lib/provider/draft.js +75 -51
- package/lib/provider/index.d.ts +1 -1
- package/lib/provider/index.js +1 -1
- package/lib/provider/injection-doc.js +4 -3
- package/lib/provider/schema.d.ts +26 -1
- package/lib/provider/secrets.d.ts +37 -0
- package/lib/provider/secrets.js +147 -6
- package/lib/provider/to-version.d.ts +26 -2
- package/lib/provider/to-version.js +29 -5
- package/package.json +2 -2
- package/skill/SKILL.builder.md +43 -95
- package/skill/SKILL.common.md +102 -0
- package/skill/SKILL.old.md +20 -74
- package/skill/builder-mode.md +4 -2
- package/skill/injection-api.md +95 -10
- package/skill/old-mode.md +5 -3
- package/src/authoring/backend.ts +4 -0
- package/src/authoring/tools.ts +4 -1
- package/src/cdp/capture.ts +24 -24
- package/src/cdp/client.ts +197 -2
- package/src/consts.ts +20 -0
- package/src/mcp/api/operations.gen.ts +33 -9
- package/src/mcp/instructions.ts +36 -3
- package/src/mcp/start.ts +13 -8
- package/src/mcp/tools/agent/attach.ts +198 -57
- package/src/mcp/tools/agent/build.ts +9 -2
- package/src/mcp/tools/agent/capture.ts +10 -1
- package/src/mcp/tools/agent/eval.ts +2 -3
- package/src/mcp/tools/agent/how-it-works.ts +1 -1
- package/src/mcp/tools/agent/publish.ts +64 -1
- package/src/mcp/tools/agent/session.ts +29 -8
- package/src/mcp/tools/agent/shared.ts +5 -10
- package/src/mcp/tools/authenticate/authenticate.ts +14 -4
- package/src/mcp/tools/authenticate/builder-access.ts +42 -0
- package/src/mcp/tools/authenticate/index.ts +4 -0
- package/src/mcp/tools/mode/mode.ts +28 -4
- package/src/old/login-cdp.ts +13 -3
- package/src/old/to-register.ts +50 -27
- package/src/old/tools/publish.ts +131 -9
- package/src/provider/draft.ts +98 -56
- package/src/provider/index.ts +9 -1
- package/src/provider/injection-doc.ts +4 -3
- package/src/provider/schema.ts +27 -1
- package/src/provider/secrets.ts +171 -6
- package/src/provider/to-version.ts +53 -7
- package/lib/authoring/find.d.ts +0 -3
- package/lib/authoring/find.js +0 -54
- package/lib/authoring/propose.d.ts +0 -3
- package/lib/authoring/propose.js +0 -109
- package/lib/authoring/replay.d.ts +0 -10
- package/lib/authoring/replay.js +0 -34
- package/lib/cli/args.d.ts +0 -14
- package/lib/cli/args.js +0 -19
- package/lib/cli/commands.d.ts +0 -1
- package/lib/cli/commands.js +0 -19
- package/lib/cli.d.ts +0 -1
- package/lib/cli.js +0 -7
- package/lib/mcp/credentials/authenticate.d.ts +0 -3
- package/lib/mcp/credentials/authenticate.js +0 -108
- package/lib/mcp/credentials/build.d.ts +0 -5
- package/lib/mcp/credentials/build.js +0 -18
- package/lib/mcp/credentials/cache.d.ts +0 -70
- package/lib/mcp/credentials/cache.js +0 -131
- package/lib/mcp/credentials/env-file.d.ts +0 -25
- package/lib/mcp/credentials/env-file.js +0 -71
- package/lib/mcp/credentials/import.d.ts +0 -2
- package/lib/mcp/credentials/import.js +0 -52
- package/lib/mcp/credentials/issue.d.ts +0 -2
- package/lib/mcp/credentials/issue.js +0 -84
- package/lib/mcp/credentials/keypair.d.ts +0 -17
- package/lib/mcp/credentials/keypair.js +0 -48
- package/lib/mcp/credentials/link.d.ts +0 -3
- package/lib/mcp/credentials/link.js +0 -107
- package/lib/mcp/credentials/list-cached.d.ts +0 -2
- package/lib/mcp/credentials/list-cached.js +0 -55
- package/lib/mcp/credentials/owner-key.d.ts +0 -23
- package/lib/mcp/credentials/owner-key.js +0 -58
- package/lib/mcp/credentials/paths.d.ts +0 -12
- package/lib/mcp/credentials/paths.js +0 -35
- package/lib/mcp/credentials/poll.d.ts +0 -27
- package/lib/mcp/credentials/poll.js +0 -61
- package/lib/mcp/credentials/register.d.ts +0 -18
- package/lib/mcp/credentials/register.js +0 -30
- package/lib/mcp/credentials/resolve-owner-key.d.ts +0 -12
- package/lib/mcp/credentials/resolve-owner-key.js +0 -68
- package/lib/mcp/credentials/session-cache.d.ts +0 -14
- package/lib/mcp/credentials/session-cache.js +0 -42
- package/lib/mcp/credentials/status.d.ts +0 -3
- package/lib/mcp/credentials/status.js +0 -76
- package/lib/mcp/register.d.ts +0 -38
- package/lib/mcp/register.js +0 -80
- package/lib/mcp/tools/analyze.d.ts +0 -3
- package/lib/mcp/tools/analyze.js +0 -26
- package/lib/mcp/tools/attach.d.ts +0 -11
- package/lib/mcp/tools/attach.js +0 -72
- package/lib/mcp/tools/capture.d.ts +0 -10
- package/lib/mcp/tools/capture.js +0 -142
- package/lib/mcp/tools/eval.d.ts +0 -5
- package/lib/mcp/tools/eval.js +0 -127
- package/lib/mcp/tools/find.d.ts +0 -3
- package/lib/mcp/tools/find.js +0 -60
- package/lib/mcp/tools/owner-key.d.ts +0 -2
- package/lib/mcp/tools/owner-key.js +0 -63
- package/lib/mcp/tools/proof.d.ts +0 -3
- package/lib/mcp/tools/proof.js +0 -214
- package/lib/mcp/tools/propose.d.ts +0 -9
- package/lib/mcp/tools/propose.js +0 -75
- package/lib/mcp/tools/publish.d.ts +0 -3
- package/lib/mcp/tools/publish.js +0 -59
- package/lib/mcp/tools/replay.d.ts +0 -3
- package/lib/mcp/tools/replay.js +0 -46
- package/lib/mcp/tools/session.d.ts +0 -10
- package/lib/mcp/tools/session.js +0 -29
- package/lib/mcp/tools/verification/diagnose.d.ts +0 -11
- package/lib/mcp/tools/verification/diagnose.js +0 -132
- package/lib/mcp/verifications/build.d.ts +0 -5
- package/lib/mcp/verifications/build.js +0 -14
- package/lib/mcp/verifications/create.d.ts +0 -3
- package/lib/mcp/verifications/create.js +0 -57
- package/lib/mcp/verifications/find.d.ts +0 -3
- package/lib/mcp/verifications/find.js +0 -37
- package/lib/mcp/verifications/get.d.ts +0 -3
- package/lib/mcp/verifications/get.js +0 -26
- package/lib/mcp/verifications/update.d.ts +0 -3
- package/lib/mcp/verifications/update.js +0 -78
- package/lib/old/tools/get-version-config.d.ts +0 -17
- package/lib/old/tools/get-version-config.js +0 -47
- package/lib/routes.gen.d.ts +0 -271
- package/lib/routes.gen.js +0 -73
- package/lib/types/openapi.d.ts +0 -4
- package/lib/types/openapi.gen.d.ts +0 -3967
- package/lib/types/openapi.gen.js +0 -5
- package/lib/types/openapi.js +0 -1
package/skill/injection-api.md
CHANGED
|
@@ -221,16 +221,101 @@ body's structure is pinned down by the hash anymore (any body at all would
|
|
|
221
221
|
hash the same), so prefer the minimal-placeholder approach above whenever
|
|
222
222
|
you can actually identify which fields vary.
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
The same opaque-string hashing (confirmed directly from the attestor-core
|
|
225
|
+
bundle shipped in this repo's `node_modules`) also applies to fields beyond
|
|
226
|
+
`body`. The caller that invokes it against a live submitted claim to gate
|
|
227
|
+
proof acceptance is `@reclaimprotocol/js-sdk`'s `verifyProof` — the next
|
|
228
|
+
section covers how that gate works and how to configure a provider that
|
|
229
|
+
satisfies it.
|
|
230
|
+
|
|
231
|
+
## Provider-hash validation (`verifyProof(proof, { providerId })`) — how it gates, and how to configure a provider that passes it
|
|
232
|
+
|
|
233
|
+
The caller for the hashing described above is `@reclaimprotocol/js-sdk`'s
|
|
234
|
+
`verifyProof`. When called with `{ providerId }` (the SDK's own
|
|
235
|
+
integration guide treats this as the normal case), traced directly from
|
|
236
|
+
that package's source (`src/utils/proofValidationUtils.ts`,
|
|
237
|
+
`src/utils/providerUtils.ts`, `src/witness.ts`):
|
|
238
|
+
|
|
239
|
+
1. `assertValidateProof` → `fetchProviderHashRequirementsBy(providerId, …)`
|
|
240
|
+
fetches this provider's CURRENT PUBLISHED CONFIG and builds a list of
|
|
241
|
+
"expected hashes" — one per entry in `requestData` PLUS one per entry in
|
|
242
|
+
`allowedInjectedRequestData` (old-devtools) / `allowedJsRequests`
|
|
243
|
+
(builder) — same concept, different field name per backend; don't
|
|
244
|
+
assume a doc or error naming one applies verbatim to the other.
|
|
245
|
+
2. Each expected hash is `hashRequestSpec` → `hashProofClaimParams({ url,
|
|
246
|
+
method, body, responseMatches, responseRedactions })` — the SAME
|
|
247
|
+
opaque-string hashing described above for `body`, applied to the WHOLE
|
|
248
|
+
spec at once.
|
|
249
|
+
3. The REAL submitted proof's own `claimData.parameters` is hashed the
|
|
250
|
+
SAME way, with `isOptional`-driven 2^N combinatorial expansion over
|
|
251
|
+
`responseMatches`/`responseRedactions` PAIRS so an optional field can be
|
|
252
|
+
either present or absent (`getProviderParamsAsCanonicalizedString`) —
|
|
253
|
+
but this expansion ONLY touches match/redaction pairs, never `url`.
|
|
254
|
+
4. `assertValidProofsByHash` requires the real proof's hash to exactly
|
|
255
|
+
equal ONE expected hash. No match → `verifyProof` rejects, even for a
|
|
256
|
+
perfectly valid, correctly-signed proof whose `responseMatches` are
|
|
257
|
+
100% correct in shape.
|
|
258
|
+
|
|
259
|
+
**Key constraint: `url` is hashed VERBATIM, with ZERO template/regex
|
|
260
|
+
resolution — even when `urlType` is `REGEX` or `TEMPLATE`.** Confirmed
|
|
261
|
+
directly from `generateSpecsFromRequestSpecTemplate` (the function that DOES
|
|
262
|
+
resolve `${var}` placeholders in `responseMatches`/`responseRedactions`
|
|
263
|
+
before hashing): it spreads every other field from the template unchanged,
|
|
264
|
+
and never touches `url`. `urlType` is consumed by a DIFFERENT validation
|
|
265
|
+
path entirely (matching an incoming live request against this spec at
|
|
266
|
+
capture time) — never by this hash computation.
|
|
267
|
+
|
|
268
|
+
**Check this before populating `allowedInjectedRequestData`/
|
|
269
|
+
`allowedJsRequests` for a `verifyProof({ providerId })` use case:** if the
|
|
270
|
+
real captured request URL contains ANY component that varies between two
|
|
271
|
+
otherwise-identical sessions — a tracking nonce, a CSRF token, a
|
|
272
|
+
cache-busting timestamp, a per-request trace id — the hash computed from
|
|
273
|
+
your declared spec can NEVER equal the hash of any real future proof, no
|
|
274
|
+
matter how correct `responseMatches`/`responseRedactions`/`body` are. This
|
|
275
|
+
is common on JS-heavy sites: e.g. Slack regenerates `_x_id`, `_x_csid`, and
|
|
276
|
+
`fp` on every single API call, so a captured `users.profile.getSections`
|
|
277
|
+
URL is never identical twice, even for the same user seconds apart.
|
|
278
|
+
Populating this field for such a provider is actively worse than leaving
|
|
279
|
+
it empty — it converts "no provider-hash check was attempted" into
|
|
280
|
+
"`verifyProof({ providerId })` rejects every real proof from this
|
|
281
|
+
provider, unconditionally."
|
|
282
|
+
|
|
283
|
+
**Verify before wiring this up**: capture the same endpoint twice, in two
|
|
284
|
+
separate sessions (ideally two different users, or the same user on two
|
|
285
|
+
different days), and diff the full request URL byte-for-byte.
|
|
286
|
+
- Identical every time → safe to populate with `urlType: CONSTANT` and the
|
|
287
|
+
literal URL.
|
|
288
|
+
- Not identical → do not populate this field expecting it to gate
|
|
289
|
+
`verifyProof({ providerId })`. There is no way to make a session-unique
|
|
290
|
+
URL satisfy this specific mechanism, on either backend — this is a
|
|
291
|
+
property of the hashing itself, not something fixable by editing
|
|
292
|
+
`responseMatches`, `responseRedactions`, or `body` more carefully. See
|
|
293
|
+
the fallback below.
|
|
294
|
+
|
|
295
|
+
Also note, independent of the `url` constraint above: even with a stable URL,
|
|
296
|
+
`responseMatches`/`responseRedactions` must be an EXACT structural match —
|
|
297
|
+
not just similar in spirit. A provider whose extracted field SET varies
|
|
298
|
+
per user (e.g. optional profile fields present for some users, absent for
|
|
299
|
+
others) needs every such field marked `isOptional: true` on BOTH the match
|
|
300
|
+
and its paired redaction, so the 2^N expansion produces a valid hash for
|
|
301
|
+
the "field absent" case too. A field that's sometimes-present but never
|
|
302
|
+
marked optional breaks the hash for any user missing it — the same failure
|
|
303
|
+
mode as the `url` constraint above, just scoped to one field instead of the
|
|
304
|
+
whole spec.
|
|
305
|
+
|
|
306
|
+
**Fallback if provider-hash validation keeps causing problems.** Whether
|
|
307
|
+
it's an unstable `url`, an un-marked optional field, or any other
|
|
308
|
+
persistent hash mismatch you can't pin down — the way out on the verifying
|
|
309
|
+
side is `verifyProof(proof, { dangerouslyDisableContentValidation: true })`.
|
|
310
|
+
That skips step 1-4 above entirely and trusts only the attestor's
|
|
311
|
+
cryptographic signature over the proof; it does NOT check that the proof's
|
|
312
|
+
`url`/`method`/`body`/`responseMatches`/`responseRedactions` match anything
|
|
313
|
+
you declared. A caller-supplied `{ hashes: [...] }` computed some other way
|
|
314
|
+
is the alternative when you still want hash-equality enforced, just not
|
|
315
|
+
derived from this provider's published config. Reach for
|
|
316
|
+
`dangerouslyDisableContentValidation` deliberately, not as a silent
|
|
317
|
+
default — it trades away the guarantee that the proof is shaped the way
|
|
318
|
+
this provider's spec says it should be.
|
|
234
319
|
|
|
235
320
|
## Recognizing this on a provider you didn't author
|
|
236
321
|
|
package/skill/old-mode.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
This server is running against the
|
|
1
|
+
This server is running against the devtools backend, so its capabilities are intentionally limited.
|
|
2
2
|
|
|
3
3
|
**Provider-authoring flow:**
|
|
4
4
|
|
|
5
5
|
1. `reclaim_authenticate` — by default opens the dashboard in Chrome, waits for you to sign in, captures the token automatically, and closes the tab (or paste a `token` / pass an eth `ethAddress` to skip the browser). Do this once before publishing or listing.
|
|
6
|
-
2. `
|
|
6
|
+
2. `attach_browser` → `start_capture` → drive the page (`navigate`, `wait_for_page`) → inspect (`list_requests`, `get_request`, `find_requests_containing`). Browser source is independent of this publishing mode: local Chrome works as before; `builder` uses `authenticate_builder` + `list_builder_organizations` to allocate a quota-accounted Popcorn browser and returns a shareable live-view URL; `custom` accepts a caller-supplied CDP websocket.
|
|
7
7
|
3. `propose_provider` to draft a provider, `replay_request` to pre-flight, `run_proof` to verify with the attestor. OPRF/`hash` redactions are optional and supported. When the data spans MULTIPLE requests (e.g. a landing call + a details call), propose + `run_proof` EACH request to get one proven draft per request.
|
|
8
8
|
4. `create_provider_version_from_capture` to publish. With NO `providerId` it REGISTERS A NEW provider (re-running creates a duplicate); pass a `providerId` from `get_me_providers` to add a new version to an existing provider instead. For a MULTI-REQUEST provider, pass `providers: [draftA, draftB, …]` (every proven draft) so EACH becomes a parallel `requestData` entry on the one version — passing a single `provider` saves only that one request.
|
|
9
9
|
5. `get_me_providers` lists the providers you own (the only provider-listing tool available in this mode).
|
|
10
10
|
|
|
11
11
|
**Editing / new versions (ownership):** you can only add a version to a provider **you own** — the `providerId` must come from `get_me_providers`. Trying to version a public provider you don't own fails with a permission error; that is expected. To publish your own version of such a site, re-run `create_provider_version_from_capture` **without** `providerId` to register your own copy (a separate provider you own). If versioning a provider you *do* own fails, confirm you authenticated as the account that created it.
|
|
12
12
|
|
|
13
|
-
Only GET/POST endpoints can be captured into providers here. There is no organization, credential-linking, or public-review workflow in this mode.
|
|
13
|
+
Only GET/POST endpoints can be captured into providers here. There is no Builder organization-management, credential-linking, or public-review workflow in this mode; the narrow `authenticate_builder` and `list_builder_organizations` exception exists only to allocate Builder-hosted authoring browsers.
|
|
14
14
|
|
|
15
15
|
**Diagnosing a session:** `session_analytics_logs` fetches the analytics logs for a verification session by id (from the logs service) — use it to see what happened in a session (matches, claims, failures).
|
|
16
16
|
|
|
17
17
|
**Templating:** bare `{{name}}` placeholders are provider params (values extracted from the response). Values the CONSUMER must supply at verification time (e.g. a username in the URL path) use the `{{context_<name>}}` form — this backend does not support the dotted `{{context.<name>}}` interpolation the new builder uses.
|
|
18
|
+
|
|
19
|
+
**Backend mode:** this is the reduced devtools surface — the BUILDER backend (generated API tools, orgs, verification sessions, public review) is one switch away. `get_devtools_mode` reports the active mode and why it's in effect; `set_devtools_mode` persists a switch to `~/.reclaim/config.json` — it takes effect on the NEXT server start (restart/reconnect), and an explicit `USE_OLD_DEVTOOLS` env var overrides the persisted choice.
|
package/src/authoring/backend.ts
CHANGED
|
@@ -71,6 +71,10 @@ export interface ProveOptions {
|
|
|
71
71
|
* its own result, passed straight through as the tool's output.
|
|
72
72
|
*/
|
|
73
73
|
export interface AuthoringBackend {
|
|
74
|
+
/** Ask Chrome to reject page evaluation when side effects cannot be ruled
|
|
75
|
+
* out. Cloud authoring enables this; local MCP keeps its existing
|
|
76
|
+
* behavior. */
|
|
77
|
+
readonly readOnlyPageEval?: boolean
|
|
74
78
|
/** Send a raw CDP command scoped to this session's page. */
|
|
75
79
|
sendCdp(method: string, params?: Record<string, unknown>): Promise<unknown>
|
|
76
80
|
/** Resolve once `method` (a CDP event) fires on this session's page, or
|
package/src/authoring/tools.ts
CHANGED
|
@@ -80,7 +80,10 @@ async function evalInPage(
|
|
|
80
80
|
expression: string,
|
|
81
81
|
): Promise<{ value?: unknown, error?: string }> {
|
|
82
82
|
const res = (await backend.sendCdp('Runtime.evaluate', {
|
|
83
|
-
expression,
|
|
83
|
+
expression,
|
|
84
|
+
returnByValue: true,
|
|
85
|
+
awaitPromise: true,
|
|
86
|
+
...(backend.readOnlyPageEval ? { throwOnSideEffect: true } : {}),
|
|
84
87
|
})) as EvalResult
|
|
85
88
|
if(res.exceptionDetails) {
|
|
86
89
|
return {
|
package/src/cdp/capture.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* request/response bookkeeping lives in `capture-engine.ts`, shared with the
|
|
5
5
|
* app's remote-Popcorn transport.
|
|
6
6
|
*/
|
|
7
|
-
import type CDP from 'chrome-remote-interface'
|
|
8
7
|
import {
|
|
9
8
|
type CaptureSession,
|
|
10
9
|
createCaptureEngine,
|
|
11
10
|
type StartCaptureOptions,
|
|
12
11
|
} from './capture-engine.ts'
|
|
12
|
+
import type { CdpTabClient } from './client.ts'
|
|
13
13
|
|
|
14
14
|
export {
|
|
15
15
|
isStaticAsset,
|
|
@@ -19,41 +19,41 @@ export {
|
|
|
19
19
|
export type { CaptureSession, StartCaptureOptions } from './capture-engine.ts'
|
|
20
20
|
|
|
21
21
|
export async function startCapture(
|
|
22
|
-
tab:
|
|
22
|
+
tab: CdpTabClient,
|
|
23
23
|
opts: StartCaptureOptions = {},
|
|
24
24
|
): Promise<CaptureSession> {
|
|
25
25
|
const engine = createCaptureEngine({
|
|
26
26
|
...(opts.maxBodyBytes !== undefined && { maxBodyBytes: opts.maxBodyBytes }),
|
|
27
|
-
fetchBody:
|
|
27
|
+
fetchBody: tab.getResponseBody,
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
})
|
|
47
|
-
Network.loadingFinished(({ requestId }) => {
|
|
48
|
-
void engine.onLoadingFinished(requestId)
|
|
49
|
-
})
|
|
30
|
+
const unsubscribe = [
|
|
31
|
+
tab.subscribe(
|
|
32
|
+
'Network.requestWillBeSent', engine.onRequestWillBeSent,
|
|
33
|
+
),
|
|
34
|
+
tab.subscribe('Network.requestWillBeSentExtraInfo', (event) => {
|
|
35
|
+
engine.mergeRequestHeaders(event.requestId, event.headers)
|
|
36
|
+
}),
|
|
37
|
+
tab.subscribe('Network.responseReceived', engine.onResponseReceived),
|
|
38
|
+
tab.subscribe('Network.responseReceivedExtraInfo', (event) => {
|
|
39
|
+
engine.mergeResponseHeaders(event.requestId, event.headers)
|
|
40
|
+
}),
|
|
41
|
+
tab.subscribe('Network.loadingFinished', (event) => {
|
|
42
|
+
void engine.onLoadingFinished(event.requestId)
|
|
43
|
+
}),
|
|
44
|
+
]
|
|
45
|
+
await tab.send('Network.enable', {})
|
|
50
46
|
|
|
51
47
|
return {
|
|
52
48
|
captureId: engine.captureId,
|
|
53
49
|
requests: engine.requests,
|
|
54
50
|
bodies: engine.bodies,
|
|
55
51
|
stop: async() => {
|
|
56
|
-
|
|
52
|
+
for(const stopListening of unsubscribe) {
|
|
53
|
+
stopListening()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await tab.send('Network.disable', {}).catch(() => {})
|
|
57
57
|
},
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/cdp/client.ts
CHANGED
|
@@ -1,20 +1,215 @@
|
|
|
1
1
|
import CDP from 'chrome-remote-interface'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { EventEmitter } from 'node:events'
|
|
4
|
+
import type {
|
|
5
|
+
RequestWillBeSentLike,
|
|
6
|
+
ResponseReceivedLike,
|
|
7
|
+
} from './capture-engine.ts'
|
|
8
|
+
import type { TabInfo } from './tabs.ts'
|
|
9
|
+
import { listTabs } from './tabs.ts'
|
|
10
|
+
|
|
11
|
+
const DIRECT_TARGET_ID = '__direct_cdp_target__'
|
|
12
|
+
|
|
13
|
+
interface CdpEventMessage {
|
|
14
|
+
method: string
|
|
15
|
+
params: object
|
|
16
|
+
sessionId?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CdpTabEventMap {
|
|
20
|
+
'Network.requestWillBeSent': RequestWillBeSentLike
|
|
21
|
+
'Network.requestWillBeSentExtraInfo': {
|
|
22
|
+
requestId: string
|
|
23
|
+
headers: Record<string, string>
|
|
24
|
+
}
|
|
25
|
+
'Network.responseReceived': ResponseReceivedLike & {
|
|
26
|
+
response: ResponseReceivedLike['response'] & { url: string }
|
|
27
|
+
}
|
|
28
|
+
'Network.responseReceivedExtraInfo': {
|
|
29
|
+
requestId: string
|
|
30
|
+
headers: Record<string, string>
|
|
31
|
+
}
|
|
32
|
+
'Network.loadingFinished': { requestId: string }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CdpTabClient {
|
|
36
|
+
send: (method: string, params?: object) => Promise<unknown>
|
|
37
|
+
getResponseBody: (
|
|
38
|
+
requestId: string,
|
|
39
|
+
) => Promise<{ body: string, base64Encoded: boolean }>
|
|
40
|
+
subscribe: <K extends keyof CdpTabEventMap>(
|
|
41
|
+
method: K,
|
|
42
|
+
listener: (params: CdpTabEventMap[K]) => void,
|
|
43
|
+
) => () => void
|
|
44
|
+
subscribeOnce: (method: string, listener: () => void) => () => void
|
|
45
|
+
close: () => Promise<void>
|
|
46
|
+
}
|
|
2
47
|
|
|
3
48
|
export interface CdpHandles {
|
|
4
49
|
/** Browser-level connection (target list, attach). */
|
|
5
50
|
browser: CDP.Client
|
|
6
51
|
/** Returns a per-target session bound to a tab. */
|
|
7
|
-
attachToTab: (targetId: string) => Promise<
|
|
52
|
+
attachToTab: (targetId: string) => Promise<CdpTabClient>
|
|
53
|
+
/** Enumerate page targets without assuming a localhost discovery endpoint. */
|
|
54
|
+
listTabs: () => Promise<TabInfo[]>
|
|
8
55
|
close: () => Promise<void>
|
|
9
56
|
}
|
|
10
57
|
|
|
11
58
|
export async function connectCdp(port: number): Promise<CdpHandles> {
|
|
12
59
|
const browser = await CDP({ port })
|
|
60
|
+
const tabs = new Set<CdpTabClient>()
|
|
61
|
+
return {
|
|
62
|
+
browser,
|
|
63
|
+
attachToTab: async(targetId) => {
|
|
64
|
+
const tab = adaptCdpClient(await CDP({ port, target: targetId }))
|
|
65
|
+
tabs.add(tab)
|
|
66
|
+
return tab
|
|
67
|
+
},
|
|
68
|
+
listTabs: () => listTabs(port),
|
|
69
|
+
close: async() => {
|
|
70
|
+
for(const tab of tabs) {
|
|
71
|
+
await tab.close().catch(() => {})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await browser.close()
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Connect to a browser-level or page-level CDP websocket. Browser-level
|
|
80
|
+
* endpoints (including Popcorn) use flattened Target sessions; a page-level
|
|
81
|
+
* custom endpoint is exposed as one synthetic tab and used directly. */
|
|
82
|
+
export async function connectCdpUrl(webSocketUrl: string): Promise<CdpHandles> {
|
|
83
|
+
const browser = await CDP({ target: webSocketUrl, local: true })
|
|
84
|
+
let browserLevel = true
|
|
85
|
+
try {
|
|
86
|
+
await browser.send('Target.getTargets')
|
|
87
|
+
} catch{
|
|
88
|
+
browserLevel = false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tabs = new Set<CdpTabClient>()
|
|
92
|
+
const directTab = browserLevel ? undefined : adaptCdpClient(browser)
|
|
13
93
|
return {
|
|
14
94
|
browser,
|
|
15
|
-
|
|
95
|
+
listTabs: async() => {
|
|
96
|
+
if(!browserLevel) {
|
|
97
|
+
return [{
|
|
98
|
+
tabId: DIRECT_TARGET_ID, url: '', title: 'Direct CDP target',
|
|
99
|
+
}]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { targetInfos } = await browser.send('Target.getTargets')
|
|
103
|
+
return targetInfos
|
|
104
|
+
.filter((target) => target.type === 'page')
|
|
105
|
+
.map((target) => ({
|
|
106
|
+
tabId: target.targetId,
|
|
107
|
+
url: target.url,
|
|
108
|
+
title: target.title,
|
|
109
|
+
}))
|
|
110
|
+
},
|
|
111
|
+
attachToTab: async(targetId) => {
|
|
112
|
+
if(!browserLevel) {
|
|
113
|
+
if(targetId !== DIRECT_TARGET_ID) {
|
|
114
|
+
throw new Error(`Unknown direct CDP target ${targetId}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
assert(directTab)
|
|
118
|
+
return directTab
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { sessionId } = await browser.send('Target.attachToTarget', {
|
|
122
|
+
targetId,
|
|
123
|
+
flatten: true,
|
|
124
|
+
})
|
|
125
|
+
const tab = flattenedSessionClient(browser, sessionId)
|
|
126
|
+
tabs.add(tab)
|
|
127
|
+
return tab
|
|
128
|
+
},
|
|
16
129
|
close: async() => {
|
|
130
|
+
for(const tab of tabs) {
|
|
131
|
+
await tab.close().catch(() => {})
|
|
132
|
+
}
|
|
133
|
+
|
|
17
134
|
await browser.close()
|
|
18
135
|
},
|
|
19
136
|
}
|
|
20
137
|
}
|
|
138
|
+
|
|
139
|
+
/** Adapt a flattened browser Target session to the small tab-client surface
|
|
140
|
+
* consumed by capture and authoring. Events are filtered by session id so
|
|
141
|
+
* concurrent tabs cannot observe each other's traffic. */
|
|
142
|
+
function flattenedSessionClient(
|
|
143
|
+
browser: CDP.Client,
|
|
144
|
+
sessionId: string,
|
|
145
|
+
): CdpTabClient {
|
|
146
|
+
const events = new EventEmitter()
|
|
147
|
+
// The declaration only accepts literal protocol method names; this adapter
|
|
148
|
+
// deliberately forwards the shared authoring layer's dynamic method string.
|
|
149
|
+
const sender = browser.send.bind(browser) as <T>(
|
|
150
|
+
method: string, params: object, sessionId?: string,
|
|
151
|
+
) => Promise<T>
|
|
152
|
+
const onEvent = (message: CdpEventMessage) => {
|
|
153
|
+
if(message.sessionId !== sessionId) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
events.emit('event', message)
|
|
158
|
+
events.emit(message.method, message.params)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
browser.on('event', onEvent)
|
|
162
|
+
|
|
163
|
+
return tabClient(events, (method, params) => {
|
|
164
|
+
return sender(method, params ?? {}, sessionId)
|
|
165
|
+
}, (requestId) => {
|
|
166
|
+
return sender<{ body: string, base64Encoded: boolean }>(
|
|
167
|
+
'Network.getResponseBody', { requestId }, sessionId,
|
|
168
|
+
)
|
|
169
|
+
}, async() => {
|
|
170
|
+
EventEmitter.prototype.removeListener.call(browser, 'event', onEvent)
|
|
171
|
+
await browser.send(
|
|
172
|
+
'Target.detachFromTarget', { sessionId },
|
|
173
|
+
).catch(() => {})
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function adaptCdpClient(client: CDP.Client): CdpTabClient {
|
|
178
|
+
const events = new EventEmitter()
|
|
179
|
+
const onEvent = (message: CdpEventMessage) => {
|
|
180
|
+
events.emit(message.method, message.params)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
client.on('event', onEvent)
|
|
184
|
+
|
|
185
|
+
// CRI intentionally types send against protocol literals. This adapter is
|
|
186
|
+
// the single boundary where shared authoring commands arrive dynamically.
|
|
187
|
+
const send = client.send.bind(client)
|
|
188
|
+
return tabClient(events, send, (requestId) => {
|
|
189
|
+
return client.send('Network.getResponseBody', { requestId })
|
|
190
|
+
}, async() => {
|
|
191
|
+
EventEmitter.prototype.removeListener.call(client, 'event', onEvent)
|
|
192
|
+
await client.close()
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function tabClient(
|
|
197
|
+
events: EventEmitter,
|
|
198
|
+
send: CdpTabClient['send'],
|
|
199
|
+
getResponseBody: CdpTabClient['getResponseBody'],
|
|
200
|
+
close: CdpTabClient['close'],
|
|
201
|
+
): CdpTabClient {
|
|
202
|
+
return {
|
|
203
|
+
send,
|
|
204
|
+
getResponseBody,
|
|
205
|
+
subscribe(method, listener) {
|
|
206
|
+
events.on(method, listener)
|
|
207
|
+
return () => events.removeListener(method, listener)
|
|
208
|
+
},
|
|
209
|
+
subscribeOnce(method, listener) {
|
|
210
|
+
events.once(method, listener)
|
|
211
|
+
return () => events.removeListener(method, listener)
|
|
212
|
+
},
|
|
213
|
+
close,
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/consts.ts
CHANGED
|
@@ -96,6 +96,26 @@ export function resolveOldMode(persisted?: 'old' | 'builder'): boolean {
|
|
|
96
96
|
return true
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Where the effective mode came from — the same precedence as
|
|
101
|
+
* `resolveOldMode`: explicit `USE_OLD_DEVTOOLS` env var → persisted config
|
|
102
|
+
* `mode` → default (old). Surfaced by the `get_devtools_mode` tool so the
|
|
103
|
+
* user can tell why the server is in the mode it reports.
|
|
104
|
+
*/
|
|
105
|
+
export function oldModeSource(
|
|
106
|
+
persisted?: 'old' | 'builder',
|
|
107
|
+
): 'env' | 'config' | 'default' {
|
|
108
|
+
if(oldDevtoolsEnvSet()) {
|
|
109
|
+
return 'env'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if(persisted) {
|
|
113
|
+
return 'config'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return 'default'
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
// ---------------------------------------------------------------------------
|
|
100
120
|
// Chrome / CDP launcher.
|
|
101
121
|
// ---------------------------------------------------------------------------
|
|
@@ -173,7 +173,7 @@ export const MCP_TOOLS = {
|
|
|
173
173
|
}),
|
|
174
174
|
},
|
|
175
175
|
"AdminSessions": {
|
|
176
|
-
description: "Explore verification sessions (admin-only)\n\nAdmin only. Search verification sessions across all organizations —
|
|
176
|
+
description: "Explore verification sessions (admin-only)\n\nAdmin only. Search verification sessions across all organizations — filter by `sessionId` (prefix match, at least 5 characters), `providerId`, `claimantClientId`, `orgId` (all exact match), `status`, and a `createdAfter`/`createdBefore` time range. Rows link to the org-scoped session detail page (admins can open any org's pages). Capped at 100, newest first.",
|
|
177
177
|
inputSchema: toZod({
|
|
178
178
|
"type": "object",
|
|
179
179
|
"properties": {
|
|
@@ -186,7 +186,19 @@ export const MCP_TOOLS = {
|
|
|
186
186
|
"query": {
|
|
187
187
|
"type": "object",
|
|
188
188
|
"properties": {
|
|
189
|
-
"
|
|
189
|
+
"sessionId": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"maxLength": 100
|
|
192
|
+
},
|
|
193
|
+
"providerId": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"maxLength": 100
|
|
196
|
+
},
|
|
197
|
+
"claimantClientId": {
|
|
198
|
+
"type": "string",
|
|
199
|
+
"maxLength": 100
|
|
200
|
+
},
|
|
201
|
+
"orgId": {
|
|
190
202
|
"type": "string",
|
|
191
203
|
"maxLength": 100
|
|
192
204
|
},
|
|
@@ -200,6 +212,14 @@ export const MCP_TOOLS = {
|
|
|
200
212
|
"error",
|
|
201
213
|
"expired"
|
|
202
214
|
]
|
|
215
|
+
},
|
|
216
|
+
"createdAfter": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"format": "date-time"
|
|
219
|
+
},
|
|
220
|
+
"createdBefore": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"format": "date-time"
|
|
203
223
|
}
|
|
204
224
|
}
|
|
205
225
|
},
|
|
@@ -623,9 +643,9 @@ export const MCP_TOOLS = {
|
|
|
623
643
|
},
|
|
624
644
|
"prompt": {
|
|
625
645
|
"type": "string",
|
|
626
|
-
"description": "Extraction prompt, used (and required) when `verificationType` is `llm`.",
|
|
646
|
+
"description": "Extraction prompt, used (and required) when `verificationType` is `llm`. Gated by the prompt guard before it is stored (docs/prompt-guard.md).",
|
|
627
647
|
"minLength": 0,
|
|
628
|
-
"maxLength":
|
|
648
|
+
"maxLength": 4000
|
|
629
649
|
},
|
|
630
650
|
"version": {
|
|
631
651
|
"type": "object",
|
|
@@ -2058,7 +2078,7 @@ export const MCP_TOOLS = {
|
|
|
2058
2078
|
}),
|
|
2059
2079
|
},
|
|
2060
2080
|
"ListOrgVerificationSessions": {
|
|
2061
|
-
description: "List an organization's verification sessions\n\nReturns verification sessions created under the organization, newest first. Optionally filter by `providerId`, `status`, `resolvedVersion`, `claimantClientId`, and a `createdAfter`/`createdBefore` time range. Caller must be an active member of the org. Opaque cursor pagination.\n\nNote: `status` filters on the stored column. A session that has passed `expiresAt` but has not yet been swept is reported as `expired` in its body while still stored as `pending`, so `status=expired` may not match such sessions until the sweep runs.",
|
|
2081
|
+
description: "List an organization's verification sessions\n\nReturns verification sessions created under the organization, newest first. Optionally filter by `sessionId` (prefix match, at least 5 characters), `providerId`, `status`, `resolvedVersion`, `claimantClientId`, and a `createdAfter`/`createdBefore` time range. Caller must be an active member of the org. Opaque cursor pagination.\n\nNote: `status` filters on the stored column. A session that has passed `expiresAt` but has not yet been swept is reported as `expired` in its body while still stored as `pending`, so `status=expired` may not match such sessions until the sweep runs.",
|
|
2062
2082
|
inputSchema: toZod({
|
|
2063
2083
|
"type": "object",
|
|
2064
2084
|
"properties": {
|
|
@@ -2078,9 +2098,13 @@ export const MCP_TOOLS = {
|
|
|
2078
2098
|
"query": {
|
|
2079
2099
|
"type": "object",
|
|
2080
2100
|
"properties": {
|
|
2101
|
+
"sessionId": {
|
|
2102
|
+
"type": "string",
|
|
2103
|
+
"maxLength": 100
|
|
2104
|
+
},
|
|
2081
2105
|
"providerId": {
|
|
2082
2106
|
"type": "string",
|
|
2083
|
-
"
|
|
2107
|
+
"maxLength": 100
|
|
2084
2108
|
},
|
|
2085
2109
|
"status": {
|
|
2086
2110
|
"type": "string",
|
|
@@ -2099,7 +2123,7 @@ export const MCP_TOOLS = {
|
|
|
2099
2123
|
},
|
|
2100
2124
|
"claimantClientId": {
|
|
2101
2125
|
"type": "string",
|
|
2102
|
-
"
|
|
2126
|
+
"maxLength": 100
|
|
2103
2127
|
},
|
|
2104
2128
|
"createdAfter": {
|
|
2105
2129
|
"type": "string",
|
|
@@ -2967,9 +2991,9 @@ export const MCP_TOOLS = {
|
|
|
2967
2991
|
},
|
|
2968
2992
|
"prompt": {
|
|
2969
2993
|
"type": "string",
|
|
2970
|
-
"description": "Extraction prompt, used when `verificationType` is `llm`.",
|
|
2994
|
+
"description": "Extraction prompt, used when `verificationType` is `llm`. Gated by the prompt guard before it is stored (docs/prompt-guard.md).",
|
|
2971
2995
|
"minLength": 0,
|
|
2972
|
-
"maxLength":
|
|
2996
|
+
"maxLength": 4000
|
|
2973
2997
|
},
|
|
2974
2998
|
"requests": {
|
|
2975
2999
|
"type": "array",
|
package/src/mcp/instructions.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `lib/mcp` (published) — both are two levels below the package root.
|
|
7
7
|
* Old-devtools mode has its own reduced set in `../old/instructions.ts`.
|
|
8
8
|
*/
|
|
9
|
+
import assert from 'node:assert'
|
|
9
10
|
import { readFileSync } from 'node:fs'
|
|
10
11
|
import { fileURLToPath } from 'node:url'
|
|
11
12
|
|
|
@@ -14,6 +15,32 @@ export const BUILDER_MODE_INSTRUCTIONS = readFileSync(
|
|
|
14
15
|
'utf8',
|
|
15
16
|
).trim()
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* `SKILL.builder.md` and `SKILL.old.md` share a lot of prose (steps that are
|
|
20
|
+
* identical regardless of backend, "never do" rules, common troubleshooting).
|
|
21
|
+
* That shared text lives once in `skill/SKILL.common.md`, split into named
|
|
22
|
+
* `<!-- section: name -->`-delimited blocks; each mode file splices a block
|
|
23
|
+
* back in at `<!-- common: name -->`. Parsed once at module load.
|
|
24
|
+
*/
|
|
25
|
+
const COMMON_SECTIONS = parseCommonSections(
|
|
26
|
+
readFileSync(
|
|
27
|
+
fileURLToPath(new URL('../../skill/SKILL.common.md', import.meta.url)),
|
|
28
|
+
'utf8',
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
function parseCommonSections(text: string): Record<string, string> {
|
|
33
|
+
const sections: Record<string, string> = {}
|
|
34
|
+
const re = /<!-- section: (\S+) -->\n([\s\S]*?)(?=\n<!-- section: |$)/g
|
|
35
|
+
for(const match of text.matchAll(re)) {
|
|
36
|
+
// Trim blank LINES only, not inline indentation — some sections are
|
|
37
|
+
// list-continuation text that's intentionally indented on its first line.
|
|
38
|
+
sections[match[1]] = match[2].replace(/^\n+/, '').replace(/\n+$/, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return sections
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
/**
|
|
18
45
|
* The full provider-authoring guide — the deep capture→prove→publish workflow,
|
|
19
46
|
* decision frameworks, and troubleshooting. Served on demand by the
|
|
@@ -24,9 +51,15 @@ export const BUILDER_MODE_INSTRUCTIONS = readFileSync(
|
|
|
24
51
|
* the instructions above; any leading frontmatter is stripped defensively.
|
|
25
52
|
*/
|
|
26
53
|
function readGuide(file: string): string {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
const url = new URL(file, import.meta.url)
|
|
55
|
+
const raw = readFileSync(fileURLToPath(url), 'utf8')
|
|
56
|
+
const stripped = raw.replace(/^---\n[\s\S]*?\n---\n/, '').trim()
|
|
57
|
+
|
|
58
|
+
return stripped.replace(/<!-- common: (\S+) -->/g, (whole, name: string) => {
|
|
59
|
+
const section = COMMON_SECTIONS[name]
|
|
60
|
+
assert(section, `${file}: no SKILL.common.md section named "${name}"`)
|
|
61
|
+
return section
|
|
62
|
+
})
|
|
30
63
|
}
|
|
31
64
|
|
|
32
65
|
export const PROVIDER_GUIDE_BUILDER = readGuide('../../skill/SKILL.builder.md')
|