@reclaimprotocol/agent 0.1.29 → 0.1.31
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 +28 -9
- package/lib/consts.d.ts +5 -5
- package/lib/consts.js +7 -7
- package/lib/live/tunnel-watchdog.d.ts +1 -0
- package/lib/live/tunnel-watchdog.js +66 -0
- package/lib/live/tunnel.d.ts +30 -7
- package/lib/live/tunnel.js +217 -40
- package/lib/mcp/api/operations.gen.d.ts +12 -0
- package/lib/mcp/api/operations.gen.js +537 -59
- package/lib/mcp/api/tools.js +27 -5
- package/lib/mcp/instructions.js +23 -32
- package/lib/mcp/remap_errors.js +6 -0
- package/lib/mcp/server.js +2 -8
- package/lib/mcp/start.js +3 -5
- package/lib/mcp/success-response.d.ts +5 -0
- package/lib/mcp/success-response.js +21 -0
- package/lib/mcp/tools/agent/attach.js +9 -66
- package/lib/mcp/tools/agent/build.d.ts +0 -3
- package/lib/mcp/tools/agent/build.js +3 -4
- package/lib/mcp/tools/agent/how-it-works.d.ts +5 -15
- package/lib/mcp/tools/agent/how-it-works.js +9 -22
- package/lib/mcp/tools/agent/live-view.d.ts +1 -15
- package/lib/mcp/tools/agent/live-view.js +71 -96
- package/lib/mcp/tools/agent/propose.js +7 -23
- package/lib/mcp/tools/agent/publish.js +80 -18
- package/lib/mcp/tools/agent/session.d.ts +4 -7
- package/lib/mcp/tools/agent/session.js +8 -23
- package/lib/mcp/tools/verification/result.d.ts +2 -1
- package/lib/mcp/tools/verification/result.js +29 -28
- package/lib/old/client.d.ts +2 -2
- package/lib/old/flag.d.ts +2 -5
- package/lib/old/flag.js +2 -5
- package/lib/old/instructions.d.ts +1 -1
- package/lib/old/instructions.js +1 -1
- package/lib/old/to-register.d.ts +4 -3
- package/lib/old/tools/publish.js +11 -14
- package/lib/proof/attestor-auth.d.ts +9 -1
- package/lib/proof/attestor-auth.js +15 -2
- package/lib/proof/attestor.d.ts +2 -2
- package/lib/proof/attestor.js +20 -2
- package/lib/proof/pod-prover.js +1 -1
- package/lib/proof/proof-core.d.ts +4 -0
- package/lib/proof/proof-core.js +20 -0
- package/lib/provider/injection-doc.d.ts +3 -2
- package/lib/provider/injection-doc.js +12 -10
- package/lib/provider/matcher.js +19 -15
- package/lib/provider/to-version.d.ts +3 -0
- package/lib/provider/to-version.js +2 -1
- package/package.json +3 -3
- package/skill/SKILL.builder.md +30 -83
- package/skill/SKILL.common.md +38 -22
- package/skill/SKILL.old.md +17 -3
- package/skill/builder-mode.md +37 -4
- package/skill/community-contributions.md +12 -3
- package/skill/hash-validation.md +9 -9
- package/skill/interception.md +39 -46
- package/skill/old-mode.md +13 -0
- package/skill/user-script.md +24 -8
- package/skill/verification-client-integration.md +130 -0
- package/src/consts.ts +9 -7
- package/src/mcp/api/operations.gen.ts +539 -59
- package/src/mcp/api/tools.ts +38 -7
- package/src/mcp/instructions.ts +33 -32
- package/src/mcp/remap_errors.ts +6 -0
- package/src/mcp/server.ts +2 -8
- package/src/mcp/start.ts +3 -5
- package/src/mcp/success-response.ts +26 -0
- package/src/mcp/tools/agent/attach.ts +9 -71
- package/src/mcp/tools/agent/build.ts +4 -7
- package/src/mcp/tools/agent/how-it-works.ts +9 -22
- package/src/mcp/tools/agent/live-view.ts +76 -116
- package/src/mcp/tools/agent/propose.ts +7 -23
- package/src/mcp/tools/agent/publish.ts +89 -18
- package/src/mcp/tools/agent/session.ts +6 -23
- package/src/mcp/tools/verification/result.ts +30 -29
- package/src/old/client.ts +2 -2
- package/src/old/flag.ts +2 -5
- package/src/old/instructions.ts +1 -1
- package/src/old/to-register.ts +4 -3
- package/src/old/tools/publish.ts +16 -14
- package/src/proof/attestor-auth.ts +18 -1
- package/src/proof/attestor.ts +24 -3
- package/src/proof/pod-prover.ts +1 -1
- package/src/proof/proof-core.ts +28 -0
- package/src/provider/injection-doc.ts +12 -10
- package/src/provider/matcher.ts +25 -17
- package/src/provider/to-version.ts +7 -1
- package/src/live/cloudflared.ts +0 -348
- package/src/live/tunnel.ts +0 -254
package/src/mcp/api/tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
IOperationId
|
|
6
6
|
} from '@reclaimprotocol/client/openapi'
|
|
7
7
|
import { remapErrorAsResponse } from '../remap_errors.ts'
|
|
8
|
+
import { successResponse } from '../success-response.ts'
|
|
8
9
|
import { MCP_TOOLS } from './operations.gen.ts'
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -23,6 +24,12 @@ function toSnakeCase(name: string): string {
|
|
|
23
24
|
|
|
24
25
|
export function registerTools(server: McpServer, client: ReclaimClient) {
|
|
25
26
|
for(const [opId, shape] of Object.entries(MCP_TOOLS)) {
|
|
27
|
+
if(!isOperationId(opId)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Generated MCP operation is not in the API client: ${opId}`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
// snake_case to match the hand-written tools. A name that collides with
|
|
27
34
|
// a hand-written tool throws here at startup — intentional: hand-written
|
|
28
35
|
// and generated names must stay disjoint (see start.ts), so a clash is a
|
|
@@ -32,15 +39,15 @@ export function registerTools(server: McpServer, client: ReclaimClient) {
|
|
|
32
39
|
shape,
|
|
33
40
|
async(args: unknown) => {
|
|
34
41
|
try {
|
|
42
|
+
if(!isOperationArgs(opId, args)) {
|
|
43
|
+
throw new Error(`Invalid generated MCP arguments for ${opId}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
const result = await client.call(
|
|
36
|
-
opId
|
|
37
|
-
args
|
|
47
|
+
opId,
|
|
48
|
+
args,
|
|
38
49
|
)
|
|
39
|
-
return
|
|
40
|
-
content: [{
|
|
41
|
-
type: 'text' as const, text: JSON.stringify(result.data, null, 2)
|
|
42
|
-
}],
|
|
43
|
-
}
|
|
50
|
+
return successResponse(result.data)
|
|
44
51
|
} catch(err) {
|
|
45
52
|
return remapErrorAsResponse(err)
|
|
46
53
|
}
|
|
@@ -48,3 +55,27 @@ export function registerTools(server: McpServer, client: ReclaimClient) {
|
|
|
48
55
|
)
|
|
49
56
|
}
|
|
50
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Always true in practice — `value` comes from `Object.entries(MCP_TOOLS)`
|
|
61
|
+
* itself — but the check still earns its keep: it narrows `opId` to
|
|
62
|
+
* `IOperationId` for `client.call` below without an unchecked cast, and it
|
|
63
|
+
* turns "the generator and the API client drifted apart" into a startup
|
|
64
|
+
* error instead of a silent `as`.
|
|
65
|
+
*/
|
|
66
|
+
function isOperationId(value: string): value is IOperationId {
|
|
67
|
+
return value in MCP_TOOLS
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The MCP SDK parses `args` through the generated Zod schema before invoking
|
|
72
|
+
* this callback. Keep a runtime object check at this TypeScript boundary so
|
|
73
|
+
* generated operation arguments retain their operation-specific type without
|
|
74
|
+
* an unchecked cast.
|
|
75
|
+
*/
|
|
76
|
+
function isOperationArgs<T extends IOperationId>(
|
|
77
|
+
_operationId: T,
|
|
78
|
+
value: unknown,
|
|
79
|
+
): value is IOperationArgs<T> {
|
|
80
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
81
|
+
}
|
package/src/mcp/instructions.ts
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Loads concise, always-on mode instructions and topic guides served on
|
|
3
|
+
* demand by `how_it_works`. Keeping detailed guidance out of tool schemas
|
|
4
|
+
* avoids adding it to every request's context.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* deliberately small, since every client pays for them on every turn;
|
|
7
|
-
* - the deep authoring guide, split into TOPICS and served on demand by the
|
|
8
|
-
* `how_it_works` tool. Nothing here is inlined into a tool schema: a doc
|
|
9
|
-
* embedded in an input-schema description is paid for on every request
|
|
10
|
-
* whether or not it's ever relevant.
|
|
11
|
-
*
|
|
12
|
-
* The prose lives in markdown (published via package.json's `files`) so it
|
|
13
|
-
* can be edited without touching code. Paths resolve identically from
|
|
14
|
-
* `src/mcp` (dev) and `lib/mcp` (published) — both are two levels below the
|
|
15
|
-
* package root.
|
|
16
|
-
*
|
|
17
|
-
* Assembly has three steps:
|
|
18
|
-
* 1. `<!-- common: name -->` splices a shared block out of `SKILL.common.md`
|
|
19
|
-
* (delimited there by `<!-- section: name -->`), so the two mode guides
|
|
20
|
-
* don't drift on the ~60% of prose that is identical.
|
|
21
|
-
* 2. `%CTX%` expands to the mode's consumer-param namespace — `context.` on
|
|
22
|
-
* the builder, `context_` on old-devtools. That is the only textual
|
|
23
|
-
* difference across most shared blocks, so tokenizing it keeps them
|
|
24
|
-
* shared.
|
|
25
|
-
* 3. `<!-- topic: name -->` splits the result into the units `how_it_works`
|
|
26
|
-
* serves. Backend-independent topics come from their own files instead.
|
|
6
|
+
* `<!-- common: name -->` includes shared sections, `%CTX%` selects the
|
|
7
|
+
* mode-specific context prefix, and `<!-- topic: name -->` creates a topic.
|
|
27
8
|
*/
|
|
28
9
|
import assert from 'node:assert'
|
|
29
10
|
import { readFileSync } from 'node:fs'
|
|
@@ -32,7 +13,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
32
13
|
function readSkill(file: string): string {
|
|
33
14
|
const url = new URL(`../../skill/${file}`, import.meta.url)
|
|
34
15
|
return readFileSync(fileURLToPath(url), 'utf8')
|
|
35
|
-
//
|
|
16
|
+
// These files are internal topic fragments, but tolerate skill frontmatter.
|
|
36
17
|
.replace(/^---\n[\s\S]*?\n---\n/, '')
|
|
37
18
|
.trim()
|
|
38
19
|
}
|
|
@@ -48,7 +29,7 @@ function splitBlocks(text: string, marker: string): Map<string, string> {
|
|
|
48
29
|
'g',
|
|
49
30
|
)
|
|
50
31
|
for(const match of text.matchAll(re)) {
|
|
51
|
-
// Trim blank
|
|
32
|
+
// Trim blank lines only, not inline indentation. Some blocks are
|
|
52
33
|
// list-continuation text that's intentionally indented on its first line.
|
|
53
34
|
blocks.set(match[1], match[2].replace(/^\n+/, '').replace(/\n+$/, ''))
|
|
54
35
|
}
|
|
@@ -65,6 +46,12 @@ const SHARED_TOPIC_FILES: Record<string, string> = {
|
|
|
65
46
|
'hash-validation': 'hash-validation.md',
|
|
66
47
|
}
|
|
67
48
|
|
|
49
|
+
/** Detailed topics that exist only in Builder mode. */
|
|
50
|
+
const BUILDER_TOPIC_FILES: Record<string, string> = {
|
|
51
|
+
'contributions': 'community-contributions.md',
|
|
52
|
+
'integration': 'verification-client-integration.md',
|
|
53
|
+
}
|
|
54
|
+
|
|
68
55
|
/** One line per topic, shown in the `how_it_works` index so a caller can pick
|
|
69
56
|
* the right one without fetching several. Keyed across both modes; only the
|
|
70
57
|
* topics a mode actually has are listed back to it. */
|
|
@@ -90,15 +77,16 @@ const TOPIC_SUMMARIES: Record<string, string> = {
|
|
|
90
77
|
+ 'and going live.',
|
|
91
78
|
'discover': 'Finding providers, resolving them, and creating a verification '
|
|
92
79
|
+ 'session.',
|
|
93
|
-
'integration': 'Builder
|
|
94
|
-
+ '
|
|
80
|
+
'integration': 'Set up a Builder token, callback, frontend and backend '
|
|
81
|
+
+ 'with @reclaimprotocol/client; direct `api=2` API and result '
|
|
82
|
+
+ 'verification.',
|
|
95
83
|
'user-script': 'Authoring jsUserScripts / customInjection to drive '
|
|
96
84
|
+ 'login-gated pages.',
|
|
97
85
|
'interception': 'injectionType / interceptorType, live interception, the '
|
|
98
86
|
+ 'document-replay flag, and what to do when a site won\'t load or '
|
|
99
87
|
+ 'won\'t let the user sign in.',
|
|
100
|
-
'hash-validation': 'bodySniff, and how
|
|
101
|
-
+ 'the declared spec hash.',
|
|
88
|
+
'hash-validation': 'bodySniff, and how verifyRequestProof({ providerId }) '
|
|
89
|
+
+ 'gates on the declared spec hash.',
|
|
102
90
|
'credentials': 'The eth proof-owner key run_proof signs with.',
|
|
103
91
|
'never-do': 'The short list of things that always break a provider.',
|
|
104
92
|
'troubleshooting': 'Expired drafts, browser problems, and where to look '
|
|
@@ -114,7 +102,11 @@ export interface Guide {
|
|
|
114
102
|
full: string
|
|
115
103
|
}
|
|
116
104
|
|
|
117
|
-
function buildGuide(
|
|
105
|
+
function buildGuide(
|
|
106
|
+
file: string,
|
|
107
|
+
ctxPrefix: string,
|
|
108
|
+
modeTopicFiles: Record<string, string> = {},
|
|
109
|
+
): Guide {
|
|
118
110
|
const raw = readSkill(file)
|
|
119
111
|
.replace(/<!-- common: (\S+) -->/g, (_whole, name: string) => {
|
|
120
112
|
const section = COMMON_SECTIONS.get(name)
|
|
@@ -129,6 +121,11 @@ function buildGuide(file: string, ctxPrefix: string): Guide {
|
|
|
129
121
|
topics.set(name, readSkill(path))
|
|
130
122
|
}
|
|
131
123
|
|
|
124
|
+
for(const [name, path] of Object.entries(modeTopicFiles)) {
|
|
125
|
+
assert(topics.has(name), `${file}: no topic marker named "${name}"`)
|
|
126
|
+
topics.set(name, readSkill(path))
|
|
127
|
+
}
|
|
128
|
+
|
|
132
129
|
const index = [...topics.keys()].map((topic) => {
|
|
133
130
|
const summary = TOPIC_SUMMARIES[topic]
|
|
134
131
|
assert(summary, `${file}: no TOPIC_SUMMARIES entry for "${topic}"`)
|
|
@@ -138,7 +135,11 @@ function buildGuide(file: string, ctxPrefix: string): Guide {
|
|
|
138
135
|
return { topics, index, full: [...topics.values()].join('\n\n') }
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
export const GUIDE_BUILDER = buildGuide(
|
|
138
|
+
export const GUIDE_BUILDER = buildGuide(
|
|
139
|
+
'SKILL.builder.md',
|
|
140
|
+
'context.',
|
|
141
|
+
BUILDER_TOPIC_FILES,
|
|
142
|
+
)
|
|
142
143
|
export const GUIDE_OLD = buildGuide('SKILL.old.md', 'context_')
|
|
143
144
|
|
|
144
145
|
export function guideFor(oldMode: boolean): Guide {
|
package/src/mcp/remap_errors.ts
CHANGED
|
@@ -10,6 +10,12 @@ export const remapErrorAsResponse = (
|
|
|
10
10
|
): CallToolResult => {
|
|
11
11
|
const text = errorAsToolText(err)
|
|
12
12
|
|
|
13
|
+
// Two unrelated things can carry structured data here: the client's own
|
|
14
|
+
// ProblemError (an RFC 9457 response from a Builder API call), and a
|
|
15
|
+
// hand-thrown Boom error whose second argument is arbitrary tool context,
|
|
16
|
+
// for example `badRequest('invalid provider', { field: 'providerId' })`.
|
|
17
|
+
// `isProblemDetails` only rejects a wrongly typed KNOWN field, so it lets
|
|
18
|
+
// that arbitrary shape through too — this isn't a strict RFC 9457 check.
|
|
13
19
|
const data = isProblemError(err)
|
|
14
20
|
? err.data
|
|
15
21
|
: isBoom(err) && err.output?.statusCode
|
package/src/mcp/server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
|
2
2
|
import { type RegisteredTool } from '../tools/registry.ts'
|
|
3
3
|
import { toZod } from './api/to-zod.ts'
|
|
4
4
|
import { remapErrorAsResponse } from './remap_errors.ts'
|
|
5
|
+
import { successResponse } from './success-response.ts'
|
|
5
6
|
|
|
6
7
|
// The tool primitive lives in `../tools/registry.ts` (SDK-free, so the cloud
|
|
7
8
|
// loop can use it too). Re-exported here so the MCP-side tool files can keep
|
|
@@ -27,14 +28,7 @@ export function registerToolList(server: McpServer, tools: RegisteredTool[]) {
|
|
|
27
28
|
},
|
|
28
29
|
async(args) => {
|
|
29
30
|
try {
|
|
30
|
-
return
|
|
31
|
-
content: [
|
|
32
|
-
{
|
|
33
|
-
type: 'text' as const,
|
|
34
|
-
text: JSON.stringify(await tool.handler(args), null, 2),
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
}
|
|
31
|
+
return successResponse(await tool.handler(args))
|
|
38
32
|
} catch(err) {
|
|
39
33
|
return remapErrorAsResponse(err)
|
|
40
34
|
}
|
package/src/mcp/start.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { BUILDER_MODE_INSTRUCTIONS } from './instructions.ts'
|
|
|
36
36
|
import { registerToolList } from './server.ts'
|
|
37
37
|
|
|
38
38
|
export async function startMcpServer() {
|
|
39
|
-
// Mode precedence: explicit
|
|
39
|
+
// Mode precedence: explicit env → persisted config → builder.
|
|
40
40
|
// The same store instance backs the set_devtools_mode toggle below.
|
|
41
41
|
const config = new ConfigStore()
|
|
42
42
|
const oldMode = resolveOldMode(config.read()?.mode)
|
|
@@ -69,8 +69,7 @@ export async function startMcpServer() {
|
|
|
69
69
|
registerToolList(server, buildModeTools(config, oldMode))
|
|
70
70
|
|
|
71
71
|
if(oldMode) {
|
|
72
|
-
// OLD-devtools mode
|
|
73
|
-
// production while builder is built. All auto-generated builder tools
|
|
72
|
+
// Explicit OLD-devtools mode. All auto-generated builder tools
|
|
74
73
|
// are disabled; a small ReclaimOldClient serves authenticate / publish /
|
|
75
74
|
// get_me_providers instead.
|
|
76
75
|
const oldClient = new ReclaimOldClient({
|
|
@@ -128,8 +127,7 @@ export async function startMcpServer() {
|
|
|
128
127
|
// Anything the session holds has to be released when the session ends, and
|
|
129
128
|
// the session can end without a single tool call: the developer closes their
|
|
130
129
|
// agent, the client kills the server, stdin drops. Nothing ran here before,
|
|
131
|
-
// which left a container up
|
|
132
|
-
// live browser after the session that made it was gone.
|
|
130
|
+
// which left a container up after the session that made it was gone.
|
|
133
131
|
if(agentToolsEnabled) {
|
|
134
132
|
installShutdownHooks(() => agentState)
|
|
135
133
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
|
|
2
|
+
|
|
3
|
+
/** Return successful tool data in both MCP representations. Older clients read
|
|
4
|
+
* the JSON text; newer clients can consume `structuredContent` without asking
|
|
5
|
+
* the model to parse or reconstruct identifiers, URLs, and state. */
|
|
6
|
+
export function successResponse(value: unknown): CallToolResult {
|
|
7
|
+
return {
|
|
8
|
+
structuredContent: structured(value),
|
|
9
|
+
content: [{
|
|
10
|
+
type: 'text',
|
|
11
|
+
text: JSON.stringify(value, null, 2) ?? 'null',
|
|
12
|
+
}],
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function structured(value: unknown): Record<string, unknown> {
|
|
17
|
+
if(isRecord(value)) {
|
|
18
|
+
return value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { result: value ?? null }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
26
|
+
}
|
|
@@ -85,22 +85,8 @@ interface AttachArgs {
|
|
|
85
85
|
viewerPort?: number
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
-
* DESCRIPTION (so an agent can put the choice to the developer straight from
|
|
91
|
-
* the tool list, without a discovery round-trip) and returned verbatim as
|
|
92
|
-
* structured `options` when the tool is called without a `mode` anyway.
|
|
93
|
-
*
|
|
94
|
-
* Being the single source for both is the point — an option list that a model
|
|
95
|
-
* reads in the description but that disagrees with what the tool returns is
|
|
96
|
-
* worse than either alone.
|
|
97
|
-
*
|
|
98
|
-
* Ordered and labelled BY USE CASE, because that is the question a developer
|
|
99
|
-
* can actually answer. "Which CDP transport?" is not; "does anyone else need to
|
|
100
|
-
* get into this browser?" is, and it decides two of the four. `useCase` is the
|
|
101
|
-
* grouping key, so the rendered list reads as two pairs rather than four
|
|
102
|
-
* unrelated transports.
|
|
103
|
-
*/
|
|
88
|
+
/** Browser sources returned when the caller omits `mode`. Keep selection detail
|
|
89
|
+
* here instead of repeating it in the always-loaded tool description. */
|
|
104
90
|
const BROWSER_OPTIONS = [
|
|
105
91
|
{
|
|
106
92
|
mode: 'dedicated',
|
|
@@ -181,33 +167,6 @@ const BROWSER_OPTIONS = [
|
|
|
181
167
|
},
|
|
182
168
|
] as const
|
|
183
169
|
|
|
184
|
-
/**
|
|
185
|
-
* The options as prose, grouped by use case.
|
|
186
|
-
*
|
|
187
|
-
* The grouping is the useful part: it turns "pick one of five transports" into
|
|
188
|
-
* "answer one question, then pick one of two".
|
|
189
|
-
*/
|
|
190
|
-
function renderBrowserOptions(): string {
|
|
191
|
-
const seen = new Set<string>()
|
|
192
|
-
const lines: string[] = []
|
|
193
|
-
for(const option of BROWSER_OPTIONS) {
|
|
194
|
-
if(!seen.has(option.useCase)) {
|
|
195
|
-
seen.add(option.useCase)
|
|
196
|
-
lines.push(`— ${option.useCase.toUpperCase()} —`)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const needs = option.requires.length
|
|
200
|
-
? ` Needs: ${option.requires.join('; ')}.`
|
|
201
|
-
: ''
|
|
202
|
-
lines.push(
|
|
203
|
-
`(${option.mode}) ${option.label}. ${option.detail}${needs}`
|
|
204
|
-
+ ` Live view: ${option.liveView}`,
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return lines.join(' ')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
170
|
interface AttachDependencies {
|
|
212
171
|
connectLocal: typeof connectCdp
|
|
213
172
|
connectRemote: typeof connectCdpUrl
|
|
@@ -238,34 +197,13 @@ export function attachTool(
|
|
|
238
197
|
{
|
|
239
198
|
name: 'attach_browser',
|
|
240
199
|
description:
|
|
241
|
-
'
|
|
242
|
-
+ '
|
|
243
|
-
+ '
|
|
244
|
-
+ '
|
|
245
|
-
+ '
|
|
246
|
-
+ '
|
|
247
|
-
+ '
|
|
248
|
-
+ 'THEMSELVES → offer `dedicated` or `attach`. SOMEONE ELSE, or the '
|
|
249
|
-
+ 'site only works on a phone → offer `container` or `builder`, '
|
|
250
|
-
+ 'which are the only two that produce a link another person can '
|
|
251
|
-
+ 'open. Say which of those two costs money: `container` is free '
|
|
252
|
-
+ 'but needs Docker, `builder` needs no install but is billed '
|
|
253
|
-
+ 'against the org\'s quota. '
|
|
254
|
-
+ 'Then relay the matching options with their live-view lines. You '
|
|
255
|
-
+ 'do not need a discovery call — the full list is here, and calling '
|
|
256
|
-
+ 'with no `mode` returns it as structured `options` if you would '
|
|
257
|
-
+ 'rather read it back verbatim. '
|
|
258
|
-
+ 'OPTIONS — ' + renderBrowserOptions() + ' '
|
|
259
|
-
+ 'A `container` browser returns a local `viewerUrl`; relay it. When '
|
|
260
|
-
+ 'someone remote needs in, call share_browser_view and relay the '
|
|
261
|
-
+ 'FULL public url verbatim, on its own line. `builder` returns a '
|
|
262
|
-
+ 'hosted `liveViewUrl` — relay that the same way. `dedicated`, '
|
|
263
|
-
+ '`attach` and `custom` cannot be shared at all: if they ask for a '
|
|
264
|
-
+ 'link after picking one, say so and offer to re-attach with '
|
|
265
|
-
+ '`container`. '
|
|
266
|
-
+ 'Builder and custom CDP credentials are held in memory and are '
|
|
267
|
-
+ 'never returned. '
|
|
268
|
-
+ 'More: how_it_works({ topic: "browser" }).',
|
|
200
|
+
'Connects provider-authoring tools to a browser. Call without '
|
|
201
|
+
+ '`mode` to receive the current options, then ask the developer to '
|
|
202
|
+
+ 'choose; `builder` consumes quota and only `container` and `builder` '
|
|
203
|
+
+ 'support a shareable live view. Call again once with the chosen '
|
|
204
|
+
+ 'mode. Relay returned live-view URLs verbatim. For selection, '
|
|
205
|
+
+ 'geolocation, and lifecycle guidance, call '
|
|
206
|
+
+ 'how_it_works({ topic: "browser" }).',
|
|
269
207
|
inputSchema: {
|
|
270
208
|
type: 'object',
|
|
271
209
|
properties: {
|
|
@@ -19,7 +19,7 @@ import { type AttachState, attachTool } from './attach.ts'
|
|
|
19
19
|
import { type CaptureStore, captureTools, resetCaptures } from './capture.ts'
|
|
20
20
|
import { listTabsTool } from './eval.ts'
|
|
21
21
|
import { howItWorksTool } from './how-it-works.ts'
|
|
22
|
-
import {
|
|
22
|
+
import { liveViewTools } from './live-view.ts'
|
|
23
23
|
import { podProofTools } from './pod-proof.ts'
|
|
24
24
|
import { createVersionFromCaptureTool } from './publish.ts'
|
|
25
25
|
import { resetAll, sessionTools } from './session.ts'
|
|
@@ -28,8 +28,6 @@ import { sharedAuthoringTools } from './shared.ts'
|
|
|
28
28
|
export interface AgentState {
|
|
29
29
|
attach: { current?: AttachState, config: ConfigStore }
|
|
30
30
|
captures: CaptureStore
|
|
31
|
-
/** The one public live view, when `share_browser_view` has opened it. */
|
|
32
|
-
liveShare: LiveShareRef
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/** Exported so a mode (for example, old-devtools) that supplies its own
|
|
@@ -41,7 +39,6 @@ export function buildAgentState(): AgentState {
|
|
|
41
39
|
return {
|
|
42
40
|
attach: { config: new ConfigStore() },
|
|
43
41
|
captures: { sessions: new Map() },
|
|
44
|
-
liveShare: {},
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
@@ -71,12 +68,12 @@ export function buildAgentTools(
|
|
|
71
68
|
tools.push(t)
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
for(const t of sessionTools(state.attach, state.captures
|
|
71
|
+
for(const t of sessionTools(state.attach, state.captures)) {
|
|
75
72
|
tools.push(t)
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
// Local-CDP only: a builder-mode browser already has a hosted live view.
|
|
79
|
-
for(const t of liveViewTools(state.attach
|
|
76
|
+
for(const t of liveViewTools(state.attach)) {
|
|
80
77
|
tools.push(t)
|
|
81
78
|
}
|
|
82
79
|
|
|
@@ -104,5 +101,5 @@ export function buildAgentTools(
|
|
|
104
101
|
* safe when nothing was ever attached.
|
|
105
102
|
*/
|
|
106
103
|
export async function disposeAgentState(state: AgentState): Promise<void> {
|
|
107
|
-
await resetAll(state.attach, state.captures
|
|
104
|
+
await resetAll(state.attach, state.captures)
|
|
108
105
|
}
|
|
@@ -2,21 +2,11 @@ import { guideFor } from '../../instructions.ts'
|
|
|
2
2
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Topic-scoped rather than all-at-once because the full guide is ~7k tokens:
|
|
11
|
-
* paying that to answer "how do I mark a header secret?" crowds out the actual
|
|
12
|
-
* task, which is exactly what makes an agent slow and prone to inventing
|
|
13
|
-
* details. Every response carries the topic index, so a caller that guessed
|
|
14
|
-
* wrong (or called with no topic at all) always learns what else is available
|
|
15
|
-
* without a second round-trip.
|
|
16
|
-
*
|
|
17
|
-
* `oldMode` selects the backend-specific guide, so the agent is never told to
|
|
18
|
-
* call auth/publish tools that aren't registered in its mode. Stateless:
|
|
19
|
-
* callable before `attach_browser`.
|
|
5
|
+
* Serve one mode-specific guide topic without loading the full guide (~7k
|
|
6
|
+
* tokens) — paying that cost to answer one question would crowd out the
|
|
7
|
+
* actual task. Every response includes the topic index, so a caller that
|
|
8
|
+
* guessed the topic name wrong still learns what else exists in the same
|
|
9
|
+
* round-trip.
|
|
20
10
|
*/
|
|
21
11
|
export function howItWorksTool(oldMode: boolean): RegisteredTool {
|
|
22
12
|
const guide = guideFor(oldMode)
|
|
@@ -26,13 +16,10 @@ export function howItWorksTool(oldMode: boolean): RegisteredTool {
|
|
|
26
16
|
{
|
|
27
17
|
name: 'how_it_works',
|
|
28
18
|
description:
|
|
29
|
-
'
|
|
30
|
-
+ '
|
|
31
|
-
+ '
|
|
32
|
-
+
|
|
33
|
-
+ '. Use `all` for the entire guide at once (large — prefer a topic). '
|
|
34
|
-
+ 'Read the relevant topic BEFORE acting unless you are already fluent '
|
|
35
|
-
+ 'in that part of the flow.',
|
|
19
|
+
'Returns topic-specific provider-authoring guidance for the active '
|
|
20
|
+
+ 'backend. Call without a topic for the overview and index. Before an '
|
|
21
|
+
+ 'unfamiliar or sensitive step, request that topic. Prefer one topic; '
|
|
22
|
+
+ '`all` returns the much larger complete guide.',
|
|
36
23
|
inputSchema: {
|
|
37
24
|
type: 'object',
|
|
38
25
|
properties: {
|