devframe 1.3.2 → 1.3.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.
Files changed (68) hide show
  1. package/dist/{_shared-CnKu5myY.d.mts → _shared-BxeuukaL.d.mts} +2 -2
  2. package/dist/{_shared-BGb9aDxr.mjs → _shared-CtXCQFTV.mjs} +1 -1
  3. package/dist/adapters/build.d.mts +1 -1
  4. package/dist/adapters/cac.d.mts +1 -1
  5. package/dist/adapters/cac.mjs +2 -2
  6. package/dist/adapters/dev.d.mts +4 -4
  7. package/dist/adapters/dev.mjs +2 -2
  8. package/dist/adapters/embedded.d.mts +1 -1
  9. package/dist/adapters/initiate.d.mts +4 -4
  10. package/dist/adapters/initiate.mjs +1 -1
  11. package/dist/adapters/mcp.d.mts +2 -2
  12. package/dist/{agentic-CCstJ5yv.d.mts → agentic-DVgUzwhV.d.mts} +1 -1
  13. package/dist/client/index.d.mts +5 -5
  14. package/dist/client/index.mjs +1 -1
  15. package/dist/{context-CsdPs3po.d.mts → context-B-AARy_U.d.mts} +1 -1
  16. package/dist/{dev-DaHYh5YK.mjs → dev-DF1pEq0n.mjs} +2 -2
  17. package/dist/{dist-DJydMELP.mjs → dist-CZ-TySHD.mjs} +1 -2
  18. package/dist/in-page-channel/index.d.mts +3 -2
  19. package/dist/in-page-channel/index.mjs +1 -1
  20. package/dist/{context-WxTq6kqb.d.mts → index-BhUqJUHG.d.mts} +1404 -1404
  21. package/dist/index.d.mts +5 -4
  22. package/dist/{instance-registry-CmZfF5u3.mjs → instance-registry-DwmnWyCK.mjs} +1 -1
  23. package/dist/{instance-shell-Dk2SejFT.mjs → instance-shell-C0yMifrP.mjs} +5 -5
  24. package/dist/{instance-shell-Bujp0BhE.d.mts → instance-shell-DTk514M9.d.mts} +4 -4
  25. package/dist/{interactive-auth-BURwIAvE.mjs → interactive-auth-CZ3CPfEs.mjs} +1 -1
  26. package/dist/internal/index.d.mts +7 -7
  27. package/dist/internal/index.mjs +4 -4
  28. package/dist/{mcp-content-CQQBt2An.d.mts → mcp-content-0a0mpCL5.d.mts} +1 -1
  29. package/dist/node/auth.d.mts +84 -3
  30. package/dist/node/auth.mjs +1 -1
  31. package/dist/node/hub-internals.d.mts +11 -2
  32. package/dist/node/hub-internals.mjs +1 -1
  33. package/dist/node/index.d.mts +4 -2
  34. package/dist/recipes/interactive-auth.d.mts +2 -2
  35. package/dist/recipes/interactive-auth.mjs +1 -1
  36. package/dist/{rolldown-runtime-DYf_mQCB.mjs → rolldown-runtime-BVN1G_yT.mjs} +4 -4
  37. package/dist/rpc/dump.d.mts +1 -1
  38. package/dist/rpc/index.d.mts +121 -3
  39. package/dist/rpc/transports/sse-client.d.mts +34 -2
  40. package/dist/rpc/transports/sse-server.d.mts +2 -2
  41. package/dist/rpc/transports/ws-bun.d.mts +2 -2
  42. package/dist/rpc/transports/ws-client.d.mts +27 -2
  43. package/dist/rpc/transports/ws-deno.d.mts +2 -2
  44. package/dist/rpc/transports/ws-server.d.mts +150 -3
  45. package/dist/{rpc-core-CDUizUQB.mjs → rpc-core-BmDHiDR6.mjs} +1 -1
  46. package/dist/{rpc-core-CFwyV10r.d.mts → rpc-core-CblgPYJ_.d.mts} +3 -3
  47. package/dist/{shared-state-COV7NCbd.mjs → shared-state-DiAsLHP8.mjs} +3 -2
  48. package/dist/{state-B_yjfeqo.mjs → state-TdDWqiG-.mjs} +2 -1
  49. package/dist/types/index.d.mts +3 -3
  50. package/dist/{types-844OBepb.d.mts → types-KZOyhez5.d.mts} +1 -0
  51. package/dist/utils/debounce.d.mts +6 -0
  52. package/dist/utils/events.d.mts +1 -1
  53. package/dist/utils/get-port.mjs +1 -1
  54. package/dist/utils/launch-editor.mjs +1 -1
  55. package/dist/utils/nostics.d.mts +40 -2
  56. package/dist/utils/origin.d.mts +55 -2
  57. package/dist/utils/serve-static.d.mts +1 -1
  58. package/dist/utils/shared-state.d.mts +1 -1
  59. package/dist/utils/shared-state.mjs +1 -1
  60. package/dist/utils/streaming-channel.d.mts +1 -1
  61. package/package.json +6 -6
  62. package/dist/index-D0qrBkhO.d.mts +0 -121
  63. package/dist/index-HMJPlexc.d.mts +0 -84
  64. package/dist/nostics-Dgwh7ytT.d.mts +0 -40
  65. package/dist/origin-D5i7K_Zb.d.mts +0 -56
  66. package/dist/sse-client-B2PrSz-H.d.mts +0 -35
  67. package/dist/ws-client-lqOeANcn.d.mts +0 -28
  68. package/dist/ws-server-CYHkeyA-.d.mts +0 -150
@@ -1,10 +1,10 @@
1
- import { i as BirpcReturn } from "./index-BKu9BxcT.mjs";
2
- import { d as RpcFunctionsCollectorBase } from "./index-D0qrBkhO.mjs";
3
1
  import { t as StandardSchemaV1 } from "./index-BIRF0E5l.mjs";
4
- import { g as RpcFunctionDefinitionAny, h as RpcFunctionDefinition } from "./types-844OBepb.mjs";
2
+ import { g as RpcFunctionDefinitionAny, h as RpcFunctionDefinition } from "./types-KZOyhez5.mjs";
3
+ import { i as BirpcReturn } from "./index-BKu9BxcT.mjs";
4
+ import { RpcFunctionsCollectorBase } from "./rpc/index.mjs";
5
5
  import { n as DevframeRpcConnection, t as DevframeNodeRpcSessionMeta } from "./session-Y9R6tqg4.mjs";
6
+ import { defineDiagnostics } from "./utils/nostics.mjs";
6
7
  import { a as RemoteAssetsStore, o as StaticAssetsSource } from "./remote-assets-5AQMHZnV.mjs";
7
- import { h as defineDiagnostics } from "./nostics-Dgwh7ytT.mjs";
8
8
  import { CAC } from "cac";
9
9
  interface EventsMap {
10
10
  [event: string]: any;
@@ -277,313 +277,196 @@ interface DevframeAgentHost {
277
277
  /** Look up a resource by id. */
278
278
  getResource: (id: string) => AgentResource | undefined;
279
279
  }
280
+ type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
281
+ type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
282
+ type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
283
+ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
284
+ type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
285
+ type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]>; };
280
286
  /**
281
- * Schema map for typed CLI flags. Keys are flag names in camelCase, so
282
- * this matches CAC's parsed-flag output ( `--no-open` → `noOpen` ). Each
283
- * value is any [Standard Schema](https://standardschema.dev/) validator
284
- * (valibot, zod, arktype, devframe's built-in `s`, …) used to both (a)
285
- * derive the CAC option type when the flag is registered and (b) validate
286
- * the parsed value before it's forwarded to `setup(ctx, { flags })`.
287
- */
288
- type CliFlagsSchema = Record<string, StandardSchemaV1>;
289
- /**
290
- * Identity helper that preserves the literal schema-map type; use this
291
- * so `InferCliFlags<typeof myFlags>` resolves to the right object shape.
292
- *
293
- * ```ts
294
- * const appFlags = defineCliFlags({
295
- * depth: v.pipe(v.number(), v.integer()),
296
- * config: v.optional(v.string()),
297
- * })
298
- *
299
- * defineDevframe({
300
- * cli: { flags: appFlags },
301
- * setup(ctx, info) {
302
- * const flags = info.flags as InferCliFlags<typeof appFlags>
303
- * flags.depth // number
304
- * flags.config // string | undefined
305
- * },
306
- * })
307
- * ```
287
+ * Serializable patch describing a single mutation to a `SharedState`.
288
+ * Structurally compatible with JSON-Patch and is safe to send over RPC.
308
289
  */
309
- declare function defineCliFlags<T extends CliFlagsSchema>(flags: T): T;
310
- /** Extract the parsed-output type from a {@link CliFlagsSchema}. */
311
- type InferCliFlags<T extends CliFlagsSchema> = { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]>; };
312
- /** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
313
- declare function parseCliFlags(schema: CliFlagsSchema, raw: Record<string, unknown>): {
314
- flags: Record<string, unknown>;
315
- issues?: string[];
316
- };
290
+ interface SharedStatePatch {
291
+ op: 'add' | 'remove' | 'replace';
292
+ path: readonly (string | number)[];
293
+ value?: unknown;
294
+ }
317
295
  /**
318
- * A ready-made pre-auth RPC handler, as produced by
319
- * `devframe/recipes/interactive-auth`'s `createInteractiveAuth`. Bundles
320
- * everything a host adapter needs to wire an authenticated server:
321
- * the handshake RPC functions, the resolver gate, the connect-time trust
322
- * hook, and the startup banner.
323
- *
324
- * `initDevframe` / `initHub` accept one of these directly via their `auth` option
325
- * (see {@link https://devfra.me | devframe}'s server docs), or a host can
326
- * wire the four pieces itself against a lower-level transport.
296
+ * State host that is immutable by default with explicit mutate.
327
297
  */
328
- interface DevframeAuthHandler {
298
+ interface SharedState<T> {
329
299
  /**
330
- * `anonymous:devframe:auth` + `anonymous:devframe:auth:exchange` (the
331
- * handshake), `anonymous:devframe:auth:request-code` (client-requested
332
- * banner print), and `devframe:auth:revoke` (self-revoke); register these
333
- * on the RPC host (e.g. `rpcHost.register(fn)` for each).
300
+ * Get the current state. Immutable.
334
301
  */
335
- rpcFunctions: RpcFunctionDefinitionAny[];
302
+ value: () => Immutable<T>;
336
303
  /**
337
- * Resolver gate: whether `methodName` is callable given `session`'s
338
- * current trust state. Defaults to allowing any `anonymous:`-prefixed
339
- * method (see `isAnonymousRpcMethod`) plus anything once the session is
340
- * trusted.
304
+ * Subscribe to state changes.
341
305
  */
342
- authorize: (methodName: string, session: DevframeNodeRpcSession) => boolean;
306
+ on: EventEmitter<SharedStateEvents<T>>['on'];
343
307
  /**
344
- * Connect-time trust: reads a bearer token off the connection's initial
345
- * request (a static/pre-shared token from `clientAuthTokens`, or a token
346
- * minted by the code exchange) and, when valid, marks the session
347
- * trusted immediately, before the client's own handshake call.
308
+ * Mutate the state.
348
309
  */
349
- onConnect: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void;
310
+ mutate: (fn: (state: T) => void, syncId?: string) => void;
350
311
  /**
351
- * Print the current one-time code and its magic-link URL. An untrusted
352
- * browser client triggers this itself over
353
- * `anonymous:devframe:auth:request-code`; call it directly when the host
354
- * wants the code on screen without waiting for a client. Safe to call
355
- * repeatedly; it only prints once per code.
312
+ * Apply patches to the state.
356
313
  */
357
- printBanner: () => void;
314
+ patch: (patches: SharedStatePatch[], syncId?: string) => void;
358
315
  /**
359
- * Rewrite a URL the CLI is about to open in the browser (`--open`) so the
360
- * tab lands already authenticated, e.g. appending the current OTP as a
361
- * query param. Called with the fully-resolved target URL; return it
362
- * unchanged to opt out. Optional: a handler that doesn't need this (e.g.
363
- * one gated by a pre-shared bearer token instead of an interactive code)
364
- * can omit it and the URL opens as-is.
316
+ * Sync IDs that have been applied to the state.
365
317
  */
366
- buildOpenUrl?: (url: string) => string;
318
+ syncIds: Set<string>;
319
+ }
320
+ interface SharedStateEvents<T> {
321
+ updated: (fullState: T, patches: SharedStatePatch[] | undefined, syncId: string) => void;
322
+ }
323
+ interface SharedStateOptions<T> {
324
+ /**
325
+ * Initial state.
326
+ */
327
+ initialValue: T;
328
+ /**
329
+ * Enable patches.
330
+ *
331
+ * @default false
332
+ */
333
+ enablePatches?: boolean;
367
334
  }
335
+ declare function createSharedState<T extends object>(options: SharedStateOptions<T>): SharedState<T>;
368
336
  /**
369
- * The MCP adapter surface, typed here so `devframe` (which probes for and
370
- * lazily loads `@devframes/agentic`) and `@devframes/agentic/mcp` (which
371
- * implements it) share one contract without a type-level dependency cycle.
372
- * None of these shapes reference the MCP SDK: the SDK is an implementation
373
- * detail of `@devframes/agentic`, freely swappable behind this surface.
374
- * The h3-bound `mountMcpHttp` shapes live in `node/agentic.ts` (exported via
375
- * `devframe/internal`) instead: `devframe/types` must stay lib-neutral, and
376
- * h3's declarations are not.
337
+ * Serialized error shape sent over the wire when a stream ends with a failure.
338
+ * Stays JSON-safe so the strict-JSON encoder can carry it without coercion.
377
339
  */
378
- interface CreateMcpServerOptions {
340
+ interface StreamErrorPayload {
341
+ name: string;
342
+ message: string;
343
+ }
344
+ /**
345
+ * Single buffered chunk in the server-side ring buffer.
346
+ *
347
+ * Sequence numbers start at 1 and increment per write. Subscribers track
348
+ * `lastSeenSeq` and ask for `afterSeq` on resubscribe so the server can
349
+ * replay any chunks the client missed during a brief disconnect.
350
+ */
351
+ interface BufferedChunk<T> {
352
+ seq: number;
353
+ chunk: T;
354
+ }
355
+ interface StreamSinkEvents<T> {
356
+ /** Fired for each `write()`. The RPC layer subscribes and broadcasts. */
357
+ chunk: (seq: number, chunk: T) => void;
358
+ /** Terminal; fired exactly once per sink lifetime. */
359
+ end: (error?: StreamErrorPayload) => void;
360
+ }
361
+ interface CreateStreamSinkOptions {
362
+ id?: string;
379
363
  /**
380
- * Transport to use. `createMcpServer` itself runs `'stdio'` (a standalone
381
- * process with its own host context); the Streamable-HTTP transport is
382
- * served route-based by the dev server instead; see `mountMcpHttp` and
383
- * the `mcp` option on `createDevServer` / `createCac`'s `--mcp` flag.
364
+ * Size of the per-stream ring buffer kept for replay-on-resubscribe.
365
+ * `0` (default) disables replay.
384
366
  */
385
- transport?: 'stdio';
367
+ replayWindow?: number;
368
+ }
369
+ /**
370
+ * Server-side producer handle. Two equivalent surfaces share one piece of
371
+ * state: the imperative `write/error/close` triple, and a `WritableStream<T>`
372
+ * for `pipeTo`-style consumption.
373
+ */
374
+ interface StreamSink<T> {
375
+ /** Stable id used by clients to subscribe. */
376
+ readonly id: string;
386
377
  /**
387
- * Expose shared-state keys as MCP resources.
388
- * - `true` (default): every key the host publishes
389
- * - `false`: none
390
- * - `(key) => boolean`: filter
378
+ * Aborts when the consumer cancels (server-side) or when the transport
379
+ * loses every subscriber. Producers should poll `signal.aborted` and exit
380
+ * cleanly.
391
381
  */
392
- exposeSharedState?: boolean | ((key: string) => boolean);
382
+ readonly signal: AbortSignal;
383
+ /** `true` after `close()` / `error()`. Further writes throw. */
384
+ readonly closed: boolean;
385
+ /** Last allocated sequence number. `0` until the first write. */
386
+ readonly lastSeq: number;
387
+ write: (chunk: T) => void;
388
+ error: (reason: unknown) => void;
389
+ close: () => void;
390
+ /** External-cancel path. Aborts the signal so handlers can short-circuit. */
391
+ abort: (reason?: unknown) => void;
392
+ /** `WritableStream<T>` adapter; same in-memory state as the imperative API. */
393
+ readonly writable: WritableStream<T>;
393
394
  /**
394
- * Usage guidance sent as `instructions` in the MCP `initialize` (and
395
- * `server/discover`) result. Omitted or empty sends no instructions.
395
+ * Internal: RPC layer subscribes to receive chunk/end notifications.
396
+ * Not part of the public contract; do not call directly.
397
+ *
398
+ * @internal
396
399
  */
397
- instructions?: string;
400
+ readonly events: EventEmitter<StreamSinkEvents<T>>;
398
401
  /**
399
- * Resource templates served through `resources/templates/list`, and read
400
- * through `resources/read`. Omitted or empty keeps the resources unchanged.
402
+ * Internal replay buffer. RPC layer reads on (re)subscribe to feed
403
+ * missed chunks before going live.
404
+ *
405
+ * @internal
401
406
  */
402
- resourceTemplates?: McpResourceTemplate[];
407
+ readonly buffer: ReadonlyArray<BufferedChunk<T>>;
408
+ }
409
+ interface CreateStreamReaderOptions {
410
+ id?: string;
403
411
  /**
404
- * Host subscription for resource-list changes; each `notify()` sends
405
- * `notifications/resources/list_changed`. See {@link McpResourcesChangedHook}.
412
+ * Maximum number of buffered chunks held client-side while the consumer
413
+ * isn't draining. On overflow, the oldest chunk is dropped.
406
414
  */
407
- onResourcesChanged?: McpResourcesChangedHook;
415
+ highWaterMark?: number;
408
416
  /**
409
- * Prompt templates served through `prompts/list` and `prompts/get`.
410
- * Omitted or empty declares no `prompts` capability.
417
+ * Called when the chunk queue overflows the high-water mark. The RPC
418
+ * layer wires this to a coded warning; the primitive itself is
419
+ * RPC-agnostic.
411
420
  */
412
- prompts?: McpPrompt[];
413
- /** Override the name reported in the MCP handshake. */
414
- serverName?: string;
415
- /** Override the version reported in the MCP handshake. Defaults to `definition.version ?? '0.0.0'`. */
416
- serverVersion?: string;
417
- /** Called once the transport is connected. */
418
- onReady?: (info: {
419
- transport: 'stdio';
420
- }) => void;
421
+ onOverflow?: (dropped: number) => void;
422
+ /** Called when the consumer cancels; the RPC layer sends `:cancel` upstream. */
423
+ onCancel?: () => void;
421
424
  }
422
425
  /**
423
- * One MCP content block in a tool result. Attach blocks to a tool's return
424
- * value with `withMcpContent` (from `devframe`) to send them in place of the
425
- * default JSON text block. `data` is base64.
426
+ * Client-side consumer handle. Both an `AsyncIterable<T>` (for `for await`)
427
+ * and exposes `readable: ReadableStream<T>` (for `pipeTo`). Pick one; they
428
+ * share a single internal queue, so concurrent draining will race.
426
429
  */
427
- type McpContentBlock = {
428
- type: 'text';
429
- text: string;
430
- } | {
431
- type: 'image';
432
- data: string;
433
- mimeType: string;
434
- } | {
435
- type: 'audio';
436
- data: string;
437
- mimeType: string;
438
- };
439
- interface McpServerHandle {
440
- stop: () => Promise<void>;
441
- }
442
- interface CreateMcpFetchHandlerOptions {
443
- /** Name reported in the MCP handshake. */
444
- serverName: string;
445
- /** Version reported in the MCP handshake. */
446
- serverVersion: string;
447
- /** Expose shared-state keys as MCP resources; see {@link CreateMcpServerOptions.exposeSharedState}. */
448
- exposeSharedState: boolean | ((key: string) => boolean);
449
- /** Usage guidance for the handshake; see {@link CreateMcpServerOptions.instructions}. */
450
- instructions?: string;
451
- /** Resource templates; see {@link CreateMcpServerOptions.resourceTemplates}. */
452
- resourceTemplates?: McpResourceTemplate[];
453
- /** Host subscription for resource-list changes; see {@link CreateMcpServerOptions.onResourcesChanged}. */
454
- onResourcesChanged?: McpResourcesChangedHook;
455
- /** Prompt templates; see {@link CreateMcpServerOptions.prompts}. */
456
- prompts?: McpPrompt[];
457
- /**
458
- * Optional identity check, layered on top of the origin gate and checked
459
- * **after** it: a bearer token string (matched in constant time against
460
- * `Authorization: Bearer <token>`), a `(request) => boolean` callback, or
461
- * `false` (the default) for origin-only, trusting same-machine callers. A
462
- * callback governs identity only and cannot relax the origin gate. See
463
- * {@link McpAuthorization}.
464
- */
465
- authorization?: McpAuthorization;
466
- /**
467
- * Origin allow-list beyond the loopback default. `false` disables the
468
- * origin gate entirely. Default: loopback-only.
469
- *
470
- * Unlike the WS transport, the MCP route does **not** allow `Origin`-less
471
- * requests: a route-based endpoint is reachable by any local process, so a
472
- * request must carry an `Origin` that passes the gate. Native clients
473
- * (e.g. `devframe connect`) send their loopback origin explicitly.
474
- */
475
- allowedOrigins?: readonly string[] | false;
476
- }
477
- /** Connection facts a host knows about a request beyond the `Request` itself. */
478
- interface McpConnectionInfo {
479
- /**
480
- * The connecting peer's remote address (a node socket's `remoteAddress`),
481
- * used to prove a same-machine caller when the endpoint relies on the
482
- * loopback origin default with no identity check. A host that can resolve a
483
- * trustworthy peer address (the h3/node mount) supplies it; when it's
484
- * omitted the origin gate stays the only locality signal.
485
- */
486
- remoteAddress?: string;
487
- }
488
- interface McpFetchHandler {
489
- /**
490
- * WHATWG-`fetch` handler for the MCP endpoint. Hand every method
491
- * (POST/GET/DELETE) on the endpoint's path to it; routing by path is the
492
- * host's job. Pass {@link McpConnectionInfo} when the host can resolve the
493
- * peer address so the default trust boundary can enforce same-machine
494
- * locality.
495
- */
496
- fetch: (request: Request, connection?: McpConnectionInfo) => Promise<Response>;
497
- /** Tear down the handler (aborts in-flight exchanges, drops the change bridge). */
498
- dispose: () => Promise<void>;
499
- }
500
- /** One concrete resource that an {@link McpResourceTemplate} lists in `resources/list`. */
501
- interface McpResourceTemplateEntry {
502
- /** The resource URI. It SHOULD match the `uriTemplate` of its template, so `resources/read` finds it. */
503
- uri: string;
504
- name: string;
505
- /** Human-readable name for display. */
506
- title?: string;
507
- description?: string;
508
- /** Defaults to the `mimeType` of the template. */
509
- mimeType?: string;
430
+ interface StreamReader<T> extends AsyncIterable<T> {
431
+ readonly id: string;
432
+ readonly cancelled: boolean;
433
+ readonly done: boolean;
434
+ /** Highest `seq` observed. Used for replay on reconnect. */
435
+ readonly lastSeenSeq: number;
436
+ /** `ReadableStream<T>` adapter for `pipeTo`-style consumption. */
437
+ readonly readable: ReadableStream<T>;
438
+ cancel: () => void;
439
+ /** @internal */
440
+ _push: (seq: number, chunk: T) => void;
441
+ /** @internal */
442
+ _end: (error?: StreamErrorPayload) => void;
510
443
  }
511
444
  /**
512
- * What an {@link McpResourceTemplate} returns from `read`, sent as the
513
- * `resources/read` result: `text` as is, or else `json` as JSON text.
445
+ * Build a server-side stream sink. RPC-agnostic; the RPC host wires
446
+ * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
447
+ * for late or reconnecting subscribers.
514
448
  */
515
- interface McpResourceTemplateContent {
516
- text?: string;
517
- json?: unknown;
518
- /** Defaults to the `mimeType` of the template, then `application/json`. */
519
- mimeType?: string;
520
- }
449
+ declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamSink<T>;
521
450
  /**
522
- * A resource template the MCP server lists in `resources/templates/list` and
523
- * reads in `resources/read`. The `uriTemplate` uses RFC 6570 level 1 only:
524
- * plain `{name}` expressions, with text between any two of them. Each
525
- * expression matches a non-empty value with no `/`, and its value is
526
- * percent-decoded.
527
- *
528
- * `list` and `read` get the node context of the devframe (or of the hub), so
529
- * they can read live state such as `ctx.rpc.sharedState` or `ctx.agent`. The
530
- * route options are resolved before `setup()` runs, so a template cannot
531
- * capture that context in a closure.
451
+ * Build a client-side stream reader. RPC-agnostic; the RPC host calls
452
+ * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
453
+ * terminal frame. Consumers iterate with `for await` or pipe `readable`.
532
454
  */
533
- interface McpResourceTemplate {
534
- /** RFC 6570 level 1 URI template, e.g. `my-tool://file/{path}`. Anything else throws `DF0084`. */
535
- uriTemplate: string;
536
- name: string;
537
- /** Human-readable name for display. */
455
+ declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
456
+ interface BrowserAgentToolManifest {
457
+ id: string;
538
458
  title?: string;
539
- description?: string;
540
- /** MIME type of every resource the template stands for. */
541
- mimeType?: string;
542
- /** The concrete resources the template stands for right now. They are added to `resources/list`. */
543
- list?: (ctx: DevframeNodeContext) => McpResourceTemplateEntry[] | Promise<McpResourceTemplateEntry[]>;
544
- /** Read one resource: `params` holds the decoded value of each `{name}`, `uri` the requested URI. */
545
- read: (params: Record<string, string>, uri: string, ctx: DevframeNodeContext) => McpResourceTemplateContent | Promise<McpResourceTemplateContent>;
546
- }
547
- /**
548
- * A host subscription for resource-list changes. Call `notify()` each time
549
- * the resources change; the MCP server then sends
550
- * `notifications/resources/list_changed`. Return a function that removes the
551
- * subscription; the MCP server calls it on teardown.
552
- */
553
- type McpResourcesChangedHook = (notify: () => void, ctx: DevframeNodeContext) => () => void;
554
- /** One argument of an {@link McpPrompt}. */
555
- interface McpPromptArgument {
556
- name: string;
557
- description?: string;
558
- /** `prompts/get` answers invalid params when a required argument is missing or empty. */
559
- required?: boolean;
560
- }
561
- /** One message of the answer to `prompts/get`. */
562
- interface McpPromptMessage {
563
- role: 'user' | 'assistant';
564
- content: {
565
- type: 'text';
566
- text: string;
567
- };
568
- }
569
- /** What an {@link McpPrompt} returns from `get`, sent as the `prompts/get` result. */
570
- interface McpPromptResult {
571
- description?: string;
572
- messages: McpPromptMessage[];
459
+ description: string;
460
+ safety: 'read' | 'action' | 'destructive';
461
+ tags?: readonly string[];
462
+ inputSchema?: unknown;
573
463
  }
574
- /**
575
- * A prompt template the MCP server lists in `prompts/list` and builds in
576
- * `prompts/get`. A coding agent usually offers it to the user as a slash
577
- * command. Each `name` is used once; a repeated name throws `DF0085`.
578
- */
579
- interface McpPrompt {
580
- name: string;
581
- /** Human-readable name for display. */
582
- title?: string;
583
- description?: string;
584
- arguments?: McpPromptArgument[];
585
- /** Build the messages from the arguments the coding agent sent. */
586
- get: (args: Record<string, string>) => McpPromptResult | Promise<McpPromptResult>;
464
+ /** What a connected document reports about itself alongside its tool manifest. */
465
+ interface BrowserAgentClientInfo {
466
+ url: string;
467
+ title: string;
468
+ visible: boolean;
469
+ focused: boolean;
587
470
  }
588
471
  /**
589
472
  * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
@@ -708,266 +591,44 @@ interface DevframeHost {
708
591
  * private, `global` is per-user.
709
592
  */
710
593
  type DevframeStorageScope = 'workspace' | 'project' | 'global';
711
- interface BrowserAgentToolManifest {
712
- id: string;
713
- title?: string;
714
- description: string;
715
- safety: 'read' | 'action' | 'destructive';
716
- tags?: readonly string[];
717
- inputSchema?: unknown;
718
- }
719
- /** What a connected document reports about itself alongside its tool manifest. */
720
- interface BrowserAgentClientInfo {
721
- url: string;
722
- title: string;
723
- visible: boolean;
724
- focused: boolean;
725
- }
726
- /**
727
- * To be extended
728
- */
729
- interface DevframeRpcClientFunctions {
730
- /** Clear cached RPC results and refresh cache eligibility. */
731
- 'devframe:rpc:cache:invalidate': () => Promise<void>;
732
- /** Invoke a tool registered in this browser document. @internal */
733
- 'devframe:agent:invoke-client-tool': (id: string, args: Record<string, unknown>) => Promise<unknown>;
734
- /**
735
- * Server→client notification that this connection's auth token has been
736
- * revoked. The client drops to untrusted on receipt. Broadcast by
737
- * `revokeActiveConnectionsForToken`.
738
- *
739
- * @internal
740
- */
741
- 'devframe:auth:revoked': () => Promise<void>;
742
- /**
743
- * Streaming chunk pushed from server to subscribed clients. Wired by
744
- * `RpcStreamingHost`; do not register manually.
745
- *
746
- * @internal
747
- */
748
- 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
749
- /**
750
- * Streaming terminator pushed from server to subscribed clients. Wired by
751
- * `RpcStreamingHost`; do not register manually.
752
- *
753
- * @internal
754
- */
755
- 'devframe:streaming:end': (channel: string, id: string, error?: {
756
- name: string;
757
- message: string;
758
- }) => Promise<void>;
594
+ interface DevframeViewHost {
759
595
  /**
760
- * Server→client cancel for an in-flight upload. Wired by
761
- * `RpcStreamingHost`; do not register manually.
596
+ * Static mounts registered through {@link DevframeViewHost.hostStatic}, each
597
+ * carrying the `resolveFrom` base it was mounted with so a build step that
598
+ * copies these itself (rather than serving them live) re-resolves a remote
599
+ * source to the same locally-installed copy it would serve live.
762
600
  *
763
601
  * @internal
764
602
  */
765
- 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
603
+ buildStaticDirs: {
604
+ baseUrl: string;
605
+ source: StaticAssetsSource;
606
+ resolveFrom?: string | null;
607
+ }[];
766
608
  /**
767
- * Full shared-state snapshot pushed from server to subscribed clients.
768
- * Wired by `RpcSharedStateHost`; do not register manually.
609
+ * Helper to host static files
610
+ * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
611
+ * - In `build` mode, it will copy the static files to the dist directory
769
612
  *
770
- * @internal
771
- */
772
- 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
773
- /**
774
- * Incremental shared-state patch pushed from server to subscribed clients.
775
- * Wired by `RpcSharedStateHost`; do not register manually.
613
+ * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
614
+ * declaration served through devframe's caching CDN back-proxy.
776
615
  *
777
- * @internal
616
+ * `defaultResolveFrom` overrides, for this call only, the context's own
617
+ * `importMetaUrl` as the default `resolveFrom` for a remote source that
618
+ * doesn't set one. A shared host that mounts assets on behalf of another
619
+ * devframe (a hub installing a plugin) passes that plugin's `importMetaUrl`
620
+ * so the assets resolve against the plugin's dependency graph rather than
621
+ * the host's.
778
622
  */
779
- 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
623
+ hostStatic: (baseUrl: string, source: StaticAssetsSource, defaultResolveFrom?: string | null) => void;
780
624
  }
625
+ type AnyRpcFn = (...args: any[]) => any;
781
626
  /**
782
- * To be extended
783
- */
784
- interface DevframeRpcServerFunctions {
785
- /** Methods eligible for automatic client caching. @internal */
786
- 'devframe:rpc:cacheable-functions': () => Promise<string[]>;
787
- /** Replace this connection's browser-agent tool manifest, tagged with the calling tab's stable client id. @internal */
788
- 'devframe:agent:sync-client-tools': (clientId: string, tools: BrowserAgentToolManifest[], info: BrowserAgentClientInfo) => Promise<void>;
789
- /**
790
- * Authenticate a connection with a previously-issued bearer token; resolves
791
- * whether the connection is now trusted. The interactive handler is provided
792
- * by the host adapter (e.g. Vite DevTools); the standalone server registers
793
- * an auto-trust noop when `auth: false`.
794
- *
795
- * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
796
- * reachable before the connection is trusted; this is the *only* rule the
797
- * pre-trust gate applies, no per-method allowlist.
798
- *
799
- * @internal
800
- */
801
- 'anonymous:devframe:auth': (params: {
802
- authToken: string;
803
- ua: string;
804
- origin: string;
805
- }) => Promise<{
806
- isTrusted: boolean;
807
- }>;
808
- /**
809
- * Exchange a one-time authentication code (shown by the dev server) for a fresh,
810
- * node-issued bearer token, returning the token on success or `null`. The
811
- * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
812
- *
813
- * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
814
- * reachable before the connection is trusted.
815
- *
816
- * @internal
817
- */
818
- 'anonymous:devframe:auth:exchange': (params: {
819
- code: string;
820
- ua: string;
821
- origin: string;
822
- }) => Promise<{
823
- authToken: string | null;
824
- }>;
825
- /**
826
- * Ask the server to print its auth banner (code + magic link) for this
827
- * client, e.g. when an auth UI first shows; `reissue: true` rotates the code
828
- * first (the manual "re-issue" action). Registered by
829
- * `recipes/interactive-auth`; a repeat request for an already-printed code
830
- * is a no-op.
831
- *
832
- * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
833
- * reachable before the connection is trusted.
834
- *
835
- * @internal
836
- */
837
- 'anonymous:devframe:auth:request-code': (params: {
838
- ua: string;
839
- origin: string;
840
- reissue?: boolean;
841
- }) => Promise<void>;
842
- /**
843
- * Self-revoke: the caller asks the server to revoke its own bearer token
844
- * (if any) and drop to untrusted. Requires an already-trusted caller, so
845
- * unlike the two handshake methods above it does **not** carry the
846
- * `anonymous:` prefix. Registered by `recipes/interactive-auth`.
847
- *
848
- * @internal
849
- */
850
- 'devframe:auth:revoke': () => Promise<void>;
851
- /**
852
- * Subscribe a client to a shared-state key. Wired by
853
- * `RpcSharedStateHost`; do not register manually.
854
- *
855
- * @internal
856
- */
857
- 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
858
- /**
859
- * Read the current value for a shared-state key. Wired by
860
- * `RpcSharedStateHost`; do not register manually.
861
- *
862
- * @internal
863
- */
864
- 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
865
- /**
866
- * Replace a shared-state value (from the client). Wired by
867
- * `RpcSharedStateHost`; do not register manually.
868
- *
869
- * @internal
870
- */
871
- 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
872
- /**
873
- * Apply a patch to a shared-state value (from the client). Wired by
874
- * `RpcSharedStateHost`; do not register manually.
875
- *
876
- * @internal
877
- */
878
- 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
879
- /**
880
- * Client→server streaming subscription with optional replay cursor.
881
- * Wired by `RpcStreamingHost`; do not register manually.
882
- *
883
- * @internal
884
- */
885
- 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
886
- afterSeq?: number;
887
- }) => Promise<void>;
888
- /**
889
- * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
890
- * do not register manually.
891
- *
892
- * @internal
893
- */
894
- 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
895
- /**
896
- * Client→server streaming cancellation request. Wired by
897
- * `RpcStreamingHost`; do not register manually.
898
- *
899
- * @internal
900
- */
901
- 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
902
- /**
903
- * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
904
- * register manually.
905
- *
906
- * @internal
907
- */
908
- 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
909
- /**
910
- * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
911
- * register manually.
912
- *
913
- * @internal
914
- */
915
- 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
916
- name: string;
917
- message: string;
918
- }) => Promise<void>;
919
- }
920
- /**
921
- * To be extended
922
- */
923
- interface DevframeRpcSharedStates {
924
- /**
925
- * Wire-service advertisements: package name → `{ package, version,
926
- * scope, meta }` for every installed {@link import('./services').DevframeServiceDefinition}.
927
- * Written by the node services host at the `ready()` barrier; clients read
928
- * it through `client.services` (or subscribe to it directly for
929
- * reactivity). Read-only from the browser.
930
- */
931
- 'devframe:services': DevframeServicesState;
932
- }
933
- interface DevframeViewHost {
934
- /**
935
- * Static mounts registered through {@link DevframeViewHost.hostStatic}, each
936
- * carrying the `resolveFrom` base it was mounted with so a build step that
937
- * copies these itself (rather than serving them live) re-resolves a remote
938
- * source to the same locally-installed copy it would serve live.
939
- *
940
- * @internal
941
- */
942
- buildStaticDirs: {
943
- baseUrl: string;
944
- source: StaticAssetsSource;
945
- resolveFrom?: string | null;
946
- }[];
947
- /**
948
- * Helper to host static files
949
- * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
950
- * - In `build` mode, it will copy the static files to the dist directory
951
- *
952
- * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
953
- * declaration served through devframe's caching CDN back-proxy.
954
- *
955
- * `defaultResolveFrom` overrides, for this call only, the context's own
956
- * `importMetaUrl` as the default `resolveFrom` for a remote source that
957
- * doesn't set one. A shared host that mounts assets on behalf of another
958
- * devframe (a hub installing a plugin) passes that plugin's `importMetaUrl`
959
- * so the assets resolve against the plugin's dependency graph rather than
960
- * the host's.
961
- */
962
- hostStatic: (baseUrl: string, source: StaticAssetsSource, defaultResolveFrom?: string | null) => void;
963
- }
964
- type AnyRpcFn = (...args: any[]) => any;
965
- /**
966
- * Resolve a scoped bare name `T` back to its fully-qualified entry in
967
- * `Registry` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
968
- * guard keeps TypeScript from rejecting a generic index into the
969
- * key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
970
- * the registry, while preserving each name's argument/return typing.
627
+ * Resolve a scoped bare name `T` back to its fully-qualified entry in
628
+ * `Registry` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
629
+ * guard keeps TypeScript from rejecting a generic index into the
630
+ * key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
631
+ * the registry, while preserving each name's argument/return typing.
971
632
  */
972
633
  type ScopedRpcFn<Registry, NS extends string, T extends string> = `${NS}:${T}` extends keyof Registry ? Extract<Registry[`${NS}:${T}`], AnyRpcFn> : AnyRpcFn;
973
634
  /**
@@ -1334,1085 +995,1424 @@ interface DevframeServicesHost {
1334
995
  ready: () => Promise<void>;
1335
996
  }
1336
997
  /**
1337
- * Classification of how a devframe is being deployed. Hosted adapters
1338
- * (`vite`, `embedded`) share their origin with a host app and must
1339
- * namespace their mount path under `/__<id>/`. Standalone adapters
1340
- * (`cli`, `build`) own the origin and default to `/`.
1341
- */
1342
- type DevframeDeploymentKind = 'standalone' | 'hosted';
1343
- /**
1344
- * How a hub deduplicates devframes that share an `id` when more than one
1345
- * is mounted onto the same hub. See {@link DevframeDefinition.duplicationStrategy}.
1346
- *
1347
- * - `'warn'` (default): keep the first registration, drop later
1348
- * duplicates, and emit a warning diagnostic (`DF8105`).
1349
- * - `'silent'`: drop later duplicates without warning.
1350
- * - `'throw'`: throw when a duplicate is mounted.
1351
- * - `'duplicate'`: let every instance coexist under a disambiguated
1352
- * dock id.
1353
- */
1354
- type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate';
1355
- /**
1356
- * Controls where the browser opens the RPC WebSocket, advertised in
1357
- * `__connection.json` and used to bind the dev server. The three shapes map
1358
- * to the three connection scenarios; precedence is `url` > `port` > `route`:
1359
- *
1360
- * 1. **Same server, different route** (default): leave `port`/`url` unset.
1361
- * The socket shares the HTTP server's port and binds to `route`
1362
- * (`__ws`). The client connects to its own origin, so the link
1363
- * survives a reverse proxy that rewrites the host/port/subpath.
1364
- *
1365
- * 2. **Different port**: set `port`. The socket binds on its own port on the
1366
- * same host; the client targets `ws(s)://<page-host>:<port>/<route>`.
1367
- *
1368
- * 3. **Remote, different origin**: set `url` to a full `ws://`/`wss://`
1369
- * endpoint (e.g. a public tunnel or relay). The client uses it verbatim.
998
+ * To be extended
1370
999
  */
1371
- interface DevframeWsOptions {
1000
+ interface DevframeRpcClientFunctions {
1001
+ /** Clear cached RPC results and refresh cache eligibility. */
1002
+ 'devframe:rpc:cache:invalidate': () => Promise<void>;
1003
+ /** Invoke a tool registered in this browser document. @internal */
1004
+ 'devframe:agent:invoke-client-tool': (id: string, args: Record<string, unknown>) => Promise<unknown>;
1372
1005
  /**
1373
- * Upgrade route segment the socket binds to and is advertised at, relative
1374
- * to the SPA base. Default: `__ws`.
1006
+ * Server→client notification that this connection's auth token has been
1007
+ * revoked. The client drops to untrusted on receipt. Broadcast by
1008
+ * `revokeActiveConnectionsForToken`.
1009
+ *
1010
+ * @internal
1375
1011
  */
1376
- route?: string;
1012
+ 'devframe:auth:revoked': () => Promise<void>;
1377
1013
  /**
1378
- * Bind the socket on its own port instead of sharing the HTTP port. The
1379
- * browser connects to this port on the page's hostname. Implies
1380
- * {@link DevframeWsOptions.sidecar}.
1014
+ * Streaming chunk pushed from server to subscribed clients. Wired by
1015
+ * `RpcStreamingHost`; do not register manually.
1016
+ *
1017
+ * @internal
1381
1018
  */
1382
- port?: number;
1019
+ 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1383
1020
  /**
1384
- * Start a side-car WebSocket server on a free port, for hosts whose request
1385
- * handlers can't accept upgrades (Next.js route handlers, Nitro, Rsbuild).
1386
- * The resolved port is advertised in `__connection.json`, so the browser
1387
- * finds it without any further wiring. Set `port` instead to pin it.
1021
+ * Streaming terminator pushed from server to subscribed clients. Wired by
1022
+ * `RpcStreamingHost`; do not register manually.
1023
+ *
1024
+ * @internal
1388
1025
  */
1389
- sidecar?: boolean;
1390
- /**
1391
- * Advertise a fixed, fully-qualified endpoint on another origin (a full
1392
- * `ws://`/`wss://` URL). Takes precedence over `port`/`route` in the meta.
1026
+ 'devframe:streaming:end': (channel: string, id: string, error?: {
1027
+ name: string;
1028
+ message: string;
1029
+ }) => Promise<void>;
1030
+ /**
1031
+ * Server→client cancel for an in-flight upload. Wired by
1032
+ * `RpcStreamingHost`; do not register manually.
1033
+ *
1034
+ * @internal
1393
1035
  */
1394
- url?: string;
1395
- }
1396
- /**
1397
- * Controls the SSE RPC endpoint, the WebSocket-free transport for hosts
1398
- * and proxies where the upgrade isn't available. It rides whatever HTTP
1399
- * surface serves the instance (the same one serving `__connection.json`),
1400
- * so a relative `route` always resolves; there is no port plumbing of its
1401
- * own. Enabled by default alongside the WebSocket; pass `sse: false` to
1402
- * disable, or `ws: false` to run SSE-only (`backend: 'sse'`).
1403
- */
1404
- interface DevframeSseOptions {
1036
+ 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
1405
1037
  /**
1406
- * Route segment the SSE endpoint binds to and is advertised at, relative
1407
- * to the SPA base. Default: `__sse`.
1038
+ * Full shared-state snapshot pushed from server to subscribed clients.
1039
+ * Wired by `RpcSharedStateHost`; do not register manually.
1040
+ *
1041
+ * @internal
1408
1042
  */
1409
- route?: string;
1043
+ 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
1044
+ /**
1045
+ * Incremental shared-state patch pushed from server to subscribed clients.
1046
+ * Wired by `RpcSharedStateHost`; do not register manually.
1047
+ *
1048
+ * @internal
1049
+ */
1050
+ 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
1410
1051
  }
1411
1052
  /**
1412
- * An **optional** identity check layered on top of the origin gate. The
1413
- * route-based MCP endpoint trusts same-machine callers by default (the
1414
- * loopback origin gate is enough), so this is opt-in hardening for the cases
1415
- * where a same-machine process is not a trust boundary, like a LAN/tunnel
1416
- * origin, a shared/CI box, or a destructive tool surface:
1417
- *
1418
- * - a non-empty **bearer token string**: the request must carry
1419
- * `Authorization: Bearer <token>`, compared in constant time. Back it with
1420
- * an environment variable rather than a literal;
1421
- * - a **callback** `(request) => boolean | Promise<boolean>`: a custom
1422
- * identity check (validate a signed header, an mTLS-derived claim, …). It
1423
- * only governs identity and cannot relax the origin gate;
1424
- * - `false`, the default: **origin-only**, trusting same-machine callers.
1425
- */
1426
- type McpAuthorization = string | ((request: Request) => boolean | Promise<boolean>) | false;
1427
- /**
1428
- * The route-based MCP setting accepted everywhere a host mounts a devframe
1429
- * (`initDevframe` / `initHub` / `createDevServer` options, `createCac` /
1430
- * `--mcp`, the framework kits):
1431
- *
1432
- * - `'auto'`, the default: mount the route when the devframe exposes an
1433
- * agent surface (an `agent`-flagged RPC function, or a tool / resource /
1434
- * provider registered on `ctx.agent`) AND the optional `@devframes/agentic`
1435
- * peer (the MCP adapter and SDK) is installed. An empty agent surface
1436
- * mounts nothing and loads no MCP code; a non-empty one without the peer
1437
- * warns once (DF0078) and mounts nothing.
1438
- * - `true`: always mount at the default `__mcp` route; a missing
1439
- * `@devframes/agentic` throws DF0079.
1440
- * - `false`: never mount, and never probe or warn.
1441
- * - {@link McpRouteOptions}: always mount (like `true`), with a custom route
1442
- * path, origin allow-list, or {@link McpAuthorization} identity check.
1443
- *
1444
- * A mounted route trusts same-machine callers by default (the loopback
1445
- * origin gate), exactly like `mcp: true`.
1446
- */
1447
- type McpSetting = boolean | 'auto' | McpRouteOptions;
1448
- /**
1449
- * Configuration for the route-based MCP server mounted alongside the dev
1450
- * server (via {@link DevframeCliOptions.mcp}). The endpoint speaks
1451
- * the MCP Streamable-HTTP transport over the same origin as the SPA,
1452
- * exposing the definition's `ctx.agent` tools + shared-state resources to
1453
- * external MCP clients connected to the *running* server.
1053
+ * To be extended
1454
1054
  */
1455
- interface McpRouteOptions {
1055
+ interface DevframeRpcServerFunctions {
1056
+ /** Methods eligible for automatic client caching. @internal */
1057
+ 'devframe:rpc:cacheable-functions': () => Promise<string[]>;
1058
+ /** Replace this connection's browser-agent tool manifest, tagged with the calling tab's stable client id. @internal */
1059
+ 'devframe:agent:sync-client-tools': (clientId: string, tools: BrowserAgentToolManifest[], info: BrowserAgentClientInfo) => Promise<void>;
1456
1060
  /**
1457
- * Route segment the MCP endpoint binds to, relative to the SPA base.
1458
- * Default: `__mcp` (i.e. `/__mcp` standalone, `/__<id>/__mcp` hosted).
1061
+ * Authenticate a connection with a previously-issued bearer token; resolves
1062
+ * whether the connection is now trusted. The interactive handler is provided
1063
+ * by the host adapter (e.g. Vite DevTools); the standalone server registers
1064
+ * an auto-trust noop when `auth: false`.
1065
+ *
1066
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1067
+ * reachable before the connection is trusted; this is the *only* rule the
1068
+ * pre-trust gate applies, no per-method allowlist.
1069
+ *
1070
+ * @internal
1459
1071
  */
1460
- path?: string;
1072
+ 'anonymous:devframe:auth': (params: {
1073
+ authToken: string;
1074
+ ua: string;
1075
+ origin: string;
1076
+ }) => Promise<{
1077
+ isTrusted: boolean;
1078
+ }>;
1461
1079
  /**
1462
- * Optional identity check, layered on top of the origin gate and checked
1463
- * **after** it. Defaults to origin-only (`false`): the route trusts
1464
- * same-machine callers, since the loopback origin gate already keeps
1465
- * arbitrary remote/browser callers out. Set a bearer token or a callback to
1466
- * harden the route when a same-machine process is not your trust boundary
1467
- * (LAN/tunnel origin, shared/CI host, destructive tools). See
1468
- * {@link McpAuthorization}.
1080
+ * Exchange a one-time authentication code (shown by the dev server) for a fresh,
1081
+ * node-issued bearer token, returning the token on success or `null`. The
1082
+ * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
1083
+ *
1084
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1085
+ * reachable before the connection is trusted.
1086
+ *
1087
+ * @internal
1469
1088
  */
1470
- authorization?: McpAuthorization;
1089
+ 'anonymous:devframe:auth:exchange': (params: {
1090
+ code: string;
1091
+ ua: string;
1092
+ origin: string;
1093
+ }) => Promise<{
1094
+ authToken: string | null;
1095
+ }>;
1471
1096
  /**
1472
- * Extra `Origin` header values to accept beyond the loopback default
1473
- * (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less native client).
1474
- * Add your LAN/tunnel origin here when reaching the endpoint from another
1475
- * host, mirroring the WS transport's origin gate. Pass `false` to disable
1476
- * origin checking entirely (not recommended). Default: loopback-only.
1097
+ * Ask the server to print its auth banner (code + magic link) for this
1098
+ * client, e.g. when an auth UI first shows; `reissue: true` rotates the code
1099
+ * first (the manual "re-issue" action). Registered by
1100
+ * `recipes/interactive-auth`; a repeat request for an already-printed code
1101
+ * is a no-op.
1477
1102
  *
1478
- * This is the endpoint's DNS-rebinding protection: the shared
1479
- * `isAllowedOrigin` gate the WS upgrade already uses, applied as external
1480
- * middleware (the approach the MCP SDK now recommends over its own
1481
- * deprecated `allowedHosts`/`allowedOrigins` transport flags). When you
1482
- * widen it past loopback, layer on {@link McpRouteOptions.authorization} to
1483
- * prove identity too.
1103
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1104
+ * reachable before the connection is trusted.
1105
+ *
1106
+ * @internal
1484
1107
  */
1485
- allowedOrigins?: readonly string[] | false;
1108
+ 'anonymous:devframe:auth:request-code': (params: {
1109
+ ua: string;
1110
+ origin: string;
1111
+ reissue?: boolean;
1112
+ }) => Promise<void>;
1486
1113
  /**
1487
- * Which shared-state keys the route exposes to agents, through the built-in
1488
- * `devframe_state_read` tool and the `devframe://state/<key>` resources.
1489
- * - `true` (default): every key the host publishes
1490
- * - `false`: none
1491
- * - `(key) => boolean`: only the keys the filter accepts
1114
+ * Self-revoke: the caller asks the server to revoke its own bearer token
1115
+ * (if any) and drop to untrusted. Requires an already-trusted caller, so
1116
+ * unlike the two handshake methods above it does **not** carry the
1117
+ * `anonymous:` prefix. Registered by `recipes/interactive-auth`.
1492
1118
  *
1493
- * The states themselves stay available to the UI over RPC.
1119
+ * @internal
1494
1120
  */
1495
- exposeSharedState?: boolean | ((key: string) => boolean);
1121
+ 'devframe:auth:revoke': () => Promise<void>;
1496
1122
  /**
1497
- * Usage guidance for the route, sent as `instructions` in the MCP
1498
- * `initialize` (and `server/discover`) result. An MCP client can add it to
1499
- * the coding agent's context, for example to say which tool to call first.
1500
- * Omitted or empty sends no instructions.
1123
+ * Subscribe a client to a shared-state key. Wired by
1124
+ * `RpcSharedStateHost`; do not register manually.
1125
+ *
1126
+ * @internal
1501
1127
  */
1502
- instructions?: string;
1128
+ 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
1503
1129
  /**
1504
- * Resource templates the route serves through MCP
1505
- * `resources/templates/list`. `resources/read` tries them after the
1506
- * registered resources and the shared-state resources (a
1507
- * `devframe://resource/` or `devframe://state/` URI never reaches them);
1508
- * the first template whose `uriTemplate` matches the URI reads it. `resources/list` adds the
1509
- * entries each template's `list` returns. Omitted or empty keeps the
1510
- * resources unchanged.
1130
+ * Read the current value for a shared-state key. Wired by
1131
+ * `RpcSharedStateHost`; do not register manually.
1132
+ *
1133
+ * @internal
1511
1134
  */
1512
- resourceTemplates?: McpResourceTemplate[];
1135
+ 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
1513
1136
  /**
1514
- * Host subscription for resource-list changes. Call `notify()` each time
1515
- * the resources change (for example, the entries of a resource template);
1516
- * the route then sends `notifications/resources/list_changed`. Return a
1517
- * function that removes the subscription; the route calls it on teardown.
1137
+ * Replace a shared-state value (from the client). Wired by
1138
+ * `RpcSharedStateHost`; do not register manually.
1139
+ *
1140
+ * @internal
1518
1141
  */
1519
- onResourcesChanged?: McpResourcesChangedHook;
1142
+ 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
1520
1143
  /**
1521
- * Prompt templates the route serves through MCP `prompts/list` and
1522
- * `prompts/get`. Omitted or empty declares no `prompts` capability.
1144
+ * Apply a patch to a shared-state value (from the client). Wired by
1145
+ * `RpcSharedStateHost`; do not register manually.
1146
+ *
1147
+ * @internal
1523
1148
  */
1524
- prompts?: McpPrompt[];
1525
- }
1526
- interface DevframeCliOptions {
1527
- /** Binary name; default: the devframe's `id`. */
1528
- command?: string;
1529
- /** Preferred port for the dev server (default 9999). */
1530
- port?: number;
1531
- /** Port scan range, forwarded to `get-port-please`. */
1532
- portRange?: [number, number];
1533
- /** Prefer a random open port. */
1534
- random?: boolean;
1535
- /** Default host to bind to; `--host` overrides. */
1536
- host?: string;
1149
+ 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
1537
1150
  /**
1538
- * Auto-open the browser when the dev server starts.
1539
- * `true` opens the resolved origin; a string opens that relative path.
1540
- * The `--open` / `--no-open` flags override this.
1151
+ * Client→server streaming subscription with optional replay cursor.
1152
+ * Wired by `RpcStreamingHost`; do not register manually.
1153
+ *
1154
+ * @internal
1541
1155
  */
1542
- open?: boolean | string;
1156
+ 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
1157
+ afterSeq?: number;
1158
+ }) => Promise<void>;
1543
1159
  /**
1544
- * Authentication for the standalone dev server.
1545
- *
1546
- * - `undefined` / `true`: the standalone adapters (`cli` /
1547
- * served `build`) auto-wire devframe's interactive OTP auth
1548
- * (`createInteractiveAuth`): an untrusted client can only reach
1549
- * `anonymous:` methods until it exchanges the printed one-time code.
1550
- * The adapter prints the code + magic-link banner once the server is
1551
- * listening.
1552
- * - `false`: no gate, for trusted single-user localhost tools where an
1553
- * auth round-trip only gets in the way (the built-in plugins set this).
1554
- * The `--no-auth` CLI flag maps here for one-off runs.
1555
- * - A {@link DevframeAuthHandler}: a custom handler (e.g. a tuned
1556
- * `createInteractiveAuth`, or an entirely different scheme) passed
1557
- * straight through to the RPC transport binding.
1558
- *
1559
- * Hosted adapters (`vite`, `embedded`) ignore this and defer to the host's
1560
- * auth; `@vitejs/devtools` honors the equivalent `devtools.clientAuth`.
1160
+ * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
1161
+ * do not register manually.
1561
1162
  *
1562
- * @default true
1163
+ * @internal
1563
1164
  */
1564
- auth?: boolean | DevframeAuthHandler;
1165
+ 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
1565
1166
  /**
1566
- * How the browser reaches the RPC WebSocket. Defaults to sharing the HTTP
1567
- * port on the `__ws` route. See {@link DevframeWsOptions} for the
1568
- * different-port and remote-origin variants. Pass `false` to serve no
1569
- * WebSocket at all; clients connect over SSE instead (`backend: 'sse'`).
1570
- */
1571
- ws?: DevframeWsOptions | false;
1572
- /**
1573
- * How the browser reaches the SSE RPC endpoint, enabled by default
1574
- * alongside the WebSocket as the more portable transport. Pass `false`
1575
- * to disable, or a {@link DevframeSseOptions} to change its route.
1167
+ * Client→server streaming cancellation request. Wired by
1168
+ * `RpcStreamingHost`; do not register manually.
1169
+ *
1170
+ * @internal
1576
1171
  */
1577
- sse?: boolean | DevframeSseOptions;
1172
+ 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
1578
1173
  /**
1579
- * Capability-side CAC hook. Called with the CAC instance after the
1580
- * adapter registers its built-in commands (`build` / `mcp`)
1581
- * but before `createCac`'s own `configureCli` caller. Use this to
1582
- * contribute tool-specific flags and subcommands from the definition
1583
- * itself.
1174
+ * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
1175
+ * register manually.
1176
+ *
1177
+ * @internal
1584
1178
  */
1585
- configure?: (cli: CAC) => void;
1179
+ 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1586
1180
  /**
1587
- * Typed CLI flags for the default `dev` command, backed by any
1588
- * [Standard Schema](https://standardschema.dev/) validator (valibot,
1589
- * zod, arktype, or devframe's built-in `s`). The adapter registers
1590
- * matching `--kebab-key` options on CAC, validates the parsed values,
1591
- * and forwards the typed bag to `setup(ctx, { flags })`.
1592
- *
1593
- * Use {@link defineCliFlags} to preserve the literal schema-map
1594
- * shape, and {@link InferCliFlags} to recover the typed output at the
1595
- * call site:
1596
- *
1597
- * ```ts
1598
- * const appFlags = defineCliFlags({
1599
- * depth: v.pipe(v.number(), v.integer()),
1600
- * config: v.optional(v.string()),
1601
- * })
1181
+ * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
1182
+ * register manually.
1602
1183
  *
1603
- * defineDevframe({
1604
- * cli: { flags: appFlags },
1605
- * setup(ctx, info) {
1606
- * const flags = info.flags as InferCliFlags<typeof appFlags>
1607
- * },
1608
- * })
1609
- * ```
1184
+ * @internal
1610
1185
  */
1611
- flags?: CliFlagsSchema;
1186
+ 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
1187
+ name: string;
1188
+ message: string;
1189
+ }) => Promise<void>;
1612
1190
  }
1613
1191
  /**
1614
- * Default dock attributes for the iframe entry a hub synthesizes when it
1615
- * mounts this devframe. Framework-neutral metadata only; the hub layer
1616
- * (`ctx.install`) merges these beneath its per-mount `dock` overrides,
1617
- * which in turn sit beneath the locked, derived `id` / `type` / `url`.
1618
- *
1619
- * Every field is optional. `title` / `icon` default to the definition's
1620
- * `name` / `icon` when omitted here; the rest are unset by default.
1621
- * Standalone adapters (`cli` / `build`) ignore this entirely.
1192
+ * To be extended
1622
1193
  */
1623
- interface DevframeDockDefaults {
1624
- /** Dock entry title. Defaults to the definition's `name`. */
1625
- title?: string;
1626
- /** Translations of `title` by BCP 47 tag; see `@devframes/hub`'s `resolveTitle()`. */
1627
- titleLocales?: Record<string, string>;
1628
- /** Dock entry icon. Defaults to the definition's `icon`. */
1629
- icon?: string | {
1630
- light: string;
1631
- dark: string;
1632
- };
1194
+ interface DevframeRpcSharedStates {
1633
1195
  /**
1634
- * Sort weight within the dock; higher sorts earlier.
1635
- * @default 0
1196
+ * Wire-service advertisements: package name → `{ package, version,
1197
+ * scope, meta }` for every installed {@link import('./services').DevframeServiceDefinition}.
1198
+ * Written by the node services host at the `ready()` barrier; clients read
1199
+ * it through `client.services` (or subscribe to it directly for
1200
+ * reactivity). Read-only from the browser.
1636
1201
  */
1637
- defaultOrder?: number;
1202
+ 'devframe:services': DevframeServicesState;
1203
+ }
1204
+ interface DevframeNodeRpcSession {
1205
+ meta: DevframeNodeRpcSessionMeta;
1206
+ rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
1207
+ }
1208
+ interface RpcBroadcastOptions<METHOD, Args extends any[]> {
1209
+ method: METHOD;
1210
+ args: Args;
1211
+ optional?: boolean;
1212
+ event?: boolean;
1213
+ filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
1214
+ }
1215
+ type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1638
1216
  /**
1639
- * Category the entry groups under in the dock.
1640
- * @default 'default'
1217
+ * Invoke a locally registered server RPC function directly.
1218
+ *
1219
+ * This bypasses transport and is useful for server-side cross-function calls.
1641
1220
  */
1642
- category?: string;
1221
+ invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1643
1222
  /**
1644
- * Conditional-visibility expression (same syntax as command `when`
1645
- * clauses). Set to `'false'` to hide the entry unconditionally.
1223
+ * Broadcast a message to all connected clients
1646
1224
  */
1647
- when?: string;
1225
+ broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1648
1226
  /**
1649
- * Render-only visibility expression, same syntax as {@link when}. Hides the
1650
- * entry's own dock-bar button when it evaluates to `false` while leaving it
1651
- * registered and reachable (activation, RPC lookups, etc.), unlike `when`,
1652
- * which is the general relevance switch for the entry as a whole.
1227
+ * Get the current RPC client
1228
+ *
1229
+ * Available in RPC functions to get the current RPC client
1653
1230
  */
1654
- visibility?: string;
1655
- /** Badge text rendered on the dock icon (e.g. an unread count). */
1656
- badge?: string;
1657
- /** Id of the dock group this entry collapses under, if any. */
1658
- groupId?: string;
1231
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1659
1232
  /**
1660
- * A client script the hub imports into the host page (this devframe's **page
1661
- * script**). An absolute-path `importFrom` is served by the hub under the
1662
- * mount base and rewritten to that URL, so mounting by package name needs no
1663
- * host wiring; a URL or bare specifier passes through untouched.
1233
+ * The shared state host
1664
1234
  */
1665
- clientScript?: {
1666
- /**
1667
- * Initialize after RPC trust without waiting for dock activation.
1668
- * @default false
1669
- */
1670
- eager?: boolean;
1671
- /** An absolute filesystem path, a served URL, or a bare npm specifier. */
1672
- importFrom: string;
1673
- /**
1674
- * The name to import the module as.
1675
- * @default 'default'
1676
- */
1677
- importName?: string;
1678
- };
1679
- }
1680
- /**
1681
- * Runtime information threaded into `setup(ctx, info)`. Adapters
1682
- * populate the fields that make sense for their deployment. In
1683
- * particular, `createCac` fills `flags` with the parsed CAC bag.
1684
- */
1685
- interface DevframeSetupInfo {
1686
- /** Parsed CLI flags, populated by the CLI adapter. */
1687
- flags?: Record<string, unknown>;
1688
- }
1689
- interface DevframeDefinition {
1690
- id: string;
1691
- name: string;
1692
- /** Semver of the tool, surfaced in hub UIs and diagnostics. */
1693
- version: string;
1694
- /** npm package name the devframe ships in (e.g. `@scope/my-tool`). */
1695
- packageName: string;
1235
+ sharedState: RpcSharedStateHost;
1696
1236
  /**
1697
- * `import.meta.url` of the module that defines this devframe. **Always
1698
- * provide it** (`importMetaUrl: import.meta.url`): it is the resolution base
1699
- * for the tool's own dependency graph, which lets the host resolve
1700
- * everything against the plugin's own installed packages rather than the
1701
- * consuming app's.
1702
- *
1703
- * - **Remote assets**: becomes the default `resolveFrom` for any remote
1704
- * {@link StaticAssetsSource} the devframe hosts (its `clientAssets`, and
1705
- * every `ctx.views.hostStatic` call) that doesn't set one explicitly, so a
1706
- * locally installed copy of the assets package is served with zero
1707
- * network. A per-asset `resolveFrom` still wins, and an explicit
1708
- * `resolveFrom: null` still opts out.
1709
- * - **Service dependencies**: becomes the base the host resolves declared
1710
- * {@link DevframeServiceInput | services} from, so a plugin can ship a
1711
- * service package as its own dependency instead of asking users to install
1712
- * it.
1237
+ * The streaming channel host. Provides per-channel `start()` /
1238
+ * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1713
1239
  *
1714
- * Optional for backward compatibility; omitting it falls back to
1715
- * runtime-directory resolution and disables the zero-network installed-copy
1716
- * fast paths above.
1240
+ * @see RpcStreamingHost
1717
1241
  */
1718
- importMetaUrl?: string;
1719
- /** Project homepage or documentation URL. */
1720
- homepage: string;
1721
- /** One-line summary of what the tool does. */
1722
- description: string;
1723
- icon?: string | {
1724
- light: string;
1725
- dark: string;
1726
- };
1242
+ streaming: RpcStreamingHost;
1243
+ };
1244
+ interface RpcSharedStateGetOptions<T> {
1245
+ sharedState?: SharedState<T>;
1246
+ initialValue?: T;
1247
+ }
1248
+ interface RpcSharedStateHost {
1249
+ get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
1250
+ keys: () => string[];
1727
1251
  /**
1728
- * Default dock attributes applied when a hub mounts this devframe as an
1729
- * iframe dock entry. Consulted only by the hub install path (`ctx.install`),
1730
- * which merge these beneath the per-mount `dock` overrides; standalone
1731
- * adapters (`cli` / `build`) ignore it.
1732
- *
1733
- * @see {@link DevframeDockDefaults}
1252
+ * Subscribe to new shared-state keys becoming available. Fires when
1253
+ * `get(key, ...)` creates a fresh entry (not on subsequent gets).
1254
+ * Useful for protocol adapters (e.g. MCP) that surface shared state
1255
+ * as dynamic resources.
1734
1256
  */
1735
- dock?: DevframeDockDefaults;
1257
+ onKeyAdded: (fn: (key: string) => void) => () => void;
1736
1258
  /**
1737
- * Mount path override. Defaults depend on the adapter:
1738
- * `/` for standalone (`cli` / `build`), `/__<id>/` for hosted
1739
- * (`vite` / `embedded`).
1259
+ * Unregister a shared state and drop its broadcast listeners. Returns
1260
+ * `true` when a state was removed, `false` when the key was unknown.
1261
+ * Used by short-lived states (e.g. a disposed JSON-render view) to avoid
1262
+ * leaking listeners and lingering entries for the context lifetime.
1740
1263
  */
1741
- basePath?: string;
1264
+ delete: (key: string) => boolean;
1265
+ }
1266
+ /**
1267
+ * Options for `RpcStreamingHost.create()`.
1268
+ */
1269
+ interface RpcStreamingChannelOptions {
1742
1270
  /**
1743
- * How a hub reacts when another devframe sharing this one's `id` is
1744
- * mounted onto the same hub. Consulted only by hub adapters
1745
- * (`ctx.install`); standalone adapters (`cli` / `build`)
1746
- * ignore it.
1271
+ * Size of the per-stream ring buffer kept on the server for
1272
+ * replay-on-resubscribe. `0` (default) disables replay; on reconnect
1273
+ * the consumer only sees chunks that arrive after subscribing.
1747
1274
  *
1748
- * @default 'warn'
1275
+ * The buffer is per stream id, not per channel; each `channel.start()`
1276
+ * gets its own.
1749
1277
  */
1750
- duplicationStrategy?: DevframeDuplicationStrategy;
1278
+ replayWindow?: number;
1751
1279
  /**
1752
- * Declares which runtimes meaningfully support this devframe. Adapters
1753
- * act on a `false` before doing any work:
1280
+ * Milliseconds a closed stream is retained on the server after its
1281
+ * last subscriber leaves (or if no subscriber ever arrived). During
1282
+ * this window, late subscribers can still join and replay the buffer
1283
+ * + receive the `end` frame.
1754
1284
  *
1755
- * - `build: false`: `createCac` skips registering the `build` subcommand,
1756
- * `createBuild` refuses (throws `DF0042`) unless `{ force: true }`, and
1757
- * a hub static build (`buildHub`) silently skips the devframe - no dock,
1758
- * no SPA copy, no RPCs in the dump. Useful for a devframe whose value is
1759
- * inherently live (e.g. it manages real files on disk), so a static
1760
- * export would only ever produce a broken, write-less shell of the tool.
1761
- * - `dev: false`: `createDevServer` refuses (throws `DF0058`) unless
1762
- * `{ force: true }`. Useful for a devframe that only makes sense as a
1763
- * static export (e.g. a report generator with nothing to serve live).
1285
+ * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
1286
+ * (immediate free). Set to `0` to opt out, or higher for longer
1287
+ * post-mortem replay.
1764
1288
  */
1765
- capabilities?: {
1766
- dev?: boolean;
1767
- build?: boolean;
1768
- };
1289
+ closedStreamRetention?: number;
1290
+ }
1291
+ /**
1292
+ * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
1293
+ * channel owns a wire namespace; calling `start()` produces individual
1294
+ * streams keyed by id.
1295
+ *
1296
+ * @see {@link https://devfra.me/guide/streaming Streaming guide}
1297
+ */
1298
+ interface RpcStreamingChannel<T = unknown> {
1299
+ /** Channel name as registered with `ctx.rpc.streaming.create()`. */
1300
+ readonly name: string;
1769
1301
  /**
1770
- * Wire services this devframe consumes (see `DevframeServiceDefinition`).
1771
- * Each entry is either a declarative descriptor
1772
- * (`{ package, version?, required?, options? }`, where the host imports the
1773
- * package's default-export factory, resolving it against **this plugin's
1774
- * own dependencies**) or a ready `DevframeServiceDefinition` (the factory
1775
- * was already called). The adapter queues these before `setup(ctx)` runs
1776
- * and constructs each service once at the `ctx.services.ready()` barrier,
1777
- * merging option sets across every declarer. Missing services are skipped
1778
- * unless marked `required`; clients feature-detect via
1779
- * `client.services.has(pkg)` and degrade.
1302
+ * Start a new stream. Returns a server-side sink with both an imperative
1303
+ * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
1304
+ * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
1305
+ * disconnects or cancels.
1780
1306
  */
1781
- services?: DevframeServiceInput[];
1307
+ start: (opts?: {
1308
+ id?: string;
1309
+ }) => StreamSink<T>;
1782
1310
  /**
1783
- * Author's SPA dist, served as the devframe's UI. A local directory, or
1784
- * a {@link StaticAssetsSource} remote declaration (`{ package, version }`)
1785
- * served through devframe's caching CDN back-proxy so the assets need not
1786
- * ship inside the node package.
1311
+ * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
1312
+ * The pipe uses `sink.signal` so cancellation propagates upstream.
1787
1313
  *
1788
- * Consumed by every adapter that serves the UI (`dev`, `build`, `vite`,
1789
- * `next`, and the hub install path).
1314
+ * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
1315
+ * before passing it here.
1790
1316
  */
1791
- clientAssets?: StaticAssetsSource;
1792
- /** RPC-level configuration for this devframe (see {@link DevframeRpcOptions}). */
1793
- rpc?: DevframeRpcOptions;
1794
- /** Server-side setup: the primary entrypoint. Runs in every runtime. */
1795
- setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise<void>;
1796
- cli?: DevframeCliOptions;
1797
- }
1798
- interface DevframeRpcOptions {
1317
+ pipeFrom: (readable: ReadableStream<T>, opts?: {
1318
+ id?: string;
1319
+ }) => Promise<StreamSink<T>>;
1320
+ /** Look up an active stream by id. Returns `undefined` if none. */
1321
+ get: (id: string) => StreamSink<T> | undefined;
1322
+ /** All active outbound stream ids on this channel. */
1323
+ ids: () => string[];
1799
1324
  /**
1800
- * Opt an RPC function into the static-build snapshot **without owning its
1801
- * definition**, the mechanism a devframe uses to bake a wire service's
1802
- * RPC (e.g. `@devframes/service-git`'s `status`/`log`/`show`) into its
1803
- * `build` export, since the service itself defines no `dump`/`snapshot`.
1325
+ * Open an inbound stream: the server side of a client-to-server
1326
+ * upload. Allocates an id, returns a `StreamReader<T>` that fills as
1327
+ * the client writes chunks. Typical pattern is to call this from an
1328
+ * action handler, kick off background processing, and return the id
1329
+ * so the caller can start uploading:
1804
1330
  *
1805
- * Each entry is either a bare method id (bakes the no-argument call, like
1806
- * `snapshot: true`) or `{ method, inputs }` where `inputs` is the list of
1807
- * argument-tuples to bake, or an async provider given the node context
1808
- * (so it can enumerate at build time, e.g. read commit hashes via the
1809
- * service's node API). `createBuild` resolves these after setup and
1810
- * executes the target's own handler per tuple; the first tuple's result
1811
- * becomes the fallback so any call variant resolves to a baked value.
1331
+ * ```ts
1332
+ * handler: async () => {
1333
+ * const reader = channel.openInbound()
1334
+ * ;(async () => {
1335
+ * for await (const chunk of reader) processChunk(chunk)
1336
+ * })()
1337
+ * return { uploadId: reader.id }
1338
+ * }
1339
+ * ```
1340
+ *
1341
+ * Calling `reader.cancel()` on the server sends an `upload-cancel` to
1342
+ * the uploading client, which aborts its sink.
1812
1343
  */
1813
- snapshot?: DevframeSnapshotRpcEntry[];
1344
+ openInbound: (opts?: {
1345
+ id?: string;
1346
+ }) => StreamReader<T>;
1814
1347
  }
1815
- /** Argument-tuples to bake for a {@link DevframeSnapshotRpcEntry}, or a provider that computes them at build time. */
1816
- type DevframeSnapshotRpcInputs = readonly (readonly unknown[])[] | ((ctx: DevframeNodeContext) => readonly (readonly unknown[])[] | Promise<readonly (readonly unknown[])[]>);
1817
- /**
1818
- * One {@link DevframeRpcOptions.snapshot} entry: a bare method id (bakes
1819
- * the no-argument call) or a method plus the argument-tuples to bake.
1820
- */
1821
- type DevframeSnapshotRpcEntry = string | {
1822
- method: string;
1823
- inputs: DevframeSnapshotRpcInputs;
1824
- };
1825
- type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
1826
- type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
1827
- type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
1828
- type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
1829
- type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
1830
- type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]>; };
1831
1348
  /**
1832
- * Serializable patch describing a single mutation to a `SharedState`.
1833
- * Structurally compatible with JSON-Patch and is safe to send over RPC.
1349
+ * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
1350
+ * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
1351
+ * and wire namespace.
1834
1352
  */
1835
- interface SharedStatePatch {
1836
- op: 'add' | 'remove' | 'replace';
1837
- path: readonly (string | number)[];
1838
- value?: unknown;
1353
+ interface RpcStreamingHost {
1354
+ /**
1355
+ * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
1356
+ * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
1357
+ * name is already taken.
1358
+ */
1359
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1360
+ /**
1361
+ * Adapters call this when a session disconnects so the host can drop
1362
+ * subscribers and abort orphaned streams. Most users do not need this;
1363
+ * it's wired by the RPC server binding automatically.
1364
+ *
1365
+ * @internal
1366
+ */
1367
+ _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
1368
+ }
1369
+ interface DevframeCapabilities {
1370
+ rpc?: boolean;
1371
+ views?: boolean;
1839
1372
  }
1840
1373
  /**
1841
- * State host that is immutable by default with explicit mutate.
1374
+ * Framework- and build-tool-agnostic node context: RPC + diagnostics +
1375
+ * agent + the view-host (HTTP file-serving). Host adapters can wrap this
1376
+ * to add their own surfaces; for example, `@vitejs/devtools-kit`'s
1377
+ * `createKitContext` adds `docks`, `terminals`, `messages`, and
1378
+ * `commands` when mounted into Vite DevTools. JSON rendering is an opt-in
1379
+ * integration (`@devframes/json-render`) layered on top, not part of this
1380
+ * core surface.
1842
1381
  */
1843
- interface SharedState<T> {
1382
+ interface DevframeNodeContext {
1383
+ readonly workspaceRoot: string;
1384
+ readonly cwd: string;
1844
1385
  /**
1845
- * Get the current state. Immutable.
1386
+ * Lifecycle distinction surfaced to plugin authors:
1387
+ *
1388
+ * - `'dev'`: long-running, interactive session. Connections come and
1389
+ * go; broadcasts and shared-state mutations are debounced
1390
+ * to keep the UI responsive.
1391
+ * - `'build'`: one-shot batch run. The context is set up, the devtool
1392
+ * collects what it needs, and a snapshot is written. No
1393
+ * live UI, no WS server.
1394
+ *
1395
+ * Names are inherited from Vite's serve/build dichotomy but the meaning
1396
+ * is general: the same distinction applies to any tool that runs in
1397
+ * either an interactive or a static-output mode.
1846
1398
  */
1847
- value: () => Immutable<T>;
1399
+ readonly mode: 'dev' | 'build';
1848
1400
  /**
1849
- * Subscribe to state changes.
1401
+ * Host runtime abstraction, exposing `mountStatic` / `resolveOrigin` /
1402
+ * `getStorageDir`.
1850
1403
  */
1851
- on: EventEmitter<SharedStateEvents<T>>['on'];
1404
+ host: DevframeHost;
1405
+ rpc: RpcFunctionsHost;
1406
+ views: DevframeViewHost;
1852
1407
  /**
1853
- * Mutate the state.
1408
+ * Structured diagnostics host; wraps `nostics` and lets integrations
1409
+ * register their own coded errors/warnings into the shared lookup.
1854
1410
  */
1855
- mutate: (fn: (state: T) => void, syncId?: string) => void;
1411
+ diagnostics: DevframeDiagnosticsHost;
1856
1412
  /**
1857
- * Apply patches to the state.
1413
+ * Agent host; aggregates the agent-exposed surface of this devtool.
1858
1414
  */
1859
- patch: (patches: SharedStatePatch[], syncId?: string) => void;
1415
+ agent: DevframeAgentHost;
1860
1416
  /**
1861
- * Sync IDs that have been applied to the state.
1417
+ * Cross-plugin services: a typed, namespaced registry through which one
1418
+ * integration exposes a capability (e.g. a data-source registry) and
1419
+ * others consume it without a hard package dependency. Ids follow the RPC
1420
+ * namespacing rule (`<plugin-id>:<service>`); types come from augmenting
1421
+ * the `DevframeServicesRegistry` interface. `whenAvailable` subscriptions
1422
+ * absorb setup-order differences between provider and consumer.
1862
1423
  */
1863
- syncIds: Set<string>;
1864
- }
1865
- interface SharedStateEvents<T> {
1866
- updated: (fullState: T, patches: SharedStatePatch[] | undefined, syncId: string) => void;
1867
- }
1868
- interface SharedStateOptions<T> {
1424
+ services: DevframeServicesHost;
1869
1425
  /**
1870
- * Initial state.
1426
+ * This context's own {@link ConnectionMeta.configs}: static, boot-time
1427
+ * config a host publishes once through the connection handshake and every
1428
+ * client reads read-only. A plain, **non-reactive** object: mutate it
1429
+ * during `setup(ctx)` (a plugin sets its keys, a hub aggregates across
1430
+ * every installed devframe), never during the session, since it's serialized
1431
+ * once, after setup, and changing it afterwards reaches no client.
1432
+ *
1433
+ * ```ts
1434
+ * ctx.staticConfig.dock = {
1435
+ * ...ctx.staticConfig.dock,
1436
+ * categoryOrder: { ...ctx.staticConfig.dock?.categoryOrder, ...myOrder },
1437
+ * }
1438
+ * ```
1871
1439
  */
1872
- initialValue: T;
1440
+ staticConfig: Partial<DevframeConnectionConfigsRegistry>;
1873
1441
  /**
1874
- * Enable patches.
1442
+ * Create a namespace-scoped view of this context. The returned
1443
+ * `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
1444
+ * key, and streaming channel with `my-plugin:`, and exposes a typed
1445
+ * top-level `settings` store. This is the preferred way to consume the
1446
+ * context from a single tool's setup code.
1875
1447
  *
1876
- * @default false
1448
+ * Pass `null` or `''` to un-scope and get the base context.
1877
1449
  */
1878
- enablePatches?: boolean;
1879
- }
1880
- declare function createSharedState<T extends object>(options: SharedStateOptions<T>): SharedState<T>;
1881
- /**
1882
- * Serialized error shape sent over the wire when a stream ends with a failure.
1883
- * Stays JSON-safe so the strict-JSON encoder can carry it without coercion.
1884
- */
1885
- interface StreamErrorPayload {
1886
- name: string;
1887
- message: string;
1450
+ scope: {
1451
+ <NS extends string>(namespace: NS): DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>;
1452
+ (namespace?: null | ''): DevframeNodeContext;
1453
+ };
1888
1454
  }
1889
1455
  /**
1890
- * Single buffered chunk in the server-side ring buffer.
1456
+ * Describes where the browser client should open its RPC WebSocket. The
1457
+ * object form is the proxy-flexible default: `path` is resolved relative to
1458
+ * where `__connection.json` was loaded, and the connection is made to the
1459
+ * page's own origin (only the `http`→`ws` / `https`→`wss` protocol swap is
1460
+ * applied). This survives reverse proxies that change the host/port, because
1461
+ * the client never trusts a server-baked hostname; it reuses its own.
1891
1462
  *
1892
- * Sequence numbers start at 1 and increment per write. Subscribers track
1893
- * `lastSeenSeq` and ask for `afterSeq` on resubscribe so the server can
1894
- * replay any chunks the client missed during a brief disconnect.
1463
+ * Set `port` (and/or `host`) only when the WS endpoint genuinely lives on a
1464
+ * different origin than the page, e.g. a side-car server on its own port.
1895
1465
  */
1896
- interface BufferedChunk<T> {
1897
- seq: number;
1898
- chunk: T;
1899
- }
1900
- interface StreamSinkEvents<T> {
1901
- /** Fired for each `write()`. The RPC layer subscribes and broadcasts. */
1902
- chunk: (seq: number, chunk: T) => void;
1903
- /** Terminal; fired exactly once per sink lifetime. */
1904
- end: (error?: StreamErrorPayload) => void;
1905
- }
1906
- interface CreateStreamSinkOptions {
1907
- id?: string;
1466
+ interface ConnectionMetaWebsocket {
1908
1467
  /**
1909
- * Size of the per-stream ring buffer kept for replay-on-resubscribe.
1910
- * `0` (default) disables replay.
1468
+ * Path to the WS endpoint. Relative paths (the default, e.g. `__ws`) are
1469
+ * resolved against `__connection.json`'s location; absolute paths (`/__ws`)
1470
+ * resolve against the page origin.
1911
1471
  */
1912
- replayWindow?: number;
1472
+ path?: string;
1473
+ /** Override the port. Combined with the page hostname unless `host` is set. */
1474
+ port?: number;
1475
+ /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1476
+ host?: string;
1913
1477
  }
1914
1478
  /**
1915
- * Server-side producer handle. Two equivalent surfaces share one piece of
1916
- * state: the imperative `write/error/close` triple, and a `WritableStream<T>`
1917
- * for `pipeTo`-style consumption.
1479
+ * Object form of {@link ConnectionMeta.sse}: the same proxy-safe shape and
1480
+ * resolution rules as {@link ConnectionMetaWebsocket}, producing an
1481
+ * `http(s)://` endpoint instead of a `ws(s)://` one. Set `port` (and/or
1482
+ * `host`) only when the SSE endpoint genuinely lives on a different origin
1483
+ * than the page, e.g. a side-car server on its own port.
1918
1484
  */
1919
- interface StreamSink<T> {
1920
- /** Stable id used by clients to subscribe. */
1921
- readonly id: string;
1922
- /**
1923
- * Aborts when the consumer cancels (server-side) or when the transport
1924
- * loses every subscriber. Producers should poll `signal.aborted` and exit
1925
- * cleanly.
1926
- */
1927
- readonly signal: AbortSignal;
1928
- /** `true` after `close()` / `error()`. Further writes throw. */
1929
- readonly closed: boolean;
1930
- /** Last allocated sequence number. `0` until the first write. */
1931
- readonly lastSeq: number;
1932
- write: (chunk: T) => void;
1933
- error: (reason: unknown) => void;
1934
- close: () => void;
1935
- /** External-cancel path. Aborts the signal so handlers can short-circuit. */
1936
- abort: (reason?: unknown) => void;
1937
- /** `WritableStream<T>` adapter; same in-memory state as the imperative API. */
1938
- readonly writable: WritableStream<T>;
1939
- /**
1940
- * Internal: RPC layer subscribes to receive chunk/end notifications.
1941
- * Not part of the public contract; do not call directly.
1942
- *
1943
- * @internal
1944
- */
1945
- readonly events: EventEmitter<StreamSinkEvents<T>>;
1485
+ interface ConnectionMetaSse {
1946
1486
  /**
1947
- * Internal replay buffer. RPC layer reads on (re)subscribe to feed
1948
- * missed chunks before going live.
1949
- *
1950
- * @internal
1487
+ * Path to the SSE endpoint. Relative paths (the default, e.g. `__sse`)
1488
+ * are resolved against `__connection.json`'s location; absolute paths
1489
+ * (`/__sse`) resolve against the page origin.
1951
1490
  */
1952
- readonly buffer: ReadonlyArray<BufferedChunk<T>>;
1491
+ path?: string;
1492
+ /** Override the port. Combined with the page hostname unless `host` is set. */
1493
+ port?: number;
1494
+ /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1495
+ host?: string;
1953
1496
  }
1954
- interface CreateStreamReaderOptions {
1955
- id?: string;
1497
+ interface ConnectionMeta {
1956
1498
  /**
1957
- * Maximum number of buffered chunks held client-side while the consumer
1958
- * isn't draining. On overflow, the oldest chunk is dropped.
1499
+ * The server's primary live-RPC transport (`websocket` / `sse`), `static`
1500
+ * for a pre-computed RPC dump with no live server, or `none` for a server
1501
+ * exposing no RPC transport at all (e.g. an MCP-only deployment).
1959
1502
  */
1960
- highWaterMark?: number;
1503
+ backend: 'websocket' | 'sse' | 'static' | 'none';
1961
1504
  /**
1962
- * Called when the chunk queue overflows the high-water mark. The RPC
1963
- * layer wires this to a coded warning; the primitive itself is
1964
- * RPC-agnostic.
1505
+ * WebSocket endpoint, resolved by the client into a `ws(s)://` URL:
1506
+ *
1507
+ * - {@link ConnectionMetaWebsocket}: the proxy-flexible default; a
1508
+ * same-origin path relative to `__connection.json`.
1509
+ * - `number`: a port on the page's hostname (`ws(s)://<host>:<port>`).
1510
+ * - `string`: a full `ws://`/`wss://` URL used verbatim, an `http(s)://`
1511
+ * URL with its protocol swapped, or a path resolved same-origin.
1965
1512
  */
1966
- onOverflow?: (dropped: number) => void;
1967
- /** Called when the consumer cancels; the RPC layer sends `:cancel` upstream. */
1968
- onCancel?: () => void;
1969
- }
1970
- /**
1971
- * Client-side consumer handle. Both an `AsyncIterable<T>` (for `for await`)
1972
- * and exposes `readable: ReadableStream<T>` (for `pipeTo`). Pick one; they
1973
- * share a single internal queue, so concurrent draining will race.
1974
- */
1975
- interface StreamReader<T> extends AsyncIterable<T> {
1976
- readonly id: string;
1977
- readonly cancelled: boolean;
1978
- readonly done: boolean;
1979
- /** Highest `seq` observed. Used for replay on reconnect. */
1980
- readonly lastSeenSeq: number;
1981
- /** `ReadableStream<T>` adapter for `pipeTo`-style consumption. */
1982
- readonly readable: ReadableStream<T>;
1983
- cancel: () => void;
1984
- /** @internal */
1985
- _push: (seq: number, chunk: T) => void;
1986
- /** @internal */
1987
- _end: (error?: StreamErrorPayload) => void;
1988
- }
1989
- /**
1990
- * Build a server-side stream sink. RPC-agnostic; the RPC host wires
1991
- * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
1992
- * for late or reconnecting subscribers.
1993
- */
1994
- declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamSink<T>;
1995
- /**
1996
- * Build a client-side stream reader. RPC-agnostic; the RPC host calls
1997
- * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
1998
- * terminal frame. Consumers iterate with `for await` or pipe `readable`.
1999
- */
2000
- declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
2001
- interface DevframeNodeRpcSession {
2002
- meta: DevframeNodeRpcSessionMeta;
2003
- rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
2004
- }
2005
- interface RpcBroadcastOptions<METHOD, Args extends any[]> {
2006
- method: METHOD;
2007
- args: Args;
2008
- optional?: boolean;
2009
- event?: boolean;
2010
- filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
2011
- }
2012
- type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1513
+ websocket?: number | string | ConnectionMetaWebsocket;
2013
1514
  /**
2014
- * Invoke a locally registered server RPC function directly.
1515
+ * SSE endpoint (`GET` opens the event stream, `POST` carries RPC calls),
1516
+ * resolved by the client into an `http(s)://` URL with the same
1517
+ * proxy-safe rules as {@link ConnectionMeta.websocket}:
2015
1518
  *
2016
- * This bypasses transport and is useful for server-side cross-function calls.
1519
+ * - {@link ConnectionMetaSse}: the proxy-flexible default; a
1520
+ * same-origin path relative to `__connection.json`.
1521
+ * - `string`: a full `http(s)://` URL used verbatim, or a path
1522
+ * resolved same-origin.
2017
1523
  */
2018
- invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1524
+ sse?: string | ConnectionMetaSse;
2019
1525
  /**
2020
- * Broadcast a message to all connected clients
1526
+ * Present when the dev server exposes a route-based MCP endpoint
1527
+ * (the host's `mcp` setting). Advertises the MCP Streamable-HTTP route so in-browser
1528
+ * tooling (e.g. an MCP inspector) can discover it without guessing the
1529
+ * path. `path` is relative to `__connection.json`'s location, like the
1530
+ * WebSocket `path`. `port` is set when the endpoint lives on a side-car
1531
+ * server on its own port (bridge mode: `devframeViteBridge`,
1532
+ * `@devframes/next`): the client combines the page hostname with `port`
1533
+ * and resolves `path` against that origin, mirroring
1534
+ * {@link ConnectionMetaWebsocket.port}.
2021
1535
  */
2022
- broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1536
+ mcp?: {
1537
+ path: string;
1538
+ port?: number;
1539
+ };
2023
1540
  /**
2024
- * Get the current RPC client
2025
- *
2026
- * Available in RPC functions to get the current RPC client
1541
+ * Names of RPC functions that have declared `jsonSerializable: true`.
1542
+ * Used by the WS / static client to dispatch the per-call wire
1543
+ * serializer (strict JSON for these methods, structured-clone for
1544
+ * the rest). Populated by the server / build adapter; absent on
1545
+ * legacy clients, in which case all outgoing messages fall back to
1546
+ * structured-clone.
2027
1547
  */
2028
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1548
+ jsonSerializableMethods?: string[];
2029
1549
  /**
2030
- * The shared state host
1550
+ * URL of the `__connection.json` that owns this meta's relative paths.
1551
+ * Two producers write it:
1552
+ *
1553
+ * - The client annotates it (absolute) when it publishes the meta on a
1554
+ * shared window for same-origin inheritance: a relative
1555
+ * `websocket.path` resolves against this, so a child SPA mounted at
1556
+ * another base (e.g. a hub mounting several devframes at `/__foo/`,
1557
+ * `/__bar/`, …) inherits a dialable endpoint rather than resolving the
1558
+ * path against its own mount.
1559
+ * - A static hub build serves it (base-absolute, e.g.
1560
+ * `/__devframes/__connection.json`) in each **per-frame** meta, so a
1561
+ * frame SPA that fetched its own copy still resolves the shared RPC
1562
+ * dump from the hub base. Resolved against the fetched URL.
2031
1563
  */
2032
- sharedState: RpcSharedStateHost;
1564
+ baseUrl?: string;
2033
1565
  /**
2034
- * The streaming channel host. Provides per-channel `start()` /
2035
- * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1566
+ * A pre-issued bearer token embedded in the meta so a client trusts the
1567
+ * server on connect without any prompt or `localStorage` lookup. Only a
1568
+ * **hub** serving a **per-frame** connection meta populates this; it
1569
+ * authenticates once at the top level and bakes the resulting token into
1570
+ * the meta each plugin iframe fetches, so a cross-origin frame (which
1571
+ * cannot read the hub's `localStorage`) is still pre-authorized. The
1572
+ * standalone `__connection.json` never carries a token.
2036
1573
  *
2037
- * @see RpcStreamingHost
1574
+ * > [!WARNING]
1575
+ * > A token in a fetchable JSON is only as protected as the URL serving
1576
+ * > it. Emit it exclusively from hub-controlled, per-frame meta, never
1577
+ * > from a publicly reachable static `__connection.json`.
2038
1578
  */
2039
- streaming: RpcStreamingHost;
2040
- };
2041
- interface RpcSharedStateGetOptions<T> {
2042
- sharedState?: SharedState<T>;
2043
- initialValue?: T;
2044
- }
2045
- interface RpcSharedStateHost {
2046
- get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
2047
- keys: () => string[];
1579
+ authToken?: string;
2048
1580
  /**
2049
- * Subscribe to new shared-state keys becoming available. Fires when
2050
- * `get(key, ...)` creates a fresh entry (not on subsequent gets).
2051
- * Useful for protocol adapters (e.g. MCP) that surface shared state
2052
- * as dynamic resources.
1581
+ * Session-scoped token that lets a trusted external viewer register its
1582
+ * browser origin before opening the WebSocket. The host includes it in the
1583
+ * connection metadata, which the viewer obtains through the host page.
1584
+ *
1585
+ * Treat this as a bearer credential. Keep connection metadata containing the
1586
+ * token same-origin until the requesting origin has been verified.
2053
1587
  */
2054
- onKeyAdded: (fn: (key: string) => void) => () => void;
1588
+ viewerOriginToken?: string;
2055
1589
  /**
2056
- * Unregister a shared state and drop its broadcast listeners. Returns
2057
- * `true` when a state was removed, `false` when the key was unknown.
2058
- * Used by short-lived states (e.g. a disposed JSON-render view) to avoid
2059
- * leaking listeners and lingering entries for the context lifetime.
1590
+ * Static, host-declared configuration, baked in once at connect time and
1591
+ * fixed for the life of the server (e.g. a hub's UI rebrand, or its
1592
+ * aggregated dock-bar layout preferences). Read-only from the browser: a
1593
+ * client only ever reads `rpc.connectionMeta.configs`, never writes to it.
1594
+ *
1595
+ * Contrast this with {@link DevframeSettingsRegistry} (`ctx.scope(ns).settings`)
1596
+ * and a hub's `devframe:user-settings` shared-state key; both are
1597
+ * mutable, user-editable, and synced bidirectionally over RPC for the
1598
+ * life of the session. `configs` is the opposite: one-way, immutable,
1599
+ * decided by whoever assembled the server.
1600
+ *
1601
+ * Each key is owned by one package, contributed via declaration merging:
1602
+ *
1603
+ * ```ts
1604
+ * declare module 'devframe/types' {
1605
+ * interface DevframeConnectionConfigsRegistry {
1606
+ * 'my-key': { some: 'shape' }
1607
+ * }
1608
+ * }
1609
+ * ```
2060
1610
  */
2061
- delete: (key: string) => boolean;
1611
+ configs?: Partial<DevframeConnectionConfigsRegistry>;
2062
1612
  }
2063
1613
  /**
2064
- * Options for `RpcStreamingHost.create()`.
1614
+ * Augmentation point for {@link ConnectionMeta.configs}. Empty by default;
1615
+ * a package that wants to publish static, boot-time config through the
1616
+ * connection handshake augments this interface with its own key (see
1617
+ * {@link ConnectionMeta.configs} for the pattern). `@devframes/hub`
1618
+ * augments it with `dock`; `@devframes/hub-ui` augments it with `ui`.
2065
1619
  */
2066
- interface RpcStreamingChannelOptions {
2067
- /**
2068
- * Size of the per-stream ring buffer kept on the server for
2069
- * replay-on-resubscribe. `0` (default) disables replay; on reconnect
2070
- * the consumer only sees chunks that arrive after subscribing.
2071
- *
2072
- * The buffer is per stream id, not per channel; each `channel.start()`
2073
- * gets its own.
1620
+ interface DevframeConnectionConfigsRegistry {}
1621
+ /**
1622
+ * Schema map for typed CLI flags. Keys are flag names in camelCase, so
1623
+ * this matches CAC's parsed-flag output ( `--no-open` → `noOpen` ). Each
1624
+ * value is any [Standard Schema](https://standardschema.dev/) validator
1625
+ * (valibot, zod, arktype, devframe's built-in `s`, …) used to both (a)
1626
+ * derive the CAC option type when the flag is registered and (b) validate
1627
+ * the parsed value before it's forwarded to `setup(ctx, { flags })`.
1628
+ */
1629
+ type CliFlagsSchema = Record<string, StandardSchemaV1>;
1630
+ /**
1631
+ * Identity helper that preserves the literal schema-map type; use this
1632
+ * so `InferCliFlags<typeof myFlags>` resolves to the right object shape.
1633
+ *
1634
+ * ```ts
1635
+ * const appFlags = defineCliFlags({
1636
+ * depth: v.pipe(v.number(), v.integer()),
1637
+ * config: v.optional(v.string()),
1638
+ * })
1639
+ *
1640
+ * defineDevframe({
1641
+ * cli: { flags: appFlags },
1642
+ * setup(ctx, info) {
1643
+ * const flags = info.flags as InferCliFlags<typeof appFlags>
1644
+ * flags.depth // number
1645
+ * flags.config // string | undefined
1646
+ * },
1647
+ * })
1648
+ * ```
1649
+ */
1650
+ declare function defineCliFlags<T extends CliFlagsSchema>(flags: T): T;
1651
+ /** Extract the parsed-output type from a {@link CliFlagsSchema}. */
1652
+ type InferCliFlags<T extends CliFlagsSchema> = { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]>; };
1653
+ /** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
1654
+ declare function parseCliFlags(schema: CliFlagsSchema, raw: Record<string, unknown>): {
1655
+ flags: Record<string, unknown>;
1656
+ issues?: string[];
1657
+ };
1658
+ /**
1659
+ * A ready-made pre-auth RPC handler, as produced by
1660
+ * `devframe/recipes/interactive-auth`'s `createInteractiveAuth`. Bundles
1661
+ * everything a host adapter needs to wire an authenticated server:
1662
+ * the handshake RPC functions, the resolver gate, the connect-time trust
1663
+ * hook, and the startup banner.
1664
+ *
1665
+ * `initDevframe` / `initHub` accept one of these directly via their `auth` option
1666
+ * (see {@link https://devfra.me | devframe}'s server docs), or a host can
1667
+ * wire the four pieces itself against a lower-level transport.
1668
+ */
1669
+ interface DevframeAuthHandler {
1670
+ /**
1671
+ * `anonymous:devframe:auth` + `anonymous:devframe:auth:exchange` (the
1672
+ * handshake), `anonymous:devframe:auth:request-code` (client-requested
1673
+ * banner print), and `devframe:auth:revoke` (self-revoke); register these
1674
+ * on the RPC host (e.g. `rpcHost.register(fn)` for each).
2074
1675
  */
2075
- replayWindow?: number;
1676
+ rpcFunctions: RpcFunctionDefinitionAny[];
2076
1677
  /**
2077
- * Milliseconds a closed stream is retained on the server after its
2078
- * last subscriber leaves (or if no subscriber ever arrived). During
2079
- * this window, late subscribers can still join and replay the buffer
2080
- * + receive the `end` frame.
1678
+ * Resolver gate: whether `methodName` is callable given `session`'s
1679
+ * current trust state. Defaults to allowing any `anonymous:`-prefixed
1680
+ * method (see `isAnonymousRpcMethod`) plus anything once the session is
1681
+ * trusted.
1682
+ */
1683
+ authorize: (methodName: string, session: DevframeNodeRpcSession) => boolean;
1684
+ /**
1685
+ * Connect-time trust: reads a bearer token off the connection's initial
1686
+ * request (a static/pre-shared token from `clientAuthTokens`, or a token
1687
+ * minted by the code exchange) and, when valid, marks the session
1688
+ * trusted immediately, before the client's own handshake call.
1689
+ */
1690
+ onConnect: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void;
1691
+ /**
1692
+ * Print the current one-time code and its magic-link URL. An untrusted
1693
+ * browser client triggers this itself over
1694
+ * `anonymous:devframe:auth:request-code`; call it directly when the host
1695
+ * wants the code on screen without waiting for a client. Safe to call
1696
+ * repeatedly; it only prints once per code.
1697
+ */
1698
+ printBanner: () => void;
1699
+ /**
1700
+ * Rewrite a URL the CLI is about to open in the browser (`--open`) so the
1701
+ * tab lands already authenticated, e.g. appending the current OTP as a
1702
+ * query param. Called with the fully-resolved target URL; return it
1703
+ * unchanged to opt out. Optional: a handler that doesn't need this (e.g.
1704
+ * one gated by a pre-shared bearer token instead of an interactive code)
1705
+ * can omit it and the URL opens as-is.
1706
+ */
1707
+ buildOpenUrl?: (url: string) => string;
1708
+ }
1709
+ /**
1710
+ * The MCP adapter surface, typed here so `devframe` (which probes for and
1711
+ * lazily loads `@devframes/agentic`) and `@devframes/agentic/mcp` (which
1712
+ * implements it) share one contract without a type-level dependency cycle.
1713
+ * None of these shapes reference the MCP SDK: the SDK is an implementation
1714
+ * detail of `@devframes/agentic`, freely swappable behind this surface.
1715
+ * The h3-bound `mountMcpHttp` shapes live in `node/agentic.ts` (exported via
1716
+ * `devframe/internal`) instead: `devframe/types` must stay lib-neutral, and
1717
+ * h3's declarations are not.
1718
+ */
1719
+ interface CreateMcpServerOptions {
1720
+ /**
1721
+ * Transport to use. `createMcpServer` itself runs `'stdio'` (a standalone
1722
+ * process with its own host context); the Streamable-HTTP transport is
1723
+ * served route-based by the dev server instead; see `mountMcpHttp` and
1724
+ * the `mcp` option on `createDevServer` / `createCac`'s `--mcp` flag.
1725
+ */
1726
+ transport?: 'stdio';
1727
+ /**
1728
+ * Expose shared-state keys as MCP resources.
1729
+ * - `true` (default): every key the host publishes
1730
+ * - `false`: none
1731
+ * - `(key) => boolean`: filter
1732
+ */
1733
+ exposeSharedState?: boolean | ((key: string) => boolean);
1734
+ /**
1735
+ * Usage guidance sent as `instructions` in the MCP `initialize` (and
1736
+ * `server/discover`) result. Omitted or empty sends no instructions.
1737
+ */
1738
+ instructions?: string;
1739
+ /**
1740
+ * Resource templates served through `resources/templates/list`, and read
1741
+ * through `resources/read`. Omitted or empty keeps the resources unchanged.
1742
+ */
1743
+ resourceTemplates?: McpResourceTemplate[];
1744
+ /**
1745
+ * Host subscription for resource-list changes; each `notify()` sends
1746
+ * `notifications/resources/list_changed`. See {@link McpResourcesChangedHook}.
1747
+ */
1748
+ onResourcesChanged?: McpResourcesChangedHook;
1749
+ /**
1750
+ * Prompt templates served through `prompts/list` and `prompts/get`.
1751
+ * Omitted or empty declares no `prompts` capability.
1752
+ */
1753
+ prompts?: McpPrompt[];
1754
+ /** Override the name reported in the MCP handshake. */
1755
+ serverName?: string;
1756
+ /** Override the version reported in the MCP handshake. Defaults to `definition.version ?? '0.0.0'`. */
1757
+ serverVersion?: string;
1758
+ /** Called once the transport is connected. */
1759
+ onReady?: (info: {
1760
+ transport: 'stdio';
1761
+ }) => void;
1762
+ }
1763
+ /**
1764
+ * One MCP content block in a tool result. Attach blocks to a tool's return
1765
+ * value with `withMcpContent` (from `devframe`) to send them in place of the
1766
+ * default JSON text block. `data` is base64.
1767
+ */
1768
+ type McpContentBlock = {
1769
+ type: 'text';
1770
+ text: string;
1771
+ } | {
1772
+ type: 'image';
1773
+ data: string;
1774
+ mimeType: string;
1775
+ } | {
1776
+ type: 'audio';
1777
+ data: string;
1778
+ mimeType: string;
1779
+ };
1780
+ interface McpServerHandle {
1781
+ stop: () => Promise<void>;
1782
+ }
1783
+ interface CreateMcpFetchHandlerOptions {
1784
+ /** Name reported in the MCP handshake. */
1785
+ serverName: string;
1786
+ /** Version reported in the MCP handshake. */
1787
+ serverVersion: string;
1788
+ /** Expose shared-state keys as MCP resources; see {@link CreateMcpServerOptions.exposeSharedState}. */
1789
+ exposeSharedState: boolean | ((key: string) => boolean);
1790
+ /** Usage guidance for the handshake; see {@link CreateMcpServerOptions.instructions}. */
1791
+ instructions?: string;
1792
+ /** Resource templates; see {@link CreateMcpServerOptions.resourceTemplates}. */
1793
+ resourceTemplates?: McpResourceTemplate[];
1794
+ /** Host subscription for resource-list changes; see {@link CreateMcpServerOptions.onResourcesChanged}. */
1795
+ onResourcesChanged?: McpResourcesChangedHook;
1796
+ /** Prompt templates; see {@link CreateMcpServerOptions.prompts}. */
1797
+ prompts?: McpPrompt[];
1798
+ /**
1799
+ * Optional identity check, layered on top of the origin gate and checked
1800
+ * **after** it: a bearer token string (matched in constant time against
1801
+ * `Authorization: Bearer <token>`), a `(request) => boolean` callback, or
1802
+ * `false` (the default) for origin-only, trusting same-machine callers. A
1803
+ * callback governs identity only and cannot relax the origin gate. See
1804
+ * {@link McpAuthorization}.
1805
+ */
1806
+ authorization?: McpAuthorization;
1807
+ /**
1808
+ * Origin allow-list beyond the loopback default. `false` disables the
1809
+ * origin gate entirely. Default: loopback-only.
2081
1810
  *
2082
- * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
2083
- * (immediate free). Set to `0` to opt out, or higher for longer
2084
- * post-mortem replay.
1811
+ * Unlike the WS transport, the MCP route does **not** allow `Origin`-less
1812
+ * requests: a route-based endpoint is reachable by any local process, so a
1813
+ * request must carry an `Origin` that passes the gate. Native clients
1814
+ * (e.g. `devframe connect`) send their loopback origin explicitly.
2085
1815
  */
2086
- closedStreamRetention?: number;
1816
+ allowedOrigins?: readonly string[] | false;
1817
+ }
1818
+ /** Connection facts a host knows about a request beyond the `Request` itself. */
1819
+ interface McpConnectionInfo {
1820
+ /**
1821
+ * The connecting peer's remote address (a node socket's `remoteAddress`),
1822
+ * used to prove a same-machine caller when the endpoint relies on the
1823
+ * loopback origin default with no identity check. A host that can resolve a
1824
+ * trustworthy peer address (the h3/node mount) supplies it; when it's
1825
+ * omitted the origin gate stays the only locality signal.
1826
+ */
1827
+ remoteAddress?: string;
1828
+ }
1829
+ interface McpFetchHandler {
1830
+ /**
1831
+ * WHATWG-`fetch` handler for the MCP endpoint. Hand every method
1832
+ * (POST/GET/DELETE) on the endpoint's path to it; routing by path is the
1833
+ * host's job. Pass {@link McpConnectionInfo} when the host can resolve the
1834
+ * peer address so the default trust boundary can enforce same-machine
1835
+ * locality.
1836
+ */
1837
+ fetch: (request: Request, connection?: McpConnectionInfo) => Promise<Response>;
1838
+ /** Tear down the handler (aborts in-flight exchanges, drops the change bridge). */
1839
+ dispose: () => Promise<void>;
1840
+ }
1841
+ /** One concrete resource that an {@link McpResourceTemplate} lists in `resources/list`. */
1842
+ interface McpResourceTemplateEntry {
1843
+ /** The resource URI. It SHOULD match the `uriTemplate` of its template, so `resources/read` finds it. */
1844
+ uri: string;
1845
+ name: string;
1846
+ /** Human-readable name for display. */
1847
+ title?: string;
1848
+ description?: string;
1849
+ /** Defaults to the `mimeType` of the template. */
1850
+ mimeType?: string;
2087
1851
  }
2088
1852
  /**
2089
- * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
2090
- * channel owns a wire namespace; calling `start()` produces individual
2091
- * streams keyed by id.
1853
+ * What an {@link McpResourceTemplate} returns from `read`, sent as the
1854
+ * `resources/read` result: `text` as is, or else `json` as JSON text.
1855
+ */
1856
+ interface McpResourceTemplateContent {
1857
+ text?: string;
1858
+ json?: unknown;
1859
+ /** Defaults to the `mimeType` of the template, then `application/json`. */
1860
+ mimeType?: string;
1861
+ }
1862
+ /**
1863
+ * A resource template the MCP server lists in `resources/templates/list` and
1864
+ * reads in `resources/read`. The `uriTemplate` uses RFC 6570 level 1 only:
1865
+ * plain `{name}` expressions, with text between any two of them. Each
1866
+ * expression matches a non-empty value with no `/`, and its value is
1867
+ * percent-decoded.
2092
1868
  *
2093
- * @see {@link https://devfra.me/guide/streaming Streaming guide}
1869
+ * `list` and `read` get the node context of the devframe (or of the hub), so
1870
+ * they can read live state such as `ctx.rpc.sharedState` or `ctx.agent`. The
1871
+ * route options are resolved before `setup()` runs, so a template cannot
1872
+ * capture that context in a closure.
2094
1873
  */
2095
- interface RpcStreamingChannel<T = unknown> {
2096
- /** Channel name as registered with `ctx.rpc.streaming.create()`. */
2097
- readonly name: string;
1874
+ interface McpResourceTemplate {
1875
+ /** RFC 6570 level 1 URI template, e.g. `my-tool://file/{path}`. Anything else throws `DF0084`. */
1876
+ uriTemplate: string;
1877
+ name: string;
1878
+ /** Human-readable name for display. */
1879
+ title?: string;
1880
+ description?: string;
1881
+ /** MIME type of every resource the template stands for. */
1882
+ mimeType?: string;
1883
+ /** The concrete resources the template stands for right now. They are added to `resources/list`. */
1884
+ list?: (ctx: DevframeNodeContext) => McpResourceTemplateEntry[] | Promise<McpResourceTemplateEntry[]>;
1885
+ /** Read one resource: `params` holds the decoded value of each `{name}`, `uri` the requested URI. */
1886
+ read: (params: Record<string, string>, uri: string, ctx: DevframeNodeContext) => McpResourceTemplateContent | Promise<McpResourceTemplateContent>;
1887
+ }
1888
+ /**
1889
+ * A host subscription for resource-list changes. Call `notify()` each time
1890
+ * the resources change; the MCP server then sends
1891
+ * `notifications/resources/list_changed`. Return a function that removes the
1892
+ * subscription; the MCP server calls it on teardown.
1893
+ */
1894
+ type McpResourcesChangedHook = (notify: () => void, ctx: DevframeNodeContext) => () => void;
1895
+ /** One argument of an {@link McpPrompt}. */
1896
+ interface McpPromptArgument {
1897
+ name: string;
1898
+ description?: string;
1899
+ /** `prompts/get` answers invalid params when a required argument is missing or empty. */
1900
+ required?: boolean;
1901
+ }
1902
+ /** One message of the answer to `prompts/get`. */
1903
+ interface McpPromptMessage {
1904
+ role: 'user' | 'assistant';
1905
+ content: {
1906
+ type: 'text';
1907
+ text: string;
1908
+ };
1909
+ }
1910
+ /** What an {@link McpPrompt} returns from `get`, sent as the `prompts/get` result. */
1911
+ interface McpPromptResult {
1912
+ description?: string;
1913
+ messages: McpPromptMessage[];
1914
+ }
1915
+ /**
1916
+ * A prompt template the MCP server lists in `prompts/list` and builds in
1917
+ * `prompts/get`. A coding agent usually offers it to the user as a slash
1918
+ * command. Each `name` is used once; a repeated name throws `DF0085`.
1919
+ */
1920
+ interface McpPrompt {
1921
+ name: string;
1922
+ /** Human-readable name for display. */
1923
+ title?: string;
1924
+ description?: string;
1925
+ arguments?: McpPromptArgument[];
1926
+ /** Build the messages from the arguments the coding agent sent. */
1927
+ get: (args: Record<string, string>) => McpPromptResult | Promise<McpPromptResult>;
1928
+ }
1929
+ /**
1930
+ * Classification of how a devframe is being deployed. Hosted adapters
1931
+ * (`vite`, `embedded`) share their origin with a host app and must
1932
+ * namespace their mount path under `/__<id>/`. Standalone adapters
1933
+ * (`cli`, `build`) own the origin and default to `/`.
1934
+ */
1935
+ type DevframeDeploymentKind = 'standalone' | 'hosted';
1936
+ /**
1937
+ * How a hub deduplicates devframes that share an `id` when more than one
1938
+ * is mounted onto the same hub. See {@link DevframeDefinition.duplicationStrategy}.
1939
+ *
1940
+ * - `'warn'` (default): keep the first registration, drop later
1941
+ * duplicates, and emit a warning diagnostic (`DF8105`).
1942
+ * - `'silent'`: drop later duplicates without warning.
1943
+ * - `'throw'`: throw when a duplicate is mounted.
1944
+ * - `'duplicate'`: let every instance coexist under a disambiguated
1945
+ * dock id.
1946
+ */
1947
+ type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate';
1948
+ /**
1949
+ * Controls where the browser opens the RPC WebSocket, advertised in
1950
+ * `__connection.json` and used to bind the dev server. The three shapes map
1951
+ * to the three connection scenarios; precedence is `url` > `port` > `route`:
1952
+ *
1953
+ * 1. **Same server, different route** (default): leave `port`/`url` unset.
1954
+ * The socket shares the HTTP server's port and binds to `route`
1955
+ * (`__ws`). The client connects to its own origin, so the link
1956
+ * survives a reverse proxy that rewrites the host/port/subpath.
1957
+ *
1958
+ * 2. **Different port**: set `port`. The socket binds on its own port on the
1959
+ * same host; the client targets `ws(s)://<page-host>:<port>/<route>`.
1960
+ *
1961
+ * 3. **Remote, different origin**: set `url` to a full `ws://`/`wss://`
1962
+ * endpoint (e.g. a public tunnel or relay). The client uses it verbatim.
1963
+ */
1964
+ interface DevframeWsOptions {
2098
1965
  /**
2099
- * Start a new stream. Returns a server-side sink with both an imperative
2100
- * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
2101
- * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
2102
- * disconnects or cancels.
1966
+ * Upgrade route segment the socket binds to and is advertised at, relative
1967
+ * to the SPA base. Default: `__ws`.
2103
1968
  */
2104
- start: (opts?: {
2105
- id?: string;
2106
- }) => StreamSink<T>;
1969
+ route?: string;
1970
+ /**
1971
+ * Bind the socket on its own port instead of sharing the HTTP port. The
1972
+ * browser connects to this port on the page's hostname. Implies
1973
+ * {@link DevframeWsOptions.sidecar}.
1974
+ */
1975
+ port?: number;
1976
+ /**
1977
+ * Start a side-car WebSocket server on a free port, for hosts whose request
1978
+ * handlers can't accept upgrades (Next.js route handlers, Nitro, Rsbuild).
1979
+ * The resolved port is advertised in `__connection.json`, so the browser
1980
+ * finds it without any further wiring. Set `port` instead to pin it.
1981
+ */
1982
+ sidecar?: boolean;
1983
+ /**
1984
+ * Advertise a fixed, fully-qualified endpoint on another origin (a full
1985
+ * `ws://`/`wss://` URL). Takes precedence over `port`/`route` in the meta.
1986
+ */
1987
+ url?: string;
1988
+ }
1989
+ /**
1990
+ * Controls the SSE RPC endpoint, the WebSocket-free transport for hosts
1991
+ * and proxies where the upgrade isn't available. It rides whatever HTTP
1992
+ * surface serves the instance (the same one serving `__connection.json`),
1993
+ * so a relative `route` always resolves; there is no port plumbing of its
1994
+ * own. Enabled by default alongside the WebSocket; pass `sse: false` to
1995
+ * disable, or `ws: false` to run SSE-only (`backend: 'sse'`).
1996
+ */
1997
+ interface DevframeSseOptions {
1998
+ /**
1999
+ * Route segment the SSE endpoint binds to and is advertised at, relative
2000
+ * to the SPA base. Default: `__sse`.
2001
+ */
2002
+ route?: string;
2003
+ }
2004
+ /**
2005
+ * An **optional** identity check layered on top of the origin gate. The
2006
+ * route-based MCP endpoint trusts same-machine callers by default (the
2007
+ * loopback origin gate is enough), so this is opt-in hardening for the cases
2008
+ * where a same-machine process is not a trust boundary, like a LAN/tunnel
2009
+ * origin, a shared/CI box, or a destructive tool surface:
2010
+ *
2011
+ * - a non-empty **bearer token string**: the request must carry
2012
+ * `Authorization: Bearer <token>`, compared in constant time. Back it with
2013
+ * an environment variable rather than a literal;
2014
+ * - a **callback** `(request) => boolean | Promise<boolean>`: a custom
2015
+ * identity check (validate a signed header, an mTLS-derived claim, …). It
2016
+ * only governs identity and cannot relax the origin gate;
2017
+ * - `false`, the default: **origin-only**, trusting same-machine callers.
2018
+ */
2019
+ type McpAuthorization = string | ((request: Request) => boolean | Promise<boolean>) | false;
2020
+ /**
2021
+ * The route-based MCP setting accepted everywhere a host mounts a devframe
2022
+ * (`initDevframe` / `initHub` / `createDevServer` options, `createCac` /
2023
+ * `--mcp`, the framework kits):
2024
+ *
2025
+ * - `'auto'`, the default: mount the route when the devframe exposes an
2026
+ * agent surface (an `agent`-flagged RPC function, or a tool / resource /
2027
+ * provider registered on `ctx.agent`) AND the optional `@devframes/agentic`
2028
+ * peer (the MCP adapter and SDK) is installed. An empty agent surface
2029
+ * mounts nothing and loads no MCP code; a non-empty one without the peer
2030
+ * warns once (DF0078) and mounts nothing.
2031
+ * - `true`: always mount at the default `__mcp` route; a missing
2032
+ * `@devframes/agentic` throws DF0079.
2033
+ * - `false`: never mount, and never probe or warn.
2034
+ * - {@link McpRouteOptions}: always mount (like `true`), with a custom route
2035
+ * path, origin allow-list, or {@link McpAuthorization} identity check.
2036
+ *
2037
+ * A mounted route trusts same-machine callers by default (the loopback
2038
+ * origin gate), exactly like `mcp: true`.
2039
+ */
2040
+ type McpSetting = boolean | 'auto' | McpRouteOptions;
2041
+ /**
2042
+ * Configuration for the route-based MCP server mounted alongside the dev
2043
+ * server (via {@link DevframeCliOptions.mcp}). The endpoint speaks
2044
+ * the MCP Streamable-HTTP transport over the same origin as the SPA,
2045
+ * exposing the definition's `ctx.agent` tools + shared-state resources to
2046
+ * external MCP clients connected to the *running* server.
2047
+ */
2048
+ interface McpRouteOptions {
2049
+ /**
2050
+ * Route segment the MCP endpoint binds to, relative to the SPA base.
2051
+ * Default: `__mcp` (i.e. `/__mcp` standalone, `/__<id>/__mcp` hosted).
2052
+ */
2053
+ path?: string;
2054
+ /**
2055
+ * Optional identity check, layered on top of the origin gate and checked
2056
+ * **after** it. Defaults to origin-only (`false`): the route trusts
2057
+ * same-machine callers, since the loopback origin gate already keeps
2058
+ * arbitrary remote/browser callers out. Set a bearer token or a callback to
2059
+ * harden the route when a same-machine process is not your trust boundary
2060
+ * (LAN/tunnel origin, shared/CI host, destructive tools). See
2061
+ * {@link McpAuthorization}.
2062
+ */
2063
+ authorization?: McpAuthorization;
2064
+ /**
2065
+ * Extra `Origin` header values to accept beyond the loopback default
2066
+ * (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less native client).
2067
+ * Add your LAN/tunnel origin here when reaching the endpoint from another
2068
+ * host, mirroring the WS transport's origin gate. Pass `false` to disable
2069
+ * origin checking entirely (not recommended). Default: loopback-only.
2070
+ *
2071
+ * This is the endpoint's DNS-rebinding protection: the shared
2072
+ * `isAllowedOrigin` gate the WS upgrade already uses, applied as external
2073
+ * middleware (the approach the MCP SDK now recommends over its own
2074
+ * deprecated `allowedHosts`/`allowedOrigins` transport flags). When you
2075
+ * widen it past loopback, layer on {@link McpRouteOptions.authorization} to
2076
+ * prove identity too.
2077
+ */
2078
+ allowedOrigins?: readonly string[] | false;
2079
+ /**
2080
+ * Which shared-state keys the route exposes to agents, through the built-in
2081
+ * `devframe_state_read` tool and the `devframe://state/<key>` resources.
2082
+ * - `true` (default): every key the host publishes
2083
+ * - `false`: none
2084
+ * - `(key) => boolean`: only the keys the filter accepts
2085
+ *
2086
+ * The states themselves stay available to the UI over RPC.
2087
+ */
2088
+ exposeSharedState?: boolean | ((key: string) => boolean);
2089
+ /**
2090
+ * Usage guidance for the route, sent as `instructions` in the MCP
2091
+ * `initialize` (and `server/discover`) result. An MCP client can add it to
2092
+ * the coding agent's context, for example to say which tool to call first.
2093
+ * Omitted or empty sends no instructions.
2094
+ */
2095
+ instructions?: string;
2096
+ /**
2097
+ * Resource templates the route serves through MCP
2098
+ * `resources/templates/list`. `resources/read` tries them after the
2099
+ * registered resources and the shared-state resources (a
2100
+ * `devframe://resource/` or `devframe://state/` URI never reaches them);
2101
+ * the first template whose `uriTemplate` matches the URI reads it. `resources/list` adds the
2102
+ * entries each template's `list` returns. Omitted or empty keeps the
2103
+ * resources unchanged.
2104
+ */
2105
+ resourceTemplates?: McpResourceTemplate[];
2107
2106
  /**
2108
- * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
2109
- * The pipe uses `sink.signal` so cancellation propagates upstream.
2110
- *
2111
- * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
2112
- * before passing it here.
2107
+ * Host subscription for resource-list changes. Call `notify()` each time
2108
+ * the resources change (for example, the entries of a resource template);
2109
+ * the route then sends `notifications/resources/list_changed`. Return a
2110
+ * function that removes the subscription; the route calls it on teardown.
2113
2111
  */
2114
- pipeFrom: (readable: ReadableStream<T>, opts?: {
2115
- id?: string;
2116
- }) => Promise<StreamSink<T>>;
2117
- /** Look up an active stream by id. Returns `undefined` if none. */
2118
- get: (id: string) => StreamSink<T> | undefined;
2119
- /** All active outbound stream ids on this channel. */
2120
- ids: () => string[];
2112
+ onResourcesChanged?: McpResourcesChangedHook;
2121
2113
  /**
2122
- * Open an inbound stream: the server side of a client-to-server
2123
- * upload. Allocates an id, returns a `StreamReader<T>` that fills as
2124
- * the client writes chunks. Typical pattern is to call this from an
2125
- * action handler, kick off background processing, and return the id
2126
- * so the caller can start uploading:
2127
- *
2128
- * ```ts
2129
- * handler: async () => {
2130
- * const reader = channel.openInbound()
2131
- * ;(async () => {
2132
- * for await (const chunk of reader) processChunk(chunk)
2133
- * })()
2134
- * return { uploadId: reader.id }
2135
- * }
2136
- * ```
2137
- *
2138
- * Calling `reader.cancel()` on the server sends an `upload-cancel` to
2139
- * the uploading client, which aborts its sink.
2114
+ * Prompt templates the route serves through MCP `prompts/list` and
2115
+ * `prompts/get`. Omitted or empty declares no `prompts` capability.
2140
2116
  */
2141
- openInbound: (opts?: {
2142
- id?: string;
2143
- }) => StreamReader<T>;
2117
+ prompts?: McpPrompt[];
2144
2118
  }
2145
- /**
2146
- * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
2147
- * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
2148
- * and wire namespace.
2149
- */
2150
- interface RpcStreamingHost {
2119
+ interface DevframeCliOptions {
2120
+ /** Binary name; default: the devframe's `id`. */
2121
+ command?: string;
2122
+ /** Preferred port for the dev server (default 9999). */
2123
+ port?: number;
2124
+ /** Port scan range, forwarded to `get-port-please`. */
2125
+ portRange?: [number, number];
2126
+ /** Prefer a random open port. */
2127
+ random?: boolean;
2128
+ /** Default host to bind to; `--host` overrides. */
2129
+ host?: string;
2151
2130
  /**
2152
- * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
2153
- * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
2154
- * name is already taken.
2131
+ * Auto-open the browser when the dev server starts.
2132
+ * `true` opens the resolved origin; a string opens that relative path.
2133
+ * The `--open` / `--no-open` flags override this.
2155
2134
  */
2156
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
2135
+ open?: boolean | string;
2157
2136
  /**
2158
- * Adapters call this when a session disconnects so the host can drop
2159
- * subscribers and abort orphaned streams. Most users do not need this;
2160
- * it's wired by the RPC server binding automatically.
2137
+ * Authentication for the standalone dev server.
2161
2138
  *
2162
- * @internal
2163
- */
2164
- _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
2165
- }
2166
- interface DevframeCapabilities {
2167
- rpc?: boolean;
2168
- views?: boolean;
2169
- }
2170
- /**
2171
- * Framework- and build-tool-agnostic node context: RPC + diagnostics +
2172
- * agent + the view-host (HTTP file-serving). Host adapters can wrap this
2173
- * to add their own surfaces; for example, `@vitejs/devtools-kit`'s
2174
- * `createKitContext` adds `docks`, `terminals`, `messages`, and
2175
- * `commands` when mounted into Vite DevTools. JSON rendering is an opt-in
2176
- * integration (`@devframes/json-render`) layered on top, not part of this
2177
- * core surface.
2178
- */
2179
- interface DevframeNodeContext {
2180
- readonly workspaceRoot: string;
2181
- readonly cwd: string;
2182
- /**
2183
- * Lifecycle distinction surfaced to plugin authors:
2139
+ * - `undefined` / `true`: the standalone adapters (`cli` /
2140
+ * served `build`) auto-wire devframe's interactive OTP auth
2141
+ * (`createInteractiveAuth`): an untrusted client can only reach
2142
+ * `anonymous:` methods until it exchanges the printed one-time code.
2143
+ * The adapter prints the code + magic-link banner once the server is
2144
+ * listening.
2145
+ * - `false`: no gate, for trusted single-user localhost tools where an
2146
+ * auth round-trip only gets in the way (the built-in plugins set this).
2147
+ * The `--no-auth` CLI flag maps here for one-off runs.
2148
+ * - A {@link DevframeAuthHandler}: a custom handler (e.g. a tuned
2149
+ * `createInteractiveAuth`, or an entirely different scheme) passed
2150
+ * straight through to the RPC transport binding.
2184
2151
  *
2185
- * - `'dev'`: long-running, interactive session. Connections come and
2186
- * go; broadcasts and shared-state mutations are debounced
2187
- * to keep the UI responsive.
2188
- * - `'build'`: one-shot batch run. The context is set up, the devtool
2189
- * collects what it needs, and a snapshot is written. No
2190
- * live UI, no WS server.
2152
+ * Hosted adapters (`vite`, `embedded`) ignore this and defer to the host's
2153
+ * auth; `@vitejs/devtools` honors the equivalent `devtools.clientAuth`.
2191
2154
  *
2192
- * Names are inherited from Vite's serve/build dichotomy but the meaning
2193
- * is general: the same distinction applies to any tool that runs in
2194
- * either an interactive or a static-output mode.
2195
- */
2196
- readonly mode: 'dev' | 'build';
2197
- /**
2198
- * Host runtime abstraction, exposing `mountStatic` / `resolveOrigin` /
2199
- * `getStorageDir`.
2155
+ * @default true
2200
2156
  */
2201
- host: DevframeHost;
2202
- rpc: RpcFunctionsHost;
2203
- views: DevframeViewHost;
2157
+ auth?: boolean | DevframeAuthHandler;
2204
2158
  /**
2205
- * Structured diagnostics host; wraps `nostics` and lets integrations
2206
- * register their own coded errors/warnings into the shared lookup.
2159
+ * How the browser reaches the RPC WebSocket. Defaults to sharing the HTTP
2160
+ * port on the `__ws` route. See {@link DevframeWsOptions} for the
2161
+ * different-port and remote-origin variants. Pass `false` to serve no
2162
+ * WebSocket at all; clients connect over SSE instead (`backend: 'sse'`).
2207
2163
  */
2208
- diagnostics: DevframeDiagnosticsHost;
2164
+ ws?: DevframeWsOptions | false;
2209
2165
  /**
2210
- * Agent host; aggregates the agent-exposed surface of this devtool.
2166
+ * How the browser reaches the SSE RPC endpoint, enabled by default
2167
+ * alongside the WebSocket as the more portable transport. Pass `false`
2168
+ * to disable, or a {@link DevframeSseOptions} to change its route.
2211
2169
  */
2212
- agent: DevframeAgentHost;
2170
+ sse?: boolean | DevframeSseOptions;
2213
2171
  /**
2214
- * Cross-plugin services: a typed, namespaced registry through which one
2215
- * integration exposes a capability (e.g. a data-source registry) and
2216
- * others consume it without a hard package dependency. Ids follow the RPC
2217
- * namespacing rule (`<plugin-id>:<service>`); types come from augmenting
2218
- * the `DevframeServicesRegistry` interface. `whenAvailable` subscriptions
2219
- * absorb setup-order differences between provider and consumer.
2172
+ * Capability-side CAC hook. Called with the CAC instance after the
2173
+ * adapter registers its built-in commands (`build` / `mcp`)
2174
+ * but before `createCac`'s own `configureCli` caller. Use this to
2175
+ * contribute tool-specific flags and subcommands from the definition
2176
+ * itself.
2220
2177
  */
2221
- services: DevframeServicesHost;
2178
+ configure?: (cli: CAC) => void;
2222
2179
  /**
2223
- * This context's own {@link ConnectionMeta.configs}: static, boot-time
2224
- * config a host publishes once through the connection handshake and every
2225
- * client reads read-only. A plain, **non-reactive** object: mutate it
2226
- * during `setup(ctx)` (a plugin sets its keys, a hub aggregates across
2227
- * every installed devframe), never during the session, since it's serialized
2228
- * once, after setup, and changing it afterwards reaches no client.
2180
+ * Typed CLI flags for the default `dev` command, backed by any
2181
+ * [Standard Schema](https://standardschema.dev/) validator (valibot,
2182
+ * zod, arktype, or devframe's built-in `s`). The adapter registers
2183
+ * matching `--kebab-key` options on CAC, validates the parsed values,
2184
+ * and forwards the typed bag to `setup(ctx, { flags })`.
2185
+ *
2186
+ * Use {@link defineCliFlags} to preserve the literal schema-map
2187
+ * shape, and {@link InferCliFlags} to recover the typed output at the
2188
+ * call site:
2229
2189
  *
2230
2190
  * ```ts
2231
- * ctx.staticConfig.dock = {
2232
- * ...ctx.staticConfig.dock,
2233
- * categoryOrder: { ...ctx.staticConfig.dock?.categoryOrder, ...myOrder },
2234
- * }
2235
- * ```
2236
- */
2237
- staticConfig: Partial<DevframeConnectionConfigsRegistry>;
2238
- /**
2239
- * Create a namespace-scoped view of this context. The returned
2240
- * `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
2241
- * key, and streaming channel with `my-plugin:`, and exposes a typed
2242
- * top-level `settings` store. This is the preferred way to consume the
2243
- * context from a single tool's setup code.
2191
+ * const appFlags = defineCliFlags({
2192
+ * depth: v.pipe(v.number(), v.integer()),
2193
+ * config: v.optional(v.string()),
2194
+ * })
2244
2195
  *
2245
- * Pass `null` or `''` to un-scope and get the base context.
2196
+ * defineDevframe({
2197
+ * cli: { flags: appFlags },
2198
+ * setup(ctx, info) {
2199
+ * const flags = info.flags as InferCliFlags<typeof appFlags>
2200
+ * },
2201
+ * })
2202
+ * ```
2246
2203
  */
2247
- scope: {
2248
- <NS extends string>(namespace: NS): DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>;
2249
- (namespace?: null | ''): DevframeNodeContext;
2250
- };
2204
+ flags?: CliFlagsSchema;
2251
2205
  }
2252
2206
  /**
2253
- * Describes where the browser client should open its RPC WebSocket. The
2254
- * object form is the proxy-flexible default: `path` is resolved relative to
2255
- * where `__connection.json` was loaded, and the connection is made to the
2256
- * page's own origin (only the `http`→`ws` / `https`→`wss` protocol swap is
2257
- * applied). This survives reverse proxies that change the host/port, because
2258
- * the client never trusts a server-baked hostname; it reuses its own.
2207
+ * Default dock attributes for the iframe entry a hub synthesizes when it
2208
+ * mounts this devframe. Framework-neutral metadata only; the hub layer
2209
+ * (`ctx.install`) merges these beneath its per-mount `dock` overrides,
2210
+ * which in turn sit beneath the locked, derived `id` / `type` / `url`.
2259
2211
  *
2260
- * Set `port` (and/or `host`) only when the WS endpoint genuinely lives on a
2261
- * different origin than the page, e.g. a side-car server on its own port.
2212
+ * Every field is optional. `title` / `icon` default to the definition's
2213
+ * `name` / `icon` when omitted here; the rest are unset by default.
2214
+ * Standalone adapters (`cli` / `build`) ignore this entirely.
2262
2215
  */
2263
- interface ConnectionMetaWebsocket {
2216
+ interface DevframeDockDefaults {
2217
+ /** Dock entry title. Defaults to the definition's `name`. */
2218
+ title?: string;
2219
+ /** Translations of `title` by BCP 47 tag; see `@devframes/hub`'s `resolveTitle()`. */
2220
+ titleLocales?: Record<string, string>;
2221
+ /** Dock entry icon. Defaults to the definition's `icon`. */
2222
+ icon?: string | {
2223
+ light: string;
2224
+ dark: string;
2225
+ };
2264
2226
  /**
2265
- * Path to the WS endpoint. Relative paths (the default, e.g. `__ws`) are
2266
- * resolved against `__connection.json`'s location; absolute paths (`/__ws`)
2267
- * resolve against the page origin.
2227
+ * Sort weight within the dock; higher sorts earlier.
2228
+ * @default 0
2229
+ */
2230
+ defaultOrder?: number;
2231
+ /**
2232
+ * Category the entry groups under in the dock.
2233
+ * @default 'default'
2234
+ */
2235
+ category?: string;
2236
+ /**
2237
+ * Conditional-visibility expression (same syntax as command `when`
2238
+ * clauses). Set to `'false'` to hide the entry unconditionally.
2239
+ */
2240
+ when?: string;
2241
+ /**
2242
+ * Render-only visibility expression, same syntax as {@link when}. Hides the
2243
+ * entry's own dock-bar button when it evaluates to `false` while leaving it
2244
+ * registered and reachable (activation, RPC lookups, etc.), unlike `when`,
2245
+ * which is the general relevance switch for the entry as a whole.
2246
+ */
2247
+ visibility?: string;
2248
+ /** Badge text rendered on the dock icon (e.g. an unread count). */
2249
+ badge?: string;
2250
+ /** Id of the dock group this entry collapses under, if any. */
2251
+ groupId?: string;
2252
+ /**
2253
+ * A client script the hub imports into the host page (this devframe's **page
2254
+ * script**). An absolute-path `importFrom` is served by the hub under the
2255
+ * mount base and rewritten to that URL, so mounting by package name needs no
2256
+ * host wiring; a URL or bare specifier passes through untouched.
2268
2257
  */
2269
- path?: string;
2270
- /** Override the port. Combined with the page hostname unless `host` is set. */
2271
- port?: number;
2272
- /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
2273
- host?: string;
2258
+ clientScript?: {
2259
+ /**
2260
+ * Initialize after RPC trust without waiting for dock activation.
2261
+ * @default false
2262
+ */
2263
+ eager?: boolean;
2264
+ /** An absolute filesystem path, a served URL, or a bare npm specifier. */
2265
+ importFrom: string;
2266
+ /**
2267
+ * The name to import the module as.
2268
+ * @default 'default'
2269
+ */
2270
+ importName?: string;
2271
+ };
2274
2272
  }
2275
2273
  /**
2276
- * Object form of {@link ConnectionMeta.sse}: the same proxy-safe shape and
2277
- * resolution rules as {@link ConnectionMetaWebsocket}, producing an
2278
- * `http(s)://` endpoint instead of a `ws(s)://` one. Set `port` (and/or
2279
- * `host`) only when the SSE endpoint genuinely lives on a different origin
2280
- * than the page, e.g. a side-car server on its own port.
2274
+ * Runtime information threaded into `setup(ctx, info)`. Adapters
2275
+ * populate the fields that make sense for their deployment. In
2276
+ * particular, `createCac` fills `flags` with the parsed CAC bag.
2281
2277
  */
2282
- interface ConnectionMetaSse {
2283
- /**
2284
- * Path to the SSE endpoint. Relative paths (the default, e.g. `__sse`)
2285
- * are resolved against `__connection.json`'s location; absolute paths
2286
- * (`/__sse`) resolve against the page origin.
2287
- */
2288
- path?: string;
2289
- /** Override the port. Combined with the page hostname unless `host` is set. */
2290
- port?: number;
2291
- /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
2292
- host?: string;
2278
+ interface DevframeSetupInfo {
2279
+ /** Parsed CLI flags, populated by the CLI adapter. */
2280
+ flags?: Record<string, unknown>;
2293
2281
  }
2294
- interface ConnectionMeta {
2295
- /**
2296
- * The server's primary live-RPC transport (`websocket` / `sse`), `static`
2297
- * for a pre-computed RPC dump with no live server, or `none` for a server
2298
- * exposing no RPC transport at all (e.g. an MCP-only deployment).
2299
- */
2300
- backend: 'websocket' | 'sse' | 'static' | 'none';
2282
+ interface DevframeDefinition {
2283
+ id: string;
2284
+ name: string;
2285
+ /** Semver of the tool, surfaced in hub UIs and diagnostics. */
2286
+ version: string;
2287
+ /** npm package name the devframe ships in (e.g. `@scope/my-tool`). */
2288
+ packageName: string;
2301
2289
  /**
2302
- * WebSocket endpoint, resolved by the client into a `ws(s)://` URL:
2290
+ * `import.meta.url` of the module that defines this devframe. **Always
2291
+ * provide it** (`importMetaUrl: import.meta.url`): it is the resolution base
2292
+ * for the tool's own dependency graph, which lets the host resolve
2293
+ * everything against the plugin's own installed packages rather than the
2294
+ * consuming app's.
2303
2295
  *
2304
- * - {@link ConnectionMetaWebsocket}: the proxy-flexible default; a
2305
- * same-origin path relative to `__connection.json`.
2306
- * - `number`: a port on the page's hostname (`ws(s)://<host>:<port>`).
2307
- * - `string`: a full `ws://`/`wss://` URL used verbatim, an `http(s)://`
2308
- * URL with its protocol swapped, or a path resolved same-origin.
2296
+ * - **Remote assets**: becomes the default `resolveFrom` for any remote
2297
+ * {@link StaticAssetsSource} the devframe hosts (its `clientAssets`, and
2298
+ * every `ctx.views.hostStatic` call) that doesn't set one explicitly, so a
2299
+ * locally installed copy of the assets package is served with zero
2300
+ * network. A per-asset `resolveFrom` still wins, and an explicit
2301
+ * `resolveFrom: null` still opts out.
2302
+ * - **Service dependencies**: becomes the base the host resolves declared
2303
+ * {@link DevframeServiceInput | services} from, so a plugin can ship a
2304
+ * service package as its own dependency instead of asking users to install
2305
+ * it.
2306
+ *
2307
+ * Optional for backward compatibility; omitting it falls back to
2308
+ * runtime-directory resolution and disables the zero-network installed-copy
2309
+ * fast paths above.
2309
2310
  */
2310
- websocket?: number | string | ConnectionMetaWebsocket;
2311
+ importMetaUrl?: string;
2312
+ /** Project homepage or documentation URL. */
2313
+ homepage: string;
2314
+ /** One-line summary of what the tool does. */
2315
+ description: string;
2316
+ icon?: string | {
2317
+ light: string;
2318
+ dark: string;
2319
+ };
2311
2320
  /**
2312
- * SSE endpoint (`GET` opens the event stream, `POST` carries RPC calls),
2313
- * resolved by the client into an `http(s)://` URL with the same
2314
- * proxy-safe rules as {@link ConnectionMeta.websocket}:
2321
+ * Default dock attributes applied when a hub mounts this devframe as an
2322
+ * iframe dock entry. Consulted only by the hub install path (`ctx.install`),
2323
+ * which merge these beneath the per-mount `dock` overrides; standalone
2324
+ * adapters (`cli` / `build`) ignore it.
2315
2325
  *
2316
- * - {@link ConnectionMetaSse}: the proxy-flexible default; a
2317
- * same-origin path relative to `__connection.json`.
2318
- * - `string`: a full `http(s)://` URL used verbatim, or a path
2319
- * resolved same-origin.
2326
+ * @see {@link DevframeDockDefaults}
2320
2327
  */
2321
- sse?: string | ConnectionMetaSse;
2328
+ dock?: DevframeDockDefaults;
2322
2329
  /**
2323
- * Present when the dev server exposes a route-based MCP endpoint
2324
- * (the host's `mcp` setting). Advertises the MCP Streamable-HTTP route so in-browser
2325
- * tooling (e.g. an MCP inspector) can discover it without guessing the
2326
- * path. `path` is relative to `__connection.json`'s location, like the
2327
- * WebSocket `path`. `port` is set when the endpoint lives on a side-car
2328
- * server on its own port (bridge mode: `devframeViteBridge`,
2329
- * `@devframes/next`): the client combines the page hostname with `port`
2330
- * and resolves `path` against that origin, mirroring
2331
- * {@link ConnectionMetaWebsocket.port}.
2330
+ * Mount path override. Defaults depend on the adapter:
2331
+ * `/` for standalone (`cli` / `build`), `/__<id>/` for hosted
2332
+ * (`vite` / `embedded`).
2332
2333
  */
2333
- mcp?: {
2334
- path: string;
2335
- port?: number;
2336
- };
2334
+ basePath?: string;
2337
2335
  /**
2338
- * Names of RPC functions that have declared `jsonSerializable: true`.
2339
- * Used by the WS / static client to dispatch the per-call wire
2340
- * serializer (strict JSON for these methods, structured-clone for
2341
- * the rest). Populated by the server / build adapter; absent on
2342
- * legacy clients, in which case all outgoing messages fall back to
2343
- * structured-clone.
2336
+ * How a hub reacts when another devframe sharing this one's `id` is
2337
+ * mounted onto the same hub. Consulted only by hub adapters
2338
+ * (`ctx.install`); standalone adapters (`cli` / `build`)
2339
+ * ignore it.
2340
+ *
2341
+ * @default 'warn'
2344
2342
  */
2345
- jsonSerializableMethods?: string[];
2343
+ duplicationStrategy?: DevframeDuplicationStrategy;
2346
2344
  /**
2347
- * URL of the `__connection.json` that owns this meta's relative paths.
2348
- * Two producers write it:
2345
+ * Declares which runtimes meaningfully support this devframe. Adapters
2346
+ * act on a `false` before doing any work:
2349
2347
  *
2350
- * - The client annotates it (absolute) when it publishes the meta on a
2351
- * shared window for same-origin inheritance: a relative
2352
- * `websocket.path` resolves against this, so a child SPA mounted at
2353
- * another base (e.g. a hub mounting several devframes at `/__foo/`,
2354
- * `/__bar/`, …) inherits a dialable endpoint rather than resolving the
2355
- * path against its own mount.
2356
- * - A static hub build serves it (base-absolute, e.g.
2357
- * `/__devframes/__connection.json`) in each **per-frame** meta, so a
2358
- * frame SPA that fetched its own copy still resolves the shared RPC
2359
- * dump from the hub base. Resolved against the fetched URL.
2348
+ * - `build: false`: `createCac` skips registering the `build` subcommand,
2349
+ * `createBuild` refuses (throws `DF0042`) unless `{ force: true }`, and
2350
+ * a hub static build (`buildHub`) silently skips the devframe - no dock,
2351
+ * no SPA copy, no RPCs in the dump. Useful for a devframe whose value is
2352
+ * inherently live (e.g. it manages real files on disk), so a static
2353
+ * export would only ever produce a broken, write-less shell of the tool.
2354
+ * - `dev: false`: `createDevServer` refuses (throws `DF0058`) unless
2355
+ * `{ force: true }`. Useful for a devframe that only makes sense as a
2356
+ * static export (e.g. a report generator with nothing to serve live).
2360
2357
  */
2361
- baseUrl?: string;
2358
+ capabilities?: {
2359
+ dev?: boolean;
2360
+ build?: boolean;
2361
+ };
2362
2362
  /**
2363
- * A pre-issued bearer token embedded in the meta so a client trusts the
2364
- * server on connect without any prompt or `localStorage` lookup. Only a
2365
- * **hub** serving a **per-frame** connection meta populates this; it
2366
- * authenticates once at the top level and bakes the resulting token into
2367
- * the meta each plugin iframe fetches, so a cross-origin frame (which
2368
- * cannot read the hub's `localStorage`) is still pre-authorized. The
2369
- * standalone `__connection.json` never carries a token.
2370
- *
2371
- * > [!WARNING]
2372
- * > A token in a fetchable JSON is only as protected as the URL serving
2373
- * > it. Emit it exclusively from hub-controlled, per-frame meta, never
2374
- * > from a publicly reachable static `__connection.json`.
2363
+ * Wire services this devframe consumes (see `DevframeServiceDefinition`).
2364
+ * Each entry is either a declarative descriptor
2365
+ * (`{ package, version?, required?, options? }`, where the host imports the
2366
+ * package's default-export factory, resolving it against **this plugin's
2367
+ * own dependencies**) or a ready `DevframeServiceDefinition` (the factory
2368
+ * was already called). The adapter queues these before `setup(ctx)` runs
2369
+ * and constructs each service once at the `ctx.services.ready()` barrier,
2370
+ * merging option sets across every declarer. Missing services are skipped
2371
+ * unless marked `required`; clients feature-detect via
2372
+ * `client.services.has(pkg)` and degrade.
2375
2373
  */
2376
- authToken?: string;
2374
+ services?: DevframeServiceInput[];
2377
2375
  /**
2378
- * Session-scoped token that lets a trusted external viewer register its
2379
- * browser origin before opening the WebSocket. The host includes it in the
2380
- * connection metadata, which the viewer obtains through the host page.
2376
+ * Author's SPA dist, served as the devframe's UI. A local directory, or
2377
+ * a {@link StaticAssetsSource} remote declaration (`{ package, version }`)
2378
+ * served through devframe's caching CDN back-proxy so the assets need not
2379
+ * ship inside the node package.
2381
2380
  *
2382
- * Treat this as a bearer credential. Keep connection metadata containing the
2383
- * token same-origin until the requesting origin has been verified.
2381
+ * Consumed by every adapter that serves the UI (`dev`, `build`, `vite`,
2382
+ * `next`, and the hub install path).
2384
2383
  */
2385
- viewerOriginToken?: string;
2384
+ clientAssets?: StaticAssetsSource;
2385
+ /** RPC-level configuration for this devframe (see {@link DevframeRpcOptions}). */
2386
+ rpc?: DevframeRpcOptions;
2387
+ /** Server-side setup: the primary entrypoint. Runs in every runtime. */
2388
+ setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise<void>;
2389
+ cli?: DevframeCliOptions;
2390
+ }
2391
+ interface DevframeRpcOptions {
2386
2392
  /**
2387
- * Static, host-declared configuration, baked in once at connect time and
2388
- * fixed for the life of the server (e.g. a hub's UI rebrand, or its
2389
- * aggregated dock-bar layout preferences). Read-only from the browser: a
2390
- * client only ever reads `rpc.connectionMeta.configs`, never writes to it.
2391
- *
2392
- * Contrast this with {@link DevframeSettingsRegistry} (`ctx.scope(ns).settings`)
2393
- * and a hub's `devframe:user-settings` shared-state key; both are
2394
- * mutable, user-editable, and synced bidirectionally over RPC for the
2395
- * life of the session. `configs` is the opposite: one-way, immutable,
2396
- * decided by whoever assembled the server.
2397
- *
2398
- * Each key is owned by one package, contributed via declaration merging:
2393
+ * Opt an RPC function into the static-build snapshot **without owning its
2394
+ * definition**, the mechanism a devframe uses to bake a wire service's
2395
+ * RPC (e.g. `@devframes/service-git`'s `status`/`log`/`show`) into its
2396
+ * `build` export, since the service itself defines no `dump`/`snapshot`.
2399
2397
  *
2400
- * ```ts
2401
- * declare module 'devframe/types' {
2402
- * interface DevframeConnectionConfigsRegistry {
2403
- * 'my-key': { some: 'shape' }
2404
- * }
2405
- * }
2406
- * ```
2398
+ * Each entry is either a bare method id (bakes the no-argument call, like
2399
+ * `snapshot: true`) or `{ method, inputs }` where `inputs` is the list of
2400
+ * argument-tuples to bake, or an async provider given the node context
2401
+ * (so it can enumerate at build time, e.g. read commit hashes via the
2402
+ * service's node API). `createBuild` resolves these after setup and
2403
+ * executes the target's own handler per tuple; the first tuple's result
2404
+ * becomes the fallback so any call variant resolves to a baked value.
2407
2405
  */
2408
- configs?: Partial<DevframeConnectionConfigsRegistry>;
2406
+ snapshot?: DevframeSnapshotRpcEntry[];
2409
2407
  }
2408
+ /** Argument-tuples to bake for a {@link DevframeSnapshotRpcEntry}, or a provider that computes them at build time. */
2409
+ type DevframeSnapshotRpcInputs = readonly (readonly unknown[])[] | ((ctx: DevframeNodeContext) => readonly (readonly unknown[])[] | Promise<readonly (readonly unknown[])[]>);
2410
2410
  /**
2411
- * Augmentation point for {@link ConnectionMeta.configs}. Empty by default;
2412
- * a package that wants to publish static, boot-time config through the
2413
- * connection handshake augments this interface with its own key (see
2414
- * {@link ConnectionMeta.configs} for the pattern). `@devframes/hub`
2415
- * augments it with `dock`; `@devframes/hub-ui` augments it with `ui`.
2411
+ * One {@link DevframeRpcOptions.snapshot} entry: a bare method id (bakes
2412
+ * the no-argument call) or a method plus the argument-tuples to bake.
2416
2413
  */
2417
- interface DevframeConnectionConfigsRegistry {}
2418
- export { DevframeServiceOf as $, AgentToolProviderHandle as $t, SharedStateEvents as A, McpFetchHandler as At, DevframeSetupInfo as B, DevframeAuthHandler as Bt, createStreamSink as C, DevframeStorageScope as Ct, ImmutableObject as D, CreateMcpServerOptions as Dt, ImmutableMap as E, CreateMcpFetchHandlerOptions as Et, DevframeDefinition as F, McpResourceTemplate as Ft, McpAuthorization as G, AgentHandle as Gt, DevframeSnapshotRpcInputs as H, InferCliFlags as Ht, DevframeDeploymentKind as I, McpResourceTemplateContent as It, DevframeServiceDefinition as J, AgentResourceContent as Jt, McpRouteOptions as K, AgentManifest as Kt, DevframeDockDefaults as L, McpResourceTemplateEntry as Lt, SharedStatePatch as M, McpPromptArgument as Mt, createSharedState as N, McpPromptMessage as Nt, ImmutableSet as O, McpConnectionInfo as Ot, DevframeCliOptions as P, McpPromptResult as Pt, DevframeServiceMeta as Q, AgentToolProvider as Qt, DevframeDuplicationStrategy as R, McpResourcesChangedHook as Rt, createStreamReader as S, DevframeHost as St, ImmutableArray as T, DevframeDiagnosticsLogger as Tt, DevframeSseOptions as U, defineCliFlags as Ut, DevframeSnapshotRpcEntry as V, CliFlagsSchema as Vt, DevframeWsOptions as W, parseCliFlags as Wt, DevframeServiceId as X, AgentTool as Xt, DevframeServiceDescriptor as Y, AgentResourceInput as Yt, DevframeServiceInput as Z, AgentToolInput as Zt, CreateStreamSinkOptions as _, DevframeViewHost as _t, DevframeConnectionConfigsRegistry as a, DevframeScopedNodeContext as at, StreamSink as b, DevframeRpcSharedStates as bt, RpcBroadcastOptions as c, DevframeSettings as ct, RpcSharedStateHost as d, ScopedBroadcastOptions as dt, DevframeAgentHost as en, DevframeServiceScopeOf as et, RpcStreamingChannel as f, ScopedClientFunctions as ft, CreateStreamReaderOptions as g, SettingsForNamespace as gt, BufferedChunk as h, ScopedSharedStates as ht, DevframeCapabilities as i, EventsMap as in, DevframeServicesState as it, SharedStateOptions as j, McpPrompt as jt, SharedState as k, McpContentBlock as kt, RpcFunctionsHost as l, DevframeSettingsRegistry as lt, RpcStreamingHost as m, ScopedServerFunctions as mt, ConnectionMetaSse as n, EventEmitter as nn, DevframeServicesRegistry as nt, DevframeNodeContext as o, DevframeScopedNodeRpc as ot, RpcStreamingChannelOptions as p, ScopedRpcFn as pt, McpSetting as q, AgentResource as qt, ConnectionMetaWebsocket as r, EventUnsubscribe as rn, DevframeServicesScopeRegistry as rt, DevframeNodeRpcSession as s, DevframeScopedStreamingHost as st, ConnectionMeta as t, DevframeAgentHostEvents as tn, DevframeServicesHost as tt, RpcSharedStateGetOptions as u, DevframeSettingsStore as ut, StreamErrorPayload as v, DevframeRpcClientFunctions as vt, Immutable as w, DevframeDiagnosticsHost as wt, StreamSinkEvents as x, BrowserAgentClientInfo as xt, StreamReader as y, DevframeRpcServerFunctions as yt, DevframeRpcOptions as z, McpServerHandle as zt };
2414
+ type DevframeSnapshotRpcEntry = string | {
2415
+ method: string;
2416
+ inputs: DevframeSnapshotRpcInputs;
2417
+ };
2418
+ export { DevframeServiceId as $, AgentToolProviderHandle as $t, CliFlagsSchema as A, StreamReader as At, DevframeNodeRpcSession as B, SharedState as Bt, McpPromptResult as C, DevframeDiagnosticsHost as Ct, McpResourcesChangedHook as D, CreateStreamReaderOptions as Dt, McpResourceTemplateEntry as E, BufferedChunk as Et, ConnectionMetaSse as F, Immutable as Ft, RpcStreamingChannel as G, AgentHandle as Gt, RpcFunctionsHost as H, SharedStateOptions as Ht, ConnectionMetaWebsocket as I, ImmutableArray as It, DevframeRpcClientFunctions as J, AgentResourceContent as Jt, RpcStreamingChannelOptions as K, AgentManifest as Kt, DevframeCapabilities as L, ImmutableMap as Lt, defineCliFlags as M, StreamSinkEvents as Mt, parseCliFlags as N, createStreamReader as Nt, McpServerHandle as O, CreateStreamSinkOptions as Ot, ConnectionMeta as P, createStreamSink as Pt, DevframeServiceDescriptor as Q, AgentToolProvider as Qt, DevframeConnectionConfigsRegistry as R, ImmutableObject as Rt, McpPromptMessage as S, DevframeStorageScope as St, McpResourceTemplateContent as T, BrowserAgentClientInfo as Tt, RpcSharedStateGetOptions as U, SharedStatePatch as Ut, RpcBroadcastOptions as V, SharedStateEvents as Vt, RpcSharedStateHost as W, createSharedState as Wt, DevframeRpcSharedStates as X, AgentTool as Xt, DevframeRpcServerFunctions as Y, AgentResourceInput as Yt, DevframeServiceDefinition as Z, AgentToolInput as Zt, McpConnectionInfo as _, ScopedServerFunctions as _t, DevframeDuplicationStrategy as a, DevframeServicesRegistry as at, McpPrompt as b, DevframeViewHost as bt, DevframeSnapshotRpcEntry as c, DevframeScopedNodeContext as ct, DevframeWsOptions as d, DevframeSettings as dt, DevframeAgentHost as en, DevframeServiceInput as et, McpAuthorization as f, DevframeSettingsRegistry as ft, CreateMcpServerOptions as g, ScopedRpcFn as gt, CreateMcpFetchHandlerOptions as h, ScopedClientFunctions as ht, DevframeDockDefaults as i, EventsMap as in, DevframeServicesHost as it, InferCliFlags as j, StreamSink as jt, DevframeAuthHandler as k, StreamErrorPayload as kt, DevframeSnapshotRpcInputs as l, DevframeScopedNodeRpc as lt, McpSetting as m, ScopedBroadcastOptions as mt, DevframeDefinition as n, EventEmitter as nn, DevframeServiceOf as nt, DevframeRpcOptions as o, DevframeServicesScopeRegistry as ot, McpRouteOptions as p, DevframeSettingsStore as pt, RpcStreamingHost as q, AgentResource as qt, DevframeDeploymentKind as r, EventUnsubscribe as rn, DevframeServiceScopeOf as rt, DevframeSetupInfo as s, DevframeServicesState as st, DevframeCliOptions as t, DevframeAgentHostEvents as tn, DevframeServiceMeta as tt, DevframeSseOptions as u, DevframeScopedStreamingHost as ut, McpContentBlock as v, ScopedSharedStates as vt, McpResourceTemplate as w, DevframeDiagnosticsLogger as wt, McpPromptArgument as x, DevframeHost as xt, McpFetchHandler as y, SettingsForNamespace as yt, DevframeNodeContext as z, ImmutableSet as zt };