@nudge-ui/mcp 0.2.0 → 0.2.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 +71 -53
- package/dist/bridge.d.ts +4 -2
- package/dist/bridge.js +237 -15
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +65 -12
- package/dist/cli.mjs +14741 -13959
- package/dist/discovery.d.ts +29 -0
- package/dist/discovery.js +112 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/internal/agent-protocol.d.ts +65 -1
- package/dist/internal/agent-protocol.js +183 -3
- package/dist/project.d.ts +54 -0
- package/dist/project.js +151 -0
- package/dist/projectSessionClient.d.ts +28 -0
- package/dist/projectSessionClient.js +93 -0
- package/dist/protocol.d.ts +13 -2
- package/dist/protocol.js +12 -1
- package/dist/server.d.ts +14 -0
- package/dist/server.js +90 -22
- package/dist/sessionRegistry.d.ts +8 -0
- package/dist/sessionRegistry.js +89 -0
- package/dist/sessionSelection.d.ts +15 -0
- package/dist/sessionSelection.js +41 -0
- package/package.json +6 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentPromptRequest, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult } from "./internal/agent-protocol.js";
|
|
2
|
+
import type { CanvasCommandInput } from "./bridge.js";
|
|
3
|
+
import { type ProjectSession } from "./project.js";
|
|
4
|
+
export interface DiscoveredProjectSession extends ProjectSession {
|
|
5
|
+
readonly matchesWorkspace: boolean;
|
|
6
|
+
readonly matchesApplication: boolean;
|
|
7
|
+
readonly claimed: boolean;
|
|
8
|
+
readonly status: Pick<AgentStatusSnapshot, "connection" | "listenerActive" | "paired">;
|
|
9
|
+
}
|
|
10
|
+
export declare function discoverProjectSessions(applicationRoot: string, registryRoot?: string): Promise<DiscoveredProjectSession[]>;
|
|
11
|
+
/** Routes MCP tools to a live project in the adapter's exact worktree. */
|
|
12
|
+
export declare class DiscoveredProjectRouter {
|
|
13
|
+
readonly workspaceRoot: string;
|
|
14
|
+
readonly applicationRoot: string;
|
|
15
|
+
readonly registryRoot: string;
|
|
16
|
+
readonly agentId: string;
|
|
17
|
+
private client;
|
|
18
|
+
private releaseGeneration;
|
|
19
|
+
private constructor();
|
|
20
|
+
static create(workspaceRoot: string, registryRoot?: string): Promise<DiscoveredProjectRouter>;
|
|
21
|
+
listSessions(): Promise<DiscoveredProjectSession[]>;
|
|
22
|
+
private connectToSession;
|
|
23
|
+
waitForPrompt(signal?: AbortSignal, sessionId?: string): Promise<AgentPromptRequest>;
|
|
24
|
+
getStatus(): Promise<AgentStatusSnapshot>;
|
|
25
|
+
updateRequestStatus(update: AgentStatusUpdate): Promise<AgentStatusSnapshot>;
|
|
26
|
+
dispatchCanvasCommand(command: CanvasCommandInput): Promise<CanvasCommandResult>;
|
|
27
|
+
release(): Promise<void>;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { ProjectSessionClient } from "./projectSessionClient.js";
|
|
5
|
+
import { readLiveProjectSessions } from "./sessionRegistry.js";
|
|
6
|
+
import { matchesApplication, selectProjectSession } from "./sessionSelection.js";
|
|
7
|
+
import { defaultSessionRegistryRoot, resolveWorkspaceRoot, } from "./project.js";
|
|
8
|
+
function publicSession(session) {
|
|
9
|
+
const { controlToken: _controlToken, pid: _pid, ...visible } = session;
|
|
10
|
+
return visible;
|
|
11
|
+
}
|
|
12
|
+
export async function discoverProjectSessions(applicationRoot, registryRoot = defaultSessionRegistryRoot()) {
|
|
13
|
+
const canonicalApplication = await realpath(resolve(applicationRoot));
|
|
14
|
+
const canonicalWorkspace = await resolveWorkspaceRoot(canonicalApplication);
|
|
15
|
+
const live = await readLiveProjectSessions(resolve(registryRoot));
|
|
16
|
+
return live.map(({ descriptor, health }) => {
|
|
17
|
+
const matchesWorkspace = descriptor.workspaceRoot === canonicalWorkspace;
|
|
18
|
+
return {
|
|
19
|
+
...publicSession(descriptor),
|
|
20
|
+
matchesWorkspace,
|
|
21
|
+
matchesApplication: matchesApplication(descriptor, {
|
|
22
|
+
workspaceRoot: canonicalWorkspace,
|
|
23
|
+
applicationRoot: canonicalApplication,
|
|
24
|
+
}),
|
|
25
|
+
claimed: health.claimed,
|
|
26
|
+
status: {
|
|
27
|
+
connection: health.status.connection,
|
|
28
|
+
listenerActive: health.status.listenerActive,
|
|
29
|
+
paired: health.status.paired,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}).sort((left, right) => right.startedAt.localeCompare(left.startedAt));
|
|
33
|
+
}
|
|
34
|
+
/** Routes MCP tools to a live project in the adapter's exact worktree. */
|
|
35
|
+
export class DiscoveredProjectRouter {
|
|
36
|
+
workspaceRoot;
|
|
37
|
+
applicationRoot;
|
|
38
|
+
registryRoot;
|
|
39
|
+
agentId;
|
|
40
|
+
client = null;
|
|
41
|
+
releaseGeneration = 0;
|
|
42
|
+
constructor(workspaceRoot, applicationRoot, registryRoot, agentId) {
|
|
43
|
+
this.workspaceRoot = workspaceRoot;
|
|
44
|
+
this.applicationRoot = applicationRoot;
|
|
45
|
+
this.registryRoot = registryRoot;
|
|
46
|
+
this.agentId = agentId;
|
|
47
|
+
}
|
|
48
|
+
static async create(workspaceRoot, registryRoot = defaultSessionRegistryRoot()) {
|
|
49
|
+
const applicationRoot = await realpath(resolve(workspaceRoot));
|
|
50
|
+
return new DiscoveredProjectRouter(await resolveWorkspaceRoot(applicationRoot), applicationRoot, resolve(registryRoot), randomUUID());
|
|
51
|
+
}
|
|
52
|
+
async listSessions() {
|
|
53
|
+
return await discoverProjectSessions(this.applicationRoot, this.registryRoot);
|
|
54
|
+
}
|
|
55
|
+
async connectToSession(sessionId) {
|
|
56
|
+
const live = await readLiveProjectSessions(this.registryRoot);
|
|
57
|
+
const selected = selectProjectSession(live.map(({ descriptor }) => descriptor), {
|
|
58
|
+
workspaceRoot: this.workspaceRoot,
|
|
59
|
+
applicationRoot: this.applicationRoot,
|
|
60
|
+
requestedSessionId: sessionId,
|
|
61
|
+
previousSession: this.client?.descriptor,
|
|
62
|
+
});
|
|
63
|
+
if (this.client?.descriptor.sessionId === selected.sessionId)
|
|
64
|
+
return this.client;
|
|
65
|
+
await this.client?.close();
|
|
66
|
+
this.client = new ProjectSessionClient(selected, this.agentId);
|
|
67
|
+
return this.client;
|
|
68
|
+
}
|
|
69
|
+
async waitForPrompt(signal, sessionId) {
|
|
70
|
+
const releaseGeneration = this.releaseGeneration;
|
|
71
|
+
const client = await this.connectToSession(sessionId);
|
|
72
|
+
if (releaseGeneration !== this.releaseGeneration) {
|
|
73
|
+
if (this.client === client) {
|
|
74
|
+
this.client = null;
|
|
75
|
+
await client.close();
|
|
76
|
+
}
|
|
77
|
+
throw new Error("The Nudge project session was released.");
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return await client.waitForPrompt(signal);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (signal?.aborted || sessionId || releaseGeneration !== this.releaseGeneration)
|
|
84
|
+
throw error;
|
|
85
|
+
const replacement = await this.connectToSession();
|
|
86
|
+
if (replacement === client)
|
|
87
|
+
throw error;
|
|
88
|
+
return await replacement.waitForPrompt(signal);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async getStatus() {
|
|
92
|
+
const client = await this.connectToSession();
|
|
93
|
+
return await client.getStatus();
|
|
94
|
+
}
|
|
95
|
+
async updateRequestStatus(update) {
|
|
96
|
+
const client = await this.connectToSession();
|
|
97
|
+
return await client.updateRequestStatus(update);
|
|
98
|
+
}
|
|
99
|
+
async dispatchCanvasCommand(command) {
|
|
100
|
+
const client = await this.connectToSession();
|
|
101
|
+
return await client.dispatchCanvasCommand(command);
|
|
102
|
+
}
|
|
103
|
+
async release() {
|
|
104
|
+
this.releaseGeneration += 1;
|
|
105
|
+
const client = this.client;
|
|
106
|
+
this.client = null;
|
|
107
|
+
await client?.close();
|
|
108
|
+
}
|
|
109
|
+
async close() {
|
|
110
|
+
await this.release();
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { createAgentCompanion, type AgentCompanion, type AgentCompanionOptions } from "./server.js";
|
|
1
|
+
export { createAgentCompanion, createDiscoveredAgentAdapter, type AgentCompanion, type AgentCompanionOptions, type DiscoveredAgentAdapter, } from "./server.js";
|
|
2
|
+
export { DiscoveredProjectRouter, discoverProjectSessions, type DiscoveredProjectSession, } from "./discovery.js";
|
|
3
|
+
export { defaultSessionRegistryRoot, resolveWorkspaceRoot, startProjectBridge, type ProjectBridgeOptions, type ProjectBridgeRuntime, type ProjectSession, } from "./project.js";
|
|
2
4
|
export { createLoopbackBridge, type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress, type BridgeClock, type CanvasCommandInput, } from "./bridge.js";
|
|
3
5
|
export { openPairedPage, parseCliArguments, runCli, type CliOptions, type PageLaunchDependencies, } from "./cli.js";
|
|
4
6
|
export * from "./protocol.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { createAgentCompanion } from "./server.js";
|
|
1
|
+
export { createAgentCompanion, createDiscoveredAgentAdapter, } from "./server.js";
|
|
2
|
+
export { DiscoveredProjectRouter, discoverProjectSessions, } from "./discovery.js";
|
|
3
|
+
export { defaultSessionRegistryRoot, resolveWorkspaceRoot, startProjectBridge, } from "./project.js";
|
|
2
4
|
export { createLoopbackBridge, } from "./bridge.js";
|
|
3
5
|
export { openPairedPage, parseCliArguments, runCli, } from "./cli.js";
|
|
4
6
|
export * from "./protocol.js";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* imports. Hosts can use the same validation and message shapes without
|
|
7
7
|
* pulling the companion into a browser bundle.
|
|
8
8
|
*/
|
|
9
|
-
export declare const AGENT_PROTOCOL_VERSION:
|
|
9
|
+
export declare const AGENT_PROTOCOL_VERSION: 2;
|
|
10
10
|
export declare const AGENT_PROTOCOL_LIMITS: {
|
|
11
11
|
readonly projectId: 256;
|
|
12
12
|
readonly workspaceRoot: 4096;
|
|
@@ -14,7 +14,21 @@ export declare const AGENT_PROTOCOL_LIMITS: {
|
|
|
14
14
|
readonly sessionToken: 256;
|
|
15
15
|
readonly requestId: 256;
|
|
16
16
|
readonly commandId: 256;
|
|
17
|
+
readonly clientDispatchId: 256;
|
|
17
18
|
readonly prompt: 32000;
|
|
19
|
+
readonly sketchId: 256;
|
|
20
|
+
readonly sketchRevision: 2147483647;
|
|
21
|
+
readonly sketchFilename: 256;
|
|
22
|
+
readonly sketchDescription: 2000;
|
|
23
|
+
readonly sketchCount: 4;
|
|
24
|
+
readonly sketchImageBytes: number;
|
|
25
|
+
readonly sketchTotalBytes: number;
|
|
26
|
+
readonly sketchEncodedBytes: number;
|
|
27
|
+
readonly sketchAnnotationCount: 50;
|
|
28
|
+
readonly sketchUrl: 2048;
|
|
29
|
+
readonly sketchTitle: 512;
|
|
30
|
+
readonly sketchFramework: 128;
|
|
31
|
+
readonly sketchHost: 128;
|
|
18
32
|
readonly groupId: 256;
|
|
19
33
|
readonly label: 160;
|
|
20
34
|
readonly routeUrl: 2048;
|
|
@@ -108,6 +122,47 @@ export interface AgentPromptRequest {
|
|
|
108
122
|
readonly prompt: string;
|
|
109
123
|
/** The browser's canonical change revision captured at dispatch time. */
|
|
110
124
|
readonly changeRevision?: number;
|
|
125
|
+
/** Correlates the durable browser handoff batch across a lost response. */
|
|
126
|
+
readonly clientDispatchId?: string;
|
|
127
|
+
/** Metadata for the exact sketch revisions included in this request. */
|
|
128
|
+
readonly sketches?: readonly AgentSketchMetadata[];
|
|
129
|
+
}
|
|
130
|
+
export interface AgentSketchCaptureMetadata {
|
|
131
|
+
readonly url: string;
|
|
132
|
+
readonly title: string;
|
|
133
|
+
readonly timestamp: number;
|
|
134
|
+
readonly viewportWidth: number;
|
|
135
|
+
readonly viewportHeight: number;
|
|
136
|
+
readonly scrollX: number;
|
|
137
|
+
readonly scrollY: number;
|
|
138
|
+
readonly devicePixelRatio: number;
|
|
139
|
+
readonly host: string;
|
|
140
|
+
readonly framework: string;
|
|
141
|
+
}
|
|
142
|
+
export interface AgentSketchAnnotation {
|
|
143
|
+
readonly number: number;
|
|
144
|
+
readonly description: string;
|
|
145
|
+
}
|
|
146
|
+
/** Metadata retained in statuses and acknowledgements without image bytes. */
|
|
147
|
+
export interface AgentSketchMetadata {
|
|
148
|
+
readonly id: string;
|
|
149
|
+
readonly revision: number;
|
|
150
|
+
readonly filename: string;
|
|
151
|
+
readonly mimeType: "image/png";
|
|
152
|
+
readonly width: number;
|
|
153
|
+
readonly height: number;
|
|
154
|
+
readonly byteSize: number;
|
|
155
|
+
readonly capture: AgentSketchCaptureMetadata;
|
|
156
|
+
readonly description?: string;
|
|
157
|
+
readonly annotations?: readonly AgentSketchAnnotation[];
|
|
158
|
+
}
|
|
159
|
+
/** The only protocol shape that carries screenshot bytes. */
|
|
160
|
+
export interface AgentSketchAttachment extends AgentSketchMetadata {
|
|
161
|
+
readonly data: string;
|
|
162
|
+
}
|
|
163
|
+
/** Internal companion delivery shape; attachment bytes are never status data. */
|
|
164
|
+
export interface AgentDeliveredPrompt extends AgentPromptRequest {
|
|
165
|
+
readonly attachments?: readonly AgentSketchAttachment[];
|
|
111
166
|
}
|
|
112
167
|
export type AgentRequestStatus = "working" | "completed" | "failed" | "interrupted";
|
|
113
168
|
export interface AgentRequestOutcome {
|
|
@@ -166,6 +221,8 @@ export interface CanvasCommandAcknowledgement {
|
|
|
166
221
|
export interface PairingRequest {
|
|
167
222
|
readonly projectId: string;
|
|
168
223
|
readonly origin: string;
|
|
224
|
+
/** Reuses an existing pairing across a same-browser reload. */
|
|
225
|
+
readonly sessionToken?: string;
|
|
169
226
|
/** The active page is retained only while this process is alive. */
|
|
170
227
|
readonly pageUrl?: string;
|
|
171
228
|
}
|
|
@@ -182,6 +239,8 @@ export interface PromptDispatchRequest {
|
|
|
182
239
|
readonly sessionToken: string;
|
|
183
240
|
readonly prompt: string;
|
|
184
241
|
readonly changeRevision?: number;
|
|
242
|
+
readonly clientDispatchId?: string;
|
|
243
|
+
readonly attachments?: readonly AgentSketchAttachment[];
|
|
185
244
|
}
|
|
186
245
|
export interface PromptDispatchResponse {
|
|
187
246
|
readonly request: AgentPromptRequest;
|
|
@@ -223,6 +282,11 @@ export declare function validateRoutes(value: unknown, origin: string): AgentRou
|
|
|
223
282
|
/** Validates the fields shared by every companion message. */
|
|
224
283
|
export declare function isProtocolVersion(value: unknown): value is AgentProtocolVersion;
|
|
225
284
|
export declare function isProjectIdentity(value: unknown): value is AgentProjectIdentity;
|
|
285
|
+
export declare function isAgentSketchCaptureMetadata(value: unknown): value is AgentSketchCaptureMetadata;
|
|
286
|
+
export declare function isAgentSketchAnnotation(value: unknown): value is AgentSketchAnnotation;
|
|
287
|
+
export declare function isAgentSketchMetadata(value: unknown): value is AgentSketchMetadata;
|
|
288
|
+
export declare function isAgentSketchAttachment(value: unknown): value is AgentSketchAttachment;
|
|
289
|
+
export declare function validateSketchAttachments(value: unknown): AgentSketchAttachment[];
|
|
226
290
|
export declare function isAgentPromptRequest(value: unknown): value is AgentPromptRequest;
|
|
227
291
|
export declare function isAgentStatusUpdate(value: unknown): value is AgentStatusUpdate;
|
|
228
292
|
export declare function isCanvasState(value: unknown): value is CanvasState;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* imports. Hosts can use the same validation and message shapes without
|
|
7
7
|
* pulling the companion into a browser bundle.
|
|
8
8
|
*/
|
|
9
|
-
export const AGENT_PROTOCOL_VERSION =
|
|
9
|
+
export const AGENT_PROTOCOL_VERSION = 2;
|
|
10
10
|
export const AGENT_PROTOCOL_LIMITS = {
|
|
11
11
|
projectId: 256,
|
|
12
12
|
workspaceRoot: 4096,
|
|
@@ -14,7 +14,21 @@ export const AGENT_PROTOCOL_LIMITS = {
|
|
|
14
14
|
sessionToken: 256,
|
|
15
15
|
requestId: 256,
|
|
16
16
|
commandId: 256,
|
|
17
|
+
clientDispatchId: 256,
|
|
17
18
|
prompt: 32_000,
|
|
19
|
+
sketchId: 256,
|
|
20
|
+
sketchRevision: 2_147_483_647,
|
|
21
|
+
sketchFilename: 256,
|
|
22
|
+
sketchDescription: 2_000,
|
|
23
|
+
sketchCount: 4,
|
|
24
|
+
sketchImageBytes: 2 * 1024 * 1024,
|
|
25
|
+
sketchTotalBytes: 8 * 1024 * 1024,
|
|
26
|
+
sketchEncodedBytes: 12 * 1024 * 1024,
|
|
27
|
+
sketchAnnotationCount: 50,
|
|
28
|
+
sketchUrl: 2_048,
|
|
29
|
+
sketchTitle: 512,
|
|
30
|
+
sketchFramework: 128,
|
|
31
|
+
sketchHost: 128,
|
|
18
32
|
groupId: 256,
|
|
19
33
|
label: 160,
|
|
20
34
|
routeUrl: 2048,
|
|
@@ -64,6 +78,25 @@ function optionalString(value, maximum) {
|
|
|
64
78
|
function optionalRevision(value) {
|
|
65
79
|
return value === undefined || (typeof value === "number" && Number.isSafeInteger(value) && value >= 0);
|
|
66
80
|
}
|
|
81
|
+
function boundedNumber(value, maximum) {
|
|
82
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum;
|
|
83
|
+
}
|
|
84
|
+
function boundedFiniteNumber(value, maximum) {
|
|
85
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= maximum;
|
|
86
|
+
}
|
|
87
|
+
function boundedString(value, maximum) {
|
|
88
|
+
return typeof value === "string" && value.length <= maximum;
|
|
89
|
+
}
|
|
90
|
+
function validBase64(value, maximum) {
|
|
91
|
+
return typeof value === "string"
|
|
92
|
+
&& value.length > 0
|
|
93
|
+
&& value.length <= maximum
|
|
94
|
+
&& /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
|
|
95
|
+
}
|
|
96
|
+
function encodedByteLength(value) {
|
|
97
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
98
|
+
return Math.max(0, Math.floor(value.length * 3 / 4) - padding);
|
|
99
|
+
}
|
|
67
100
|
/**
|
|
68
101
|
* Returns a canonical origin or null. Credentials, paths, and fragments are
|
|
69
102
|
* rejected so the browser cannot broaden the pairing boundary accidentally.
|
|
@@ -157,13 +190,160 @@ export function isProjectIdentity(value) {
|
|
|
157
190
|
&& (value.origin === undefined || isCanonicalOrigin(value.origin))
|
|
158
191
|
&& optionalString(value.workspaceRoot, AGENT_PROTOCOL_LIMITS.workspaceRoot);
|
|
159
192
|
}
|
|
193
|
+
export function isAgentSketchCaptureMetadata(value) {
|
|
194
|
+
if (!isRecord(value) || !hasOnlyKeys(value, [
|
|
195
|
+
"url",
|
|
196
|
+
"title",
|
|
197
|
+
"timestamp",
|
|
198
|
+
"viewportWidth",
|
|
199
|
+
"viewportHeight",
|
|
200
|
+
"scrollX",
|
|
201
|
+
"scrollY",
|
|
202
|
+
"devicePixelRatio",
|
|
203
|
+
"host",
|
|
204
|
+
"framework",
|
|
205
|
+
]))
|
|
206
|
+
return false;
|
|
207
|
+
if (!nonEmptyString(value.url, AGENT_PROTOCOL_LIMITS.sketchUrl)
|
|
208
|
+
|| !boundedString(value.title, AGENT_PROTOCOL_LIMITS.sketchTitle)
|
|
209
|
+
|| !boundedNumber(value.timestamp, Number.MAX_SAFE_INTEGER)
|
|
210
|
+
|| !boundedNumber(value.viewportWidth, 100_000)
|
|
211
|
+
|| !boundedNumber(value.viewportHeight, 100_000)
|
|
212
|
+
|| !boundedFiniteNumber(value.scrollX, Number.MAX_SAFE_INTEGER)
|
|
213
|
+
|| !boundedFiniteNumber(value.scrollY, Number.MAX_SAFE_INTEGER)
|
|
214
|
+
|| !boundedFiniteNumber(value.devicePixelRatio, 100)
|
|
215
|
+
|| value.devicePixelRatio <= 0
|
|
216
|
+
|| !nonEmptyString(value.host, AGENT_PROTOCOL_LIMITS.sketchHost)
|
|
217
|
+
|| !nonEmptyString(value.framework, AGENT_PROTOCOL_LIMITS.sketchFramework)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const url = new URL(value.url);
|
|
222
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
export function isAgentSketchAnnotation(value) {
|
|
231
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["number", "description"]))
|
|
232
|
+
return false;
|
|
233
|
+
return boundedNumber(value.number, AGENT_PROTOCOL_LIMITS.sketchAnnotationCount)
|
|
234
|
+
&& value.number >= 1
|
|
235
|
+
&& nonEmptyString(value.description, AGENT_PROTOCOL_LIMITS.sketchDescription);
|
|
236
|
+
}
|
|
237
|
+
function validSketchAnnotationList(value) {
|
|
238
|
+
if (!Array.isArray(value)
|
|
239
|
+
|| value.length > AGENT_PROTOCOL_LIMITS.sketchAnnotationCount
|
|
240
|
+
|| !value.every(isAgentSketchAnnotation))
|
|
241
|
+
return false;
|
|
242
|
+
return new Set(value.map((annotation) => annotation.number)).size === value.length;
|
|
243
|
+
}
|
|
244
|
+
export function isAgentSketchMetadata(value) {
|
|
245
|
+
if (!isRecord(value) || !hasOnlyKeys(value, [
|
|
246
|
+
"id",
|
|
247
|
+
"revision",
|
|
248
|
+
"filename",
|
|
249
|
+
"mimeType",
|
|
250
|
+
"width",
|
|
251
|
+
"height",
|
|
252
|
+
"byteSize",
|
|
253
|
+
"capture",
|
|
254
|
+
"description",
|
|
255
|
+
"annotations",
|
|
256
|
+
]))
|
|
257
|
+
return false;
|
|
258
|
+
return nonEmptyString(value.id, AGENT_PROTOCOL_LIMITS.sketchId)
|
|
259
|
+
&& boundedNumber(value.revision, AGENT_PROTOCOL_LIMITS.sketchRevision)
|
|
260
|
+
&& value.revision > 0
|
|
261
|
+
&& nonEmptyString(value.filename, AGENT_PROTOCOL_LIMITS.sketchFilename)
|
|
262
|
+
&& value.mimeType === "image/png"
|
|
263
|
+
&& boundedNumber(value.width, 2_048)
|
|
264
|
+
&& value.width > 0
|
|
265
|
+
&& boundedNumber(value.height, 2_048)
|
|
266
|
+
&& value.height > 0
|
|
267
|
+
&& boundedNumber(value.byteSize, AGENT_PROTOCOL_LIMITS.sketchImageBytes)
|
|
268
|
+
&& value.byteSize > 0
|
|
269
|
+
&& isAgentSketchCaptureMetadata(value.capture)
|
|
270
|
+
&& (value.description === undefined
|
|
271
|
+
|| boundedString(value.description, AGENT_PROTOCOL_LIMITS.sketchDescription))
|
|
272
|
+
&& (value.annotations === undefined || validSketchAnnotationList(value.annotations));
|
|
273
|
+
}
|
|
274
|
+
export function isAgentSketchAttachment(value) {
|
|
275
|
+
if (!isRecord(value) || !hasOnlyKeys(value, [
|
|
276
|
+
"id",
|
|
277
|
+
"revision",
|
|
278
|
+
"filename",
|
|
279
|
+
"mimeType",
|
|
280
|
+
"width",
|
|
281
|
+
"height",
|
|
282
|
+
"byteSize",
|
|
283
|
+
"capture",
|
|
284
|
+
"description",
|
|
285
|
+
"annotations",
|
|
286
|
+
"data",
|
|
287
|
+
]) || !isAgentSketchMetadata({
|
|
288
|
+
id: value.id,
|
|
289
|
+
revision: value.revision,
|
|
290
|
+
filename: value.filename,
|
|
291
|
+
mimeType: value.mimeType,
|
|
292
|
+
width: value.width,
|
|
293
|
+
height: value.height,
|
|
294
|
+
byteSize: value.byteSize,
|
|
295
|
+
capture: value.capture,
|
|
296
|
+
...(value.description === undefined ? {} : { description: value.description }),
|
|
297
|
+
...(value.annotations === undefined ? {} : { annotations: value.annotations }),
|
|
298
|
+
}))
|
|
299
|
+
return false;
|
|
300
|
+
return validBase64(value.data, AGENT_PROTOCOL_LIMITS.sketchEncodedBytes)
|
|
301
|
+
&& encodedByteLength(value.data) === value.byteSize;
|
|
302
|
+
}
|
|
303
|
+
export function validateSketchAttachments(value) {
|
|
304
|
+
if (value === undefined)
|
|
305
|
+
return [];
|
|
306
|
+
if (!Array.isArray(value) || value.length > AGENT_PROTOCOL_LIMITS.sketchCount) {
|
|
307
|
+
throw new TypeError(`attachments must contain at most ${AGENT_PROTOCOL_LIMITS.sketchCount} items`);
|
|
308
|
+
}
|
|
309
|
+
const attachments = value.map((candidate, index) => {
|
|
310
|
+
if (!isAgentSketchAttachment(candidate)) {
|
|
311
|
+
throw new TypeError(`attachments[${index}] is not a valid PNG attachment`);
|
|
312
|
+
}
|
|
313
|
+
return { ...candidate, capture: { ...candidate.capture } };
|
|
314
|
+
});
|
|
315
|
+
if (new Set(attachments.map((attachment) => `${attachment.id}\u0000${attachment.revision}`)).size
|
|
316
|
+
!== attachments.length) {
|
|
317
|
+
throw new TypeError("attachments must not contain duplicate sketch revisions");
|
|
318
|
+
}
|
|
319
|
+
const totalBytes = attachments.reduce((total, attachment) => total + attachment.byteSize, 0);
|
|
320
|
+
if (totalBytes > AGENT_PROTOCOL_LIMITS.sketchTotalBytes) {
|
|
321
|
+
throw new TypeError(`attachments must contain at most ${AGENT_PROTOCOL_LIMITS.sketchTotalBytes} decoded bytes`);
|
|
322
|
+
}
|
|
323
|
+
return attachments;
|
|
324
|
+
}
|
|
325
|
+
function validSketchMetadataList(value) {
|
|
326
|
+
if (!Array.isArray(value) || value.length > AGENT_PROTOCOL_LIMITS.sketchCount
|
|
327
|
+
|| !value.every(isAgentSketchMetadata))
|
|
328
|
+
return false;
|
|
329
|
+
return new Set(value.map((sketch) => `${sketch.id}\u0000${sketch.revision}`)).size === value.length;
|
|
330
|
+
}
|
|
160
331
|
export function isAgentPromptRequest(value) {
|
|
161
|
-
if (!isRecord(value) || !hasOnlyKeys(value, [
|
|
332
|
+
if (!isRecord(value) || !hasOnlyKeys(value, [
|
|
333
|
+
"requestId",
|
|
334
|
+
"projectId",
|
|
335
|
+
"prompt",
|
|
336
|
+
"changeRevision",
|
|
337
|
+
"clientDispatchId",
|
|
338
|
+
"sketches",
|
|
339
|
+
]))
|
|
162
340
|
return false;
|
|
163
341
|
return nonEmptyString(value.requestId, AGENT_PROTOCOL_LIMITS.requestId)
|
|
164
342
|
&& nonEmptyString(value.projectId, AGENT_PROTOCOL_LIMITS.projectId)
|
|
165
343
|
&& nonEmptyString(value.prompt, AGENT_PROTOCOL_LIMITS.prompt)
|
|
166
|
-
&& optionalRevision(value.changeRevision)
|
|
344
|
+
&& optionalRevision(value.changeRevision)
|
|
345
|
+
&& (value.clientDispatchId === undefined || nonEmptyString(value.clientDispatchId, AGENT_PROTOCOL_LIMITS.clientDispatchId))
|
|
346
|
+
&& (value.sketches === undefined || validSketchMetadataList(value.sketches));
|
|
167
347
|
}
|
|
168
348
|
export function isAgentStatusUpdate(value) {
|
|
169
349
|
if (!isRecord(value) || !hasOnlyKeys(value, ["requestId", "status", "summary", "error"]))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AGENT_PROTOCOL_VERSION } from "./internal/agent-protocol.js";
|
|
2
|
+
import { type BridgeAddress, type BrowserBridge } from "./bridge.js";
|
|
3
|
+
declare const REGISTRY_SCHEMA_VERSION: 1;
|
|
4
|
+
export interface ProjectSession {
|
|
5
|
+
readonly schemaVersion: typeof REGISTRY_SCHEMA_VERSION;
|
|
6
|
+
readonly protocolVersion: typeof AGENT_PROTOCOL_VERSION;
|
|
7
|
+
readonly sessionId: string;
|
|
8
|
+
readonly projectId: string;
|
|
9
|
+
readonly workspaceRoot: string;
|
|
10
|
+
readonly appRoot: string;
|
|
11
|
+
readonly gitCommonDir?: string;
|
|
12
|
+
readonly branch?: string;
|
|
13
|
+
readonly origin: string;
|
|
14
|
+
readonly endpoint: string;
|
|
15
|
+
readonly startedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ProjectBridgeOptions {
|
|
18
|
+
readonly appRoot: string;
|
|
19
|
+
readonly workspaceRoot?: string;
|
|
20
|
+
readonly projectId?: string;
|
|
21
|
+
readonly origin: string;
|
|
22
|
+
/** Other explicit browser origins, such as loopback aliases on the same port. */
|
|
23
|
+
readonly allowedOrigins?: readonly string[];
|
|
24
|
+
readonly host?: "127.0.0.1" | "::1" | "localhost";
|
|
25
|
+
readonly port?: number;
|
|
26
|
+
readonly commandTimeoutMs?: number;
|
|
27
|
+
/** Test/embedding override. Production uses a private per-user directory. */
|
|
28
|
+
readonly registryRoot?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ProjectBridgeRuntime {
|
|
31
|
+
readonly bridge: BrowserBridge;
|
|
32
|
+
readonly address: BridgeAddress;
|
|
33
|
+
readonly session: ProjectSession;
|
|
34
|
+
readonly browser: {
|
|
35
|
+
readonly projectId: string;
|
|
36
|
+
readonly origin: string;
|
|
37
|
+
readonly bridgeUrl: string;
|
|
38
|
+
};
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export interface StoredProjectSession extends ProjectSession {
|
|
42
|
+
readonly pid: number;
|
|
43
|
+
readonly controlToken: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function defaultSessionRegistryRoot(): string;
|
|
46
|
+
/** Resolves a checkout or worktree without conflating related worktrees. */
|
|
47
|
+
export declare function resolveWorkspaceRoot(input: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Starts the project-owned browser bridge and registers it for local MCP
|
|
50
|
+
* adapters. The descriptor contains a private control credential and is never
|
|
51
|
+
* suitable for browser bundles or user-facing tool results.
|
|
52
|
+
*/
|
|
53
|
+
export declare function startProjectBridge(options: ProjectBridgeOptions): Promise<ProjectBridgeRuntime>;
|
|
54
|
+
export {};
|