@nudge-ui/mcp 0.2.4 → 0.2.5

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.
@@ -1,6 +1,6 @@
1
1
  import type { AgentActivity } from "./internal/agent-protocol.js";
2
- import type { AgentPromptRequest, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult } from "./internal/agent-protocol.js";
3
- import type { CanvasCommandInput } from "./bridge.js";
2
+ import type { AgentDeliveredPrompt, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult } from "./internal/agent-protocol.js";
3
+ import type { CanvasCommandInput } from "./bridgeSession.js";
4
4
  import { type ProjectSession, type StoredProjectSession } from "./project.js";
5
5
  export interface DiscoveredProjectSession extends ProjectSession {
6
6
  readonly matchesWorkspace: boolean;
@@ -9,7 +9,7 @@ export interface DiscoveredProjectSession extends ProjectSession {
9
9
  readonly status: Pick<AgentStatusSnapshot, "connection" | "listenerActive" | "paired">;
10
10
  }
11
11
  export declare function discoverProjectSessions(applicationRoot: string | undefined, registryRoot?: string): Promise<DiscoveredProjectSession[]>;
12
- /** Routes MCP tools to a live project in the adapter's exact worktree. */
12
+ /** Routes MCP tools to one selected live project, following it across bridge restarts. */
13
13
  export declare class DiscoveredProjectRouter {
14
14
  readonly workspaceRoot: string;
15
15
  readonly applicationRoot: string;
@@ -21,11 +21,10 @@ export declare class DiscoveredProjectRouter {
21
21
  private constructor();
22
22
  get selectedSessionId(): string | undefined;
23
23
  static forSession(session: StoredProjectSession, registryRoot: string): DiscoveredProjectRouter;
24
- static create(workspaceRoot: string, registryRoot?: string): Promise<DiscoveredProjectRouter>;
25
- listSessions(): Promise<DiscoveredProjectSession[]>;
26
24
  private connectToSession;
27
25
  private selectSession;
28
- waitForPrompt(signal?: AbortSignal, sessionId?: string, agentClientName?: string): Promise<AgentPromptRequest>;
26
+ waitForPrompt(signal?: AbortSignal, sessionId?: string, agentClientName?: string): Promise<AgentDeliveredPrompt>;
27
+ getRequest(requestId: string): Promise<AgentDeliveredPrompt>;
29
28
  getStatus(): Promise<AgentStatusSnapshot>;
30
29
  reportActivity(activity: AgentActivity): Promise<void>;
31
30
  updateRequestStatus(update: AgentStatusUpdate): Promise<AgentStatusSnapshot>;
package/dist/discovery.js CHANGED
@@ -33,7 +33,7 @@ export async function discoverProjectSessions(applicationRoot, registryRoot = de
33
33
  };
34
34
  }).sort((left, right) => right.startedAt.localeCompare(left.startedAt));
35
35
  }
36
- /** Routes MCP tools to a live project in the adapter's exact worktree. */
36
+ /** Routes MCP tools to one selected live project, following it across bridge restarts. */
37
37
  export class DiscoveredProjectRouter {
38
38
  workspaceRoot;
39
39
  applicationRoot;
@@ -56,13 +56,6 @@ export class DiscoveredProjectRouter {
56
56
  router.client = new ProjectSessionClient(session, router.agentId);
57
57
  return router;
58
58
  }
59
- static async create(workspaceRoot, registryRoot = defaultSessionRegistryRoot()) {
60
- const applicationRoot = await realpath(requireAbsoluteWorkspaceRoot(workspaceRoot));
61
- return new DiscoveredProjectRouter(await resolveWorkspaceRoot(applicationRoot), applicationRoot, resolve(registryRoot), randomUUID());
62
- }
63
- async listSessions() {
64
- return await discoverProjectSessions(this.applicationRoot, this.registryRoot);
65
- }
66
59
  connectToSession(sessionId) {
67
60
  const connection = this.connectionQueue.then(() => this.selectSession(sessionId));
68
61
  this.connectionQueue = connection.then(() => undefined, () => undefined);
@@ -104,6 +97,10 @@ export class DiscoveredProjectRouter {
104
97
  return await replacement.waitForPrompt(signal, agentClientName);
105
98
  }
106
99
  }
100
+ async getRequest(requestId) {
101
+ const client = await this.connectToSession();
102
+ return await client.getRequest(requestId);
103
+ }
107
104
  async getStatus() {
108
105
  const client = await this.connectToSession();
109
106
  return await client.getStatus();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { createAgentCompanion, createDiscoveredAgentAdapter, type AgentCompanion, type AgentCompanionOptions, type DiscoveredAgentAdapter, } from "./server.js";
1
+ export { createDiscoveredAgentAdapter, type DiscoveredAgentAdapter, } from "./server.js";
2
2
  export { DiscoveredProjectRouter, discoverProjectSessions, type DiscoveredProjectSession, } from "./discovery.js";
3
3
  export { defaultSessionRegistryRoot, resolveWorkspaceRoot, startProjectBridge, type ProjectBridgeOptions, type ProjectBridgeRuntime, type ProjectSession, } from "./project.js";
4
- export { createLoopbackBridge, type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress, type BridgeClock, type CanvasCommandInput, } from "./bridge.js";
5
- export { openPairedPage, parseCliArguments, runCli, type CliOptions, type PageLaunchDependencies, } from "./cli.js";
4
+ export { createLoopbackBridge, type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress, } from "./bridge.js";
5
+ export { BridgeSessionError, type BridgeClock, type BridgeSessionErrorCode, type CanvasCommandInput, type ListenOptions, } from "./bridgeSession.js";
6
6
  export * from "./protocol.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- export { createAgentCompanion, createDiscoveredAgentAdapter, } from "./server.js";
1
+ export { createDiscoveredAgentAdapter, } from "./server.js";
2
2
  export { DiscoveredProjectRouter, discoverProjectSessions, } from "./discovery.js";
3
3
  export { defaultSessionRegistryRoot, resolveWorkspaceRoot, startProjectBridge, } from "./project.js";
4
4
  export { createLoopbackBridge, } from "./bridge.js";
5
- export { openPairedPage, parseCliArguments, runCli, } from "./cli.js";
5
+ export { BridgeSessionError, } from "./bridgeSession.js";
6
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: 2;
9
+ export declare const AGENT_PROTOCOL_VERSION: 3;
10
10
  export declare const AGENT_PROTOCOL_LIMITS: {
11
11
  readonly projectId: 256;
12
12
  readonly workspaceRoot: 4096;
@@ -35,15 +35,8 @@ export declare const AGENT_PROTOCOL_LIMITS: {
35
35
  readonly routeTitle: 512;
36
36
  readonly routeCount: 64;
37
37
  readonly canvasCardCount: 128;
38
+ readonly agentClientName: 120;
38
39
  };
39
- /**
40
- * Returns the default project-specific loopback port used by the CLI.
41
- *
42
- * The value is deterministic so a browser can probe the companion without a
43
- * port file, while the range stays high enough to avoid common dev servers.
44
- * Callers may still override it with an explicit port when a collision occurs.
45
- */
46
- export declare function defaultBridgePort(projectId: string): number;
47
40
  export type AgentProtocolVersion = typeof AGENT_PROTOCOL_VERSION;
48
41
  /** A project identity is the pairing boundary for one companion process. */
49
42
  export interface AgentProjectIdentity {
@@ -164,14 +157,14 @@ export interface AgentSketchAttachment extends AgentSketchMetadata {
164
157
  export interface AgentDeliveredPrompt extends AgentPromptRequest {
165
158
  readonly attachments?: readonly AgentSketchAttachment[];
166
159
  }
167
- export type AgentRequestStatus = "working" | "completed" | "failed" | "interrupted";
160
+ export type AgentRequestStatus = "delivered" | "working" | "completed" | "failed" | "interrupted";
168
161
  export interface AgentRequestOutcome {
169
162
  readonly requestId: string;
170
- readonly status: AgentRequestStatus;
163
+ readonly status: Exclude<AgentRequestStatus, "delivered">;
171
164
  readonly summary?: string;
172
165
  readonly error?: string;
173
166
  }
174
- export type AgentConnectionState = "offline" | "listening" | "paired" | "working";
167
+ export type AgentConnectionState = "offline" | "listening" | "paired" | "delivered" | "working";
175
168
  export interface AgentStatusSnapshot {
176
169
  readonly protocolVersion: AgentProtocolVersion;
177
170
  readonly projectId: string;
@@ -269,7 +262,7 @@ export interface StatusEventRequest {
269
262
  }
270
263
  export interface AgentStatusUpdate {
271
264
  readonly requestId: string;
272
- readonly status: AgentRequestStatus;
265
+ readonly status: Exclude<AgentRequestStatus, "delivered">;
273
266
  readonly summary?: string;
274
267
  readonly error?: string;
275
268
  }
@@ -277,6 +270,29 @@ export interface ProtocolErrorShape {
277
270
  readonly code: string;
278
271
  readonly message: string;
279
272
  }
273
+ /** Options a development host passes to the optional project bridge. */
274
+ export interface ProjectBridgeStartOptions {
275
+ readonly appRoot: string;
276
+ readonly projectId: string;
277
+ readonly origin: string;
278
+ /** Other explicit browser origins, such as loopback aliases on the same port. */
279
+ readonly allowedOrigins?: readonly string[];
280
+ }
281
+ /** The started project bridge as seen by the development host that owns it. */
282
+ export interface ProjectBridgeHandle {
283
+ readonly browser: {
284
+ readonly projectId: string;
285
+ readonly origin: string;
286
+ readonly bridgeUrl: string;
287
+ };
288
+ close(): Promise<void>;
289
+ }
290
+ /**
291
+ * The `startProjectBridge` export of `@nudge-ui/mcp/project`. Hosts load it
292
+ * dynamically from the application's own dependencies, so this type is the
293
+ * only compile-time link between the two packages.
294
+ */
295
+ export type StartProjectBridge = (options: ProjectBridgeStartOptions) => Promise<ProjectBridgeHandle>;
280
296
  /**
281
297
  * Returns a canonical origin or null. Credentials, paths, and fragments are
282
298
  * rejected so the browser cannot broaden the pairing boundary accidentally.
@@ -309,5 +325,26 @@ export declare function isAgentStatusUpdate(value: unknown): value is AgentStatu
309
325
  export declare function isCanvasState(value: unknown): value is CanvasState;
310
326
  export declare function isCanvasGroup(value: unknown): value is CanvasGroup;
311
327
  export declare function isCanvasCommand(value: unknown): value is CanvasCommand;
328
+ export declare function isAgentDeliveredPrompt(value: unknown): value is AgentDeliveredPrompt;
329
+ /** A Canvas acknowledgement or result; a successful one never carries an error. */
330
+ export declare function isCanvasCommandResult(value: unknown): value is CanvasCommandResult;
331
+ /** The connection state shown for a pairing, a listener, and the current request. */
332
+ export declare function deriveConnectionState(status: Pick<AgentStatusSnapshot, "listenerActive" | "paired" | "request">): AgentConnectionState;
333
+ /**
334
+ * Decodes a status snapshot from any peer. Unknown fields are dropped and the
335
+ * connection state is recomputed, so a snapshot can never claim a state its
336
+ * own fields contradict.
337
+ */
338
+ export declare function decodeAgentStatusSnapshot(value: unknown): AgentStatusSnapshot | null;
339
+ /**
340
+ * Decodes the event inside a bridge envelope. A Canvas command is passed
341
+ * through as received: the Canvas controller validates it so a malformed
342
+ * command still receives an error acknowledgement instead of a timeout.
343
+ */
344
+ export declare function decodeBridgeEvent(value: unknown): BridgeEvent | null;
345
+ /** Decodes a pairing or takeover response for the exact project and origin requested. */
346
+ export declare function decodePairingResponse(value: unknown, expected: Pick<PairingRequest, "projectId" | "origin">): PairingResponse | null;
347
+ /** Decodes the accepted-prompt response for the project that dispatched it. */
348
+ export declare function decodePromptDispatchResponse(value: unknown, projectId: string): PromptDispatchResponse | null;
312
349
  /** Creates a stable protocol error shape without exposing thrown values. */
313
350
  export declare function protocolError(code: string, message: string): ProtocolErrorShape;
@@ -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 = 2;
9
+ export const AGENT_PROTOCOL_VERSION = 3;
10
10
  export const AGENT_PROTOCOL_LIMITS = {
11
11
  projectId: 256,
12
12
  workspaceRoot: 4096,
@@ -35,25 +35,8 @@ export const AGENT_PROTOCOL_LIMITS = {
35
35
  routeTitle: 512,
36
36
  routeCount: 64,
37
37
  canvasCardCount: 128,
38
+ agentClientName: 120,
38
39
  };
39
- /**
40
- * Returns the default project-specific loopback port used by the CLI.
41
- *
42
- * The value is deterministic so a browser can probe the companion without a
43
- * port file, while the range stays high enough to avoid common dev servers.
44
- * Callers may still override it with an explicit port when a collision occurs.
45
- */
46
- export function defaultBridgePort(projectId) {
47
- if (!nonEmptyString(projectId, AGENT_PROTOCOL_LIMITS.projectId)) {
48
- throw new TypeError("projectId must be a bounded non-empty string");
49
- }
50
- let hash = 2_166_136_261;
51
- for (let index = 0; index < projectId.length; index += 1) {
52
- hash ^= projectId.charCodeAt(index);
53
- hash = Math.imul(hash, 16_777_619);
54
- }
55
- return 30_000 + ((hash >>> 0) % 20_000);
56
- }
57
40
  export const CANVAS_COMMAND_TYPES = [
58
41
  "read-state",
59
42
  "present-routes",
@@ -421,6 +404,149 @@ export function isCanvasCommand(value) {
421
404
  return false;
422
405
  }
423
406
  }
407
+ export function isAgentDeliveredPrompt(value) {
408
+ if (!isRecord(value))
409
+ return false;
410
+ const { attachments, ...request } = value;
411
+ return isAgentPromptRequest(request)
412
+ && (attachments === undefined || (Array.isArray(attachments)
413
+ && attachments.length <= AGENT_PROTOCOL_LIMITS.sketchCount
414
+ && attachments.every(isAgentSketchAttachment)));
415
+ }
416
+ function isCanvasCommandError(value) {
417
+ return isRecord(value) && hasOnlyKeys(value, ["code", "message"])
418
+ && nonEmptyString(value.code, 128)
419
+ && typeof value.message === "string" && value.message.length <= AGENT_PROTOCOL_LIMITS.prompt;
420
+ }
421
+ /** A Canvas acknowledgement or result; a successful one never carries an error. */
422
+ export function isCanvasCommandResult(value) {
423
+ if (!isRecord(value) || !hasOnlyKeys(value, ["commandId", "ok", "state", "group", "error"]))
424
+ return false;
425
+ return nonEmptyString(value.commandId, AGENT_PROTOCOL_LIMITS.commandId)
426
+ && typeof value.ok === "boolean"
427
+ && (value.state === undefined || isCanvasState(value.state))
428
+ && (value.group === undefined || isCanvasGroup(value.group))
429
+ && (value.error === undefined || (!value.ok && isCanvasCommandError(value.error)));
430
+ }
431
+ /** The connection state shown for a pairing, a listener, and the current request. */
432
+ export function deriveConnectionState(status) {
433
+ if (status.request?.status === "delivered")
434
+ return "delivered";
435
+ if (status.request?.status === "working")
436
+ return "working";
437
+ if (status.paired)
438
+ return "paired";
439
+ if (status.listenerActive)
440
+ return "listening";
441
+ return "offline";
442
+ }
443
+ function isRequestStatus(value) {
444
+ return value === "delivered" || value === "working" || value === "completed" || value === "failed" || value === "interrupted";
445
+ }
446
+ function decodeStatusRequest(value) {
447
+ if (value === null || value === undefined)
448
+ return null;
449
+ if (!isRecord(value))
450
+ return undefined;
451
+ const { status, summary, error, ...request } = value;
452
+ if (!isRequestStatus(status) || !isAgentPromptRequest(request)
453
+ || !optionalString(summary, AGENT_PROTOCOL_LIMITS.prompt)
454
+ || !optionalString(error, AGENT_PROTOCOL_LIMITS.prompt))
455
+ return undefined;
456
+ return {
457
+ ...request,
458
+ status,
459
+ ...(summary === undefined ? {} : { summary }),
460
+ ...(error === undefined ? {} : { error }),
461
+ };
462
+ }
463
+ /**
464
+ * Decodes a status snapshot from any peer. Unknown fields are dropped and the
465
+ * connection state is recomputed, so a snapshot can never claim a state its
466
+ * own fields contradict.
467
+ */
468
+ export function decodeAgentStatusSnapshot(value) {
469
+ if (!isRecord(value)
470
+ || value.protocolVersion !== AGENT_PROTOCOL_VERSION
471
+ || !nonEmptyString(value.projectId, AGENT_PROTOCOL_LIMITS.projectId)
472
+ || (value.connection !== "offline" && value.connection !== "listening"
473
+ && value.connection !== "paired" && value.connection !== "delivered" && value.connection !== "working")
474
+ || typeof value.listenerActive !== "boolean"
475
+ || typeof value.paired !== "boolean"
476
+ || !(value.pageUrl === undefined || value.pageUrl === null
477
+ || nonEmptyString(value.pageUrl, AGENT_PROTOCOL_LIMITS.routeUrl)))
478
+ return null;
479
+ const request = decodeStatusRequest(value.request);
480
+ if (request === undefined)
481
+ return null;
482
+ const fields = { listenerActive: value.listenerActive, paired: value.paired, request };
483
+ return {
484
+ protocolVersion: AGENT_PROTOCOL_VERSION,
485
+ projectId: value.projectId,
486
+ connection: deriveConnectionState(fields),
487
+ ...fields,
488
+ ...(value.pageUrl === undefined ? {} : { pageUrl: value.pageUrl }),
489
+ ...(typeof value.agentClientName === "string" && value.agentClientName.length > 0
490
+ ? { agentClientName: value.agentClientName.slice(0, AGENT_PROTOCOL_LIMITS.agentClientName) }
491
+ : {}),
492
+ };
493
+ }
494
+ /**
495
+ * Decodes the event inside a bridge envelope. A Canvas command is passed
496
+ * through as received: the Canvas controller validates it so a malformed
497
+ * command still receives an error acknowledgement instead of a timeout.
498
+ */
499
+ export function decodeBridgeEvent(value) {
500
+ if (!isRecord(value))
501
+ return null;
502
+ switch (value.type) {
503
+ case "status":
504
+ case "connected": {
505
+ const status = decodeAgentStatusSnapshot(value.status);
506
+ return status ? { type: value.type, status } : null;
507
+ }
508
+ case "disconnected":
509
+ return {
510
+ type: "disconnected",
511
+ ...(nonEmptyString(value.reason, AGENT_PROTOCOL_LIMITS.prompt) ? { reason: value.reason } : {}),
512
+ };
513
+ case "activity":
514
+ return isAgentActivity(value.activity) ? { type: "activity", activity: value.activity } : null;
515
+ case "canvas-command":
516
+ // SAFETY: the Canvas controller validates the command before acting on it.
517
+ return isRecord(value.command) ? { type: "canvas-command", command: value.command } : null;
518
+ default:
519
+ return null;
520
+ }
521
+ }
522
+ /** Decodes a pairing or takeover response for the exact project and origin requested. */
523
+ export function decodePairingResponse(value, expected) {
524
+ if (!isRecord(value)
525
+ || value.protocolVersion !== AGENT_PROTOCOL_VERSION
526
+ || value.projectId !== expected.projectId
527
+ || value.origin !== expected.origin
528
+ || !nonEmptyString(value.sessionToken, AGENT_PROTOCOL_LIMITS.sessionToken)
529
+ || !(value.pageUrl === undefined || nonEmptyString(value.pageUrl, AGENT_PROTOCOL_LIMITS.routeUrl)))
530
+ return null;
531
+ const status = decodeAgentStatusSnapshot(value.status);
532
+ if (!status)
533
+ return null;
534
+ return {
535
+ protocolVersion: AGENT_PROTOCOL_VERSION,
536
+ projectId: expected.projectId,
537
+ origin: expected.origin,
538
+ sessionToken: value.sessionToken,
539
+ ...(value.pageUrl === undefined ? {} : { pageUrl: value.pageUrl }),
540
+ status,
541
+ };
542
+ }
543
+ /** Decodes the accepted-prompt response for the project that dispatched it. */
544
+ export function decodePromptDispatchResponse(value, projectId) {
545
+ if (!isRecord(value) || (value.status !== "delivered" && value.status !== "working") || !isAgentPromptRequest(value.request)
546
+ || value.request.projectId !== projectId)
547
+ return null;
548
+ return { request: value.request, status: value.status };
549
+ }
424
550
  /** Creates a stable protocol error shape without exposing thrown values. */
425
551
  export function protocolError(code, message) {
426
552
  if (!nonEmptyString(code, 128) || !nonEmptyString(message, AGENT_PROTOCOL_LIMITS.prompt)) {
package/dist/project.d.ts CHANGED
@@ -50,5 +50,5 @@ export declare function resolveWorkspaceRoot(input: string): Promise<string>;
50
50
  * adapters. The descriptor contains a private control credential and is never
51
51
  * suitable for browser bundles or user-facing tool results.
52
52
  */
53
- export declare function startProjectBridge(options: ProjectBridgeOptions): Promise<ProjectBridgeRuntime>;
53
+ export declare const startProjectBridge: (options: ProjectBridgeOptions) => Promise<ProjectBridgeRuntime>;
54
54
  export {};
package/dist/project.js CHANGED
@@ -97,7 +97,7 @@ async function closeProjectBridge(descriptorPath, bridge) {
97
97
  * adapters. The descriptor contains a private control credential and is never
98
98
  * suitable for browser bundles or user-facing tool results.
99
99
  */
100
- export async function startProjectBridge(options) {
100
+ export const startProjectBridge = (async (options) => {
101
101
  const origin = canonicalOrigin(options.origin);
102
102
  if (!origin)
103
103
  throw new TypeError("origin must be a canonical HTTP(S) origin");
@@ -148,4 +148,4 @@ export async function startProjectBridge(options) {
148
148
  browser: { projectId, origin, bridgeUrl: address.url },
149
149
  close,
150
150
  };
151
- }
151
+ });
@@ -1,6 +1,5 @@
1
- import type { AgentActivity } from "./internal/agent-protocol.js";
2
- import type { AgentPromptRequest, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult } from "./internal/agent-protocol.js";
3
- import type { CanvasCommandInput } from "./bridge.js";
1
+ import type { AgentActivity, AgentDeliveredPrompt, AgentStatusSnapshot, AgentStatusUpdate, CanvasCommandResult } from "./internal/agent-protocol.js";
2
+ import type { CanvasCommandInput } from "./bridgeSession.js";
4
3
  import type { StoredProjectSession } from "./project.js";
5
4
  export interface SessionHealth {
6
5
  readonly projectId: string;
@@ -13,6 +12,7 @@ export declare class ProjectSessionResponseError extends Error {
13
12
  readonly status: number;
14
13
  constructor(status: number, message: string);
15
14
  }
15
+ /** Reads and validates a bridge's health; an invalid body is a classified response error. */
16
16
  export declare function readSessionHealth(session: StoredProjectSession, timeoutMs?: number): Promise<SessionHealth>;
17
17
  /** Owns one project's claim, heartbeat, and authenticated control requests. */
18
18
  export declare class ProjectSessionClient {
@@ -26,7 +26,8 @@ export declare class ProjectSessionClient {
26
26
  private send;
27
27
  private acquireClaim;
28
28
  private claim;
29
- waitForPrompt(signal?: AbortSignal, agentClientName?: string): Promise<AgentPromptRequest>;
29
+ waitForPrompt(signal?: AbortSignal, agentClientName?: string): Promise<AgentDeliveredPrompt>;
30
+ getRequest(requestId: string): Promise<AgentDeliveredPrompt>;
30
31
  getStatus(): Promise<AgentStatusSnapshot>;
31
32
  reportActivity(activity: AgentActivity): Promise<void>;
32
33
  updateRequestStatus(update: AgentStatusUpdate): Promise<AgentStatusSnapshot>;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { decodeAgentStatusSnapshot, isAgentDeliveredPrompt, isCanvasCommandResult, } from "./internal/agent-protocol.js";
2
3
  import { AGENT_CONTROL_ENDPOINTS } from "./protocol.js";
3
4
  const CONTROL_TIMEOUT_MS = 5_000;
4
5
  const CANVAS_TIMEOUT_MS = 65_000;
@@ -11,7 +12,21 @@ export class ProjectSessionResponseError extends Error {
11
12
  this.status = status;
12
13
  }
13
14
  }
14
- async function requestJson(session, path, init = {}, { signal, timeoutMs = CONTROL_TIMEOUT_MS } = {}) {
15
+ const ignoreBody = () => undefined;
16
+ function decodeSessionHealth(body) {
17
+ if (!body || typeof body !== "object" || Array.isArray(body))
18
+ return null;
19
+ const health = body;
20
+ const status = decodeAgentStatusSnapshot(health.status);
21
+ if (typeof health.projectId !== "string" || health.available !== true
22
+ || typeof health.claimed !== "boolean" || !status)
23
+ return null;
24
+ return { projectId: health.projectId, available: true, claimed: health.claimed, status };
25
+ }
26
+ function guarded(guard) {
27
+ return (body) => guard(body) ? body : null;
28
+ }
29
+ async function requestJson(session, path, decode, init = {}, { signal, timeoutMs = CONTROL_TIMEOUT_MS } = {}) {
15
30
  const response = await fetch(`${session.endpoint}${path}`, {
16
31
  ...init,
17
32
  redirect: "error",
@@ -24,12 +39,18 @@ async function requestJson(session, path, init = {}, { signal, timeoutMs = CONTR
24
39
  });
25
40
  const body = await response.json().catch(() => undefined);
26
41
  if (!response.ok) {
27
- throw new ProjectSessionResponseError(response.status, body?.error?.message ?? `Nudge project bridge returned ${response.status}.`);
42
+ const error = body?.error;
43
+ throw new ProjectSessionResponseError(response.status, typeof error?.message === "string" ? error.message : `Nudge project bridge returned ${response.status}.`);
44
+ }
45
+ const decoded = decode(body);
46
+ if (decoded === null) {
47
+ throw new ProjectSessionResponseError(response.status, `Nudge project bridge returned an invalid ${path} response.`);
28
48
  }
29
- return body;
49
+ return decoded;
30
50
  }
51
+ /** Reads and validates a bridge's health; an invalid body is a classified response error. */
31
52
  export async function readSessionHealth(session, timeoutMs = CONTROL_TIMEOUT_MS) {
32
- return await requestJson(session, AGENT_CONTROL_ENDPOINTS.health, {}, { timeoutMs });
53
+ return await requestJson(session, AGENT_CONTROL_ENDPOINTS.health, decodeSessionHealth, {}, { timeoutMs });
33
54
  }
34
55
  /** Owns one project's claim, heartbeat, and authenticated control requests. */
35
56
  export class ProjectSessionClient {
@@ -43,17 +64,17 @@ export class ProjectSessionClient {
43
64
  this.descriptor = descriptor;
44
65
  this.agentId = agentId;
45
66
  }
46
- async send(path, payload = {}, options) {
47
- return await requestJson(this.descriptor, path, {
67
+ async send(path, decode, payload = {}, options) {
68
+ return await requestJson(this.descriptor, path, decode, {
48
69
  method: "POST",
49
70
  body: JSON.stringify({ agentId: this.agentId, ...payload }),
50
71
  }, options);
51
72
  }
52
73
  async acquireClaim() {
53
- await this.send(AGENT_CONTROL_ENDPOINTS.claim);
74
+ await this.send(AGENT_CONTROL_ENDPOINTS.claim, ignoreBody);
54
75
  this.claimed = true;
55
76
  this.heartbeat = setInterval(() => {
56
- void this.send(AGENT_CONTROL_ENDPOINTS.heartbeat).catch(() => undefined);
77
+ void this.send(AGENT_CONTROL_ENDPOINTS.heartbeat, ignoreBody).catch(() => undefined);
57
78
  }, 10_000);
58
79
  this.heartbeat.unref();
59
80
  }
@@ -74,7 +95,11 @@ export class ProjectSessionClient {
74
95
  }
75
96
  async waitForPrompt(signal, agentClientName) {
76
97
  await this.claim();
77
- return await this.send(AGENT_CONTROL_ENDPOINTS.listen, { agentClientName }, { signal, timeoutMs: null });
98
+ return await this.send(AGENT_CONTROL_ENDPOINTS.listen, guarded(isAgentDeliveredPrompt), { agentClientName }, { signal, timeoutMs: null });
99
+ }
100
+ async getRequest(requestId) {
101
+ await this.claim();
102
+ return await this.send(AGENT_CONTROL_ENDPOINTS.request, guarded(isAgentDeliveredPrompt), { requestId });
78
103
  }
79
104
  async getStatus() {
80
105
  const health = await readSessionHealth(this.descriptor);
@@ -82,16 +107,16 @@ export class ProjectSessionClient {
82
107
  }
83
108
  async reportActivity(activity) {
84
109
  await this.claim();
85
- await this.send(AGENT_CONTROL_ENDPOINTS.activity, { activity });
110
+ await this.send(AGENT_CONTROL_ENDPOINTS.activity, ignoreBody, { activity });
86
111
  }
87
112
  async updateRequestStatus(update) {
88
113
  await this.claim();
89
- return await this.send(AGENT_CONTROL_ENDPOINTS.reportStatus, { update });
114
+ return await this.send(AGENT_CONTROL_ENDPOINTS.reportStatus, decodeAgentStatusSnapshot, { update });
90
115
  }
91
116
  async dispatchCanvasCommand(input) {
92
117
  await this.claim();
93
118
  const command = { ...input, commandId: input.commandId ?? randomUUID() };
94
- return await this.send(AGENT_CONTROL_ENDPOINTS.canvas, { command }, { timeoutMs: CANVAS_TIMEOUT_MS });
119
+ return await this.send(AGENT_CONTROL_ENDPOINTS.canvas, guarded(isCanvasCommandResult), { command }, { timeoutMs: CANVAS_TIMEOUT_MS });
95
120
  }
96
121
  async close() {
97
122
  this.closing = true;
@@ -102,6 +127,6 @@ export class ProjectSessionClient {
102
127
  if (this.heartbeat)
103
128
  clearInterval(this.heartbeat);
104
129
  this.heartbeat = null;
105
- await this.send(AGENT_CONTROL_ENDPOINTS.release).catch(() => undefined);
130
+ await this.send(AGENT_CONTROL_ENDPOINTS.release, ignoreBody).catch(() => undefined);
106
131
  }
107
132
  }
@@ -5,7 +5,7 @@
5
5
  * subpath keeps transport names discoverable for Node consumers while
6
6
  * preserving one protocol vocabulary for browser and agent adapters.
7
7
  */
8
- export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentSketchAttachment, isAgentSketchCaptureMetadata, isAgentSketchMetadata, isAgentStatusUpdate, isAgentActivity, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateSketchAttachments, validateRoutes, } from "./internal/agent-protocol.js";
8
+ export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, isAgentPromptRequest, isAgentSketchAttachment, isAgentSketchCaptureMetadata, isAgentSketchMetadata, isAgentStatusUpdate, isAgentActivity, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateSketchAttachments, validateRoutes, } from "./internal/agent-protocol.js";
9
9
  export type { AgentActivity, BridgeActivityEvent, AgentConnectionState, AgentProjectIdentity, AgentPromptRequest, AgentDeliveredPrompt, AgentSketchAttachment, AgentSketchCaptureMetadata, AgentSketchMetadata, AgentRequestOutcome, AgentRequestStatus, AgentRoute, AgentStatusSnapshot, AgentStatusUpdate, BridgeCanvasCommandEvent, BridgeConnectedEvent, BridgeDisconnectedEvent, BridgeEnvelope, BridgeEvent, BridgeEventType, BridgeStatusEvent, CanvasCommand, CanvasCommandAcknowledgement, CanvasCommandError, CanvasCommandResult, CanvasGroup, CanvasGroupOwner, CanvasState, FitCanvasCommand, FocusCanvasGroupCommand, PairingRequest, PairingResponse, PresentRoutesCommand, PromptDispatchRequest, PromptDispatchResponse, ReadCanvasStateCommand, RemoveCanvasGroupCommand, StatusEventRequest, } from "./internal/agent-protocol.js";
10
10
  /** Stable endpoint names used by the loopback browser bridge. */
11
11
  export declare const BRIDGE_ENDPOINTS: {
@@ -26,6 +26,7 @@ export declare const AGENT_CONTROL_ENDPOINTS: {
26
26
  readonly release: "/__nudge/agent/release";
27
27
  readonly listen: "/__nudge/agent/listen";
28
28
  readonly status: "/__nudge/agent/status";
29
+ readonly request: "/__nudge/agent/request";
29
30
  readonly reportStatus: "/__nudge/agent/report-status";
30
31
  readonly activity: "/__nudge/agent/activity";
31
32
  readonly canvas: "/__nudge/agent/canvas";
package/dist/protocol.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * subpath keeps transport names discoverable for Node consumers while
6
6
  * preserving one protocol vocabulary for browser and agent adapters.
7
7
  */
8
- export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, defaultBridgePort, isAgentPromptRequest, isAgentSketchAttachment, isAgentSketchCaptureMetadata, isAgentSketchMetadata, isAgentStatusUpdate, isAgentActivity, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateSketchAttachments, validateRoutes, } from "./internal/agent-protocol.js";
8
+ export { AGENT_PROTOCOL_LIMITS, AGENT_PROTOCOL_VERSION, CANVAS_COMMAND_TYPES, canonicalOrigin, isAgentPromptRequest, isAgentSketchAttachment, isAgentSketchCaptureMetadata, isAgentSketchMetadata, isAgentStatusUpdate, isAgentActivity, isAllowedOrigin, isCanvasCommand, isCanvasGroup, isCanvasState, isCanonicalOrigin, isProjectIdentity, isSameOriginRoute, protocolError, validateSketchAttachments, validateRoutes, } from "./internal/agent-protocol.js";
9
9
  /** Stable endpoint names used by the loopback browser bridge. */
10
10
  export const BRIDGE_ENDPOINTS = {
11
11
  health: "/health",
@@ -25,6 +25,7 @@ export const AGENT_CONTROL_ENDPOINTS = {
25
25
  release: "/__nudge/agent/release",
26
26
  listen: "/__nudge/agent/listen",
27
27
  status: "/__nudge/agent/status",
28
+ request: "/__nudge/agent/request",
28
29
  reportStatus: "/__nudge/agent/report-status",
29
30
  activity: "/__nudge/agent/activity",
30
31
  canvas: "/__nudge/agent/canvas",
package/dist/server.d.ts CHANGED
@@ -1,43 +1,19 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
- import type { AgentProjectIdentity } from "./internal/agent-protocol.js";
4
- import { type BrowserBridge, type BrowserBridgeOptions, type BridgeAddress } from "./bridge.js";
5
3
  import { AdapterRouting } from "./adapterRouting.js";
6
- import { DiscoveredProjectRouter } from "./discovery.js";
7
4
  export declare const MCP_SERVER_NAME = "nudge-ui";
8
5
  export declare const MCP_SERVER_VERSION: string;
9
6
  /** Instructions are sent through MCP initialization for every host. */
10
7
  export declare const MCP_SERVER_INSTRUCTIONS: string;
11
- export interface AgentCompanionOptions extends BrowserBridgeOptions {
12
- readonly workspaceRoot?: string;
13
- readonly serverName?: string;
14
- readonly serverVersion?: string;
15
- }
16
- export interface AgentCompanionStartResult {
17
- readonly address: BridgeAddress;
18
- }
19
- export interface AgentCompanion {
20
- readonly bridge: BrowserBridge;
21
- readonly mcpServer: McpServer;
22
- readonly project: AgentProjectIdentity;
23
- start(transport?: Transport): Promise<AgentCompanionStartResult>;
24
- close(): Promise<void>;
25
- }
26
- /**
27
- * Creates one project-scoped MCP companion and its loopback browser bridge.
28
- * The returned companion is inert until `start` is called.
29
- */
30
- export declare function createAgentCompanion(options: AgentCompanionOptions): AgentCompanion;
31
8
  export interface DiscoveredAgentAdapter {
32
9
  readonly mcpServer: McpServer;
33
- readonly router: DiscoveredProjectRouter | AdapterRouting;
10
+ readonly router: AdapterRouting;
34
11
  start(transport?: Transport): Promise<void>;
35
12
  close(): Promise<void>;
36
13
  }
37
14
  /** Creates a stdio MCP adapter that discovers project-owned bridges. */
38
- export declare function createDiscoveredAgentAdapter(options: {
39
- readonly workspaceRoot?: string;
15
+ export declare function createDiscoveredAgentAdapter(options?: {
40
16
  readonly registryRoot?: string;
41
17
  readonly serverName?: string;
42
18
  readonly serverVersion?: string;
43
- }): Promise<DiscoveredAgentAdapter>;
19
+ }): DiscoveredAgentAdapter;