devframe 0.9.2 → 0.9.4

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 (63) hide show
  1. package/dist/{_shared-BAGBgTUO.d.mts → _shared-BPovvJW8.d.mts} +1 -1
  2. package/dist/adapters/build.d.mts +11 -2
  3. package/dist/adapters/build.mjs +40 -4
  4. package/dist/adapters/cac.d.mts +1 -1
  5. package/dist/adapters/cac.mjs +1 -1
  6. package/dist/adapters/dev.d.mts +4 -4
  7. package/dist/adapters/dev.mjs +1 -1
  8. package/dist/adapters/embedded.d.mts +1 -1
  9. package/dist/adapters/initiate.d.mts +3 -3
  10. package/dist/adapters/initiate.mjs +1 -1
  11. package/dist/adapters/mcp.d.mts +1 -1
  12. package/dist/adapters/mcp.mjs +1 -1
  13. package/dist/cli/main.mjs +2 -2
  14. package/dist/client/index.d.mts +5 -5
  15. package/dist/{context-af2w_F0_.mjs → context-BUJGvlje.mjs} +1 -1
  16. package/dist/{context-al99DDJ8.d.mts → context-CO7eZHJy.d.mts} +1 -1
  17. package/dist/{context-riPPHGiu.mjs → context-CQW0lSw8.mjs} +10 -5
  18. package/dist/{devframe-mbfgpQQC.d.mts → context-_i51nYOs.d.mts} +1199 -1170
  19. package/dist/{dev-BKm_HisC.mjs → dev-lpIvwagf.mjs} +5 -5
  20. package/dist/{diagnostics-DI1HGj2I.mjs → diagnostics-BXWW3VK-.mjs} +4 -0
  21. package/dist/{host-agent-DVDLqjvj.mjs → host-agent-B84av916.mjs} +1 -1
  22. package/dist/{http-POepPnaQ.mjs → http-BKiqHcXX.mjs} +2 -2
  23. package/dist/{index-Dipgo9ji.d.mts → index-CEvd-YXD.d.mts} +2 -2
  24. package/dist/{index-tdrk1bzV.d.mts → index-DOg4S5wk.d.mts} +1 -1
  25. package/dist/index.d.mts +6 -6
  26. package/dist/{instance-registry-Dy28drtm.mjs → instance-registry-BjlMQbFB.mjs} +1 -1
  27. package/dist/{instance-shell-GqTY93gC.mjs → instance-shell-B-uevnK-.mjs} +4 -4
  28. package/dist/{instance-shell-mU5j5xWn.d.mts → instance-shell-BHvBIGP4.d.mts} +3 -3
  29. package/dist/internal/index.d.mts +11 -5
  30. package/dist/internal/index.mjs +5 -5
  31. package/dist/node/auth.d.mts +2 -2
  32. package/dist/node/hub-internals.d.mts +2 -2
  33. package/dist/node/hub-internals.mjs +1 -1
  34. package/dist/node/index.d.mts +3 -3
  35. package/dist/node/index.mjs +2 -2
  36. package/dist/recipes/common-rpc-functions.d.mts +3 -3
  37. package/dist/recipes/interactive-auth.d.mts +2 -2
  38. package/dist/recipes/interactive-auth.mjs +1 -1
  39. package/dist/{remote-assets-CqxiyltC.mjs → remote-assets-DezQmPQU.mjs} +1 -1
  40. package/dist/rpc/dump.d.mts +1 -1
  41. package/dist/rpc/index.d.mts +2 -2
  42. package/dist/rpc/transports/sse-client.d.mts +1 -1
  43. package/dist/rpc/transports/sse-server.d.mts +2 -2
  44. package/dist/rpc/transports/ws-bun.d.mts +2 -2
  45. package/dist/rpc/transports/ws-client.d.mts +1 -1
  46. package/dist/rpc/transports/ws-deno.d.mts +2 -2
  47. package/dist/rpc/transports/ws-server.d.mts +1 -1
  48. package/dist/{rpc-core-dV69u4lY.d.mts → rpc-core-CDZoGYiD.d.mts} +3 -3
  49. package/dist/{rpc-core-DRYnbxdD.mjs → rpc-core-DlHd276T.mjs} +1 -1
  50. package/dist/{sse-client-CFBOIaUi.d.mts → sse-client-CdGD_B8_.d.mts} +1 -1
  51. package/dist/{storage-BNqSAOxA.mjs → storage-BXNVhKyR.mjs} +1 -1
  52. package/dist/types/index.d.mts +5 -5
  53. package/dist/{types-Djq7CBXh.d.mts → types-BBLeGfpt.d.mts} +1 -1
  54. package/dist/utils/events.d.mts +1 -1
  55. package/dist/utils/remote-assets.mjs +1 -1
  56. package/dist/utils/shared-state.d.mts +1 -1
  57. package/dist/utils/streaming-channel.d.mts +1 -1
  58. package/dist/{ws-client-DhjNyQo3.d.mts → ws-client-Bo2t6hES.d.mts} +1 -1
  59. package/dist/{ws-server-D1d3QM9f.d.mts → ws-server-DLtEoLdR.d.mts} +1 -1
  60. package/package.json +1 -1
  61. package/skills/devframe/SKILL.md +313 -292
  62. package/skills/devframe/templates/counter-devframe.ts +8 -3
  63. package/skills/devframe/templates/hub.ts +50 -0
@@ -1,63 +1,85 @@
1
1
  ---
2
2
  name: devframe
3
3
  description: >
4
- Use when building a devtool with devframe - the
5
- framework- and build-tool-agnostic foundation for defining a
6
- devtool once and serving it in many places. Covers
7
- DevframeDefinition, picking the right deployment adapter
8
- (cli / build / vite / embedded / mcp), designing RPC
9
- contracts, exposing an agent-native surface over MCP, and
10
- wiring the author's SPA client. For host-level features (docks,
11
- terminals, palette, etc.), the devframe can be mounted into a
12
- host that provides them - Vite DevTools is one supported target,
13
- reached via the `vite` adapter. Triggers on `devframe` imports,
14
- `defineDevframe`, `createCac`, `createMcpServer`,
15
- `connectDevframe`, and on migrations of existing inspectors
4
+ Use when building a devtool with devframe - the framework- and
5
+ build-tool-agnostic foundation for defining a devtool once and
6
+ serving it anywhere. Covers the DevframeDefinition, the standard
7
+ web handler (`initDevframe` → `handler` / `nodeMiddleware`) that
8
+ mounts a tool into any host, the packaging adapters (cli / build /
9
+ dev / mcp / embedded), the framework packages (`@devframes/vite`,
10
+ `@devframes/next`, `@devframes/nuxt`, each split into `single` and
11
+ `hub`), composing many tools into one devtools host with
12
+ `@devframes/hub` (`initHub`, docks / commands / messages /
13
+ terminals), designing RPC contracts, exposing an agent-native
14
+ surface over MCP, and wiring the author's SPA client. Triggers on
15
+ `devframe` imports, `defineDevframe`, `initDevframe`, `initHub`,
16
+ `createCac`, `createMcpServer`, `connectDevframe`, `@devframes/*`
17
+ imports, and on migrations of existing inspectors
16
18
  (eslint-config-inspector, unocss-inspector,
17
- node-modules-inspector-style tools) to devframe.
19
+ node-modules-inspector-style tools) onto devframe.
18
20
  ---
19
21
 
20
22
  # devframe skill
21
23
 
22
- **Devframe is an asset: define your devtool once, serve it anywhere.** A devtool built on devframe is a single `DevframeDefinition` plus an author-provided SPA - the same definition deploys through a set of pluggable adapters (standalone CLI, static report, embedded SPA, MCP server, mounted into a host, etc.). Devframe is framework- and build-tool-agnostic; it has no Vite dependency and makes no UI-framework assumption.
24
+ **Devframe is the `unplugin` for devtools: define a tool once, mount it anywhere.** A devtool built on devframe is a single `DevframeDefinition` plus an author-provided SPA. That definition describes one tool - its RPC surface, shared state, diagnostics, web interface, and agent-facing surface - independent of how it is presented. The same definition then deploys through a standard web handler, a set of packaging adapters, thin framework integrations, or composed with other tools inside a hub.
23
25
 
24
- Devframe describes one tool. If you need host-level features (cross-tool palette, integrated terminals, dock aggregation), mount the devframe into a host that provides them - [Vite DevTools](https://devtools.vite.dev/) is the canonical example, reached via the `vite` adapter - or build your own host adapter. `devframe` itself must not depend on Vite or any `@vitejs/*` package.
26
+ Two layers, one boundary:
25
27
 
26
- Full reference: [devfra.me/](https://devfra.me/).
28
+ - **A devframe** is one portable tool. `initDevframe(def, { base })` turns it into a live instance whose `.handler` is a Web-Standard `(request: Request) => Promise<Response>` carrying the whole surface (SPA, discovery, WebSocket RPC, auth gate, optional MCP route) under one mount base. Anything that can mount a catch-all route or Connect-style middleware can host it.
29
+ - **A hub** (`@devframes/hub`) composes *many* devframes behind one namespace with a shared RPC registry, one transport, one auth gate, and the orchestration features that only make sense when tools share a UI (docks, commands, messages, terminals). `initHub()` puts the whole collection behind the same kind of standard handler.
27
30
 
28
- ## When to use devframe
31
+ Devframe is framework- and build-tool-agnostic - it has zero dependency on Vite or any `@vitejs/*` package and makes no UI-framework assumption. [Vite DevTools](https://devtools.vite.dev/) is the first flagship *host* built on it; the built-in plugins deliberately span Vue, Svelte, Solid, React, and Next to prove the point.
29
32
 
30
- All adapter factories share the shape `createXxx(devframeDef, options?)`.
33
+ High-level concept: [Pluggable, Extensible, and Playful DevTools](https://antfu.me/posts/pluggable-extensible-playful-devtools). Full reference: [devfra.me](https://devfra.me/).
31
34
 
32
- | Author goal | Factory | Entry |
33
- |-------------|---------|-------|
34
- | Standalone CLI for local use | `createCac(def, options?)` | `devframe/adapters/cac` |
35
- | Run the dev server programmatically (any CLI framework) | `createDevServer(def, options?)` | `devframe/adapters/dev` |
36
- | Self-contained static deploy with baked data | `createBuild(def, options?)` | `devframe/adapters/build` |
37
- | Mount into a host (Vite DevTools or any compatible host) | `createPluginFromDevframe(def, options?)` | `@vitejs/devtools-kit/node` |
38
- | Register dynamically at runtime | `createEmbedded(def, { ctx })` | `devframe/adapters/embedded` |
39
- | Expose to coding agents (MCP) | `createMcpServer(def, options?)` | `devframe/adapters/mcp` |
35
+ ## Deployment map — pick by how it's served, not by what it does
40
36
 
41
- The same `DevframeDefinition` runs under every adapter - pick based on deployment, not on what the tool does.
37
+ The same `DevframeDefinition` runs under every one of these. Choose based on where the tool needs to live.
42
38
 
43
- For Vite-based hosts that don't use the kit (Nuxt, Astro, SolidStart, plain Vite apps), `@devframes/vite` exports `devframeVitePlugin(def, options?)` (static SPA mount) and `devframeViteBridge(def, options?)` (RPC + WS bridge alongside the host's dev server) - plus `devframeVite(def, { bridge, ...options })` as a convenience wrapper over both. Not an adapter; just a Vite integration helper.
39
+ **Serve one devframe:**
40
+
41
+ | Goal | Entry | Import |
42
+ |------|-------|--------|
43
+ | Mount into any host (the portability primitive) | `initDevframe(def, { base })` → `.handler` / `.nodeMiddleware` | `devframe/initiate` |
44
+ | Standalone CLI (dev / build / mcp subcommands) | `createCac(def, opts?).parse()` | `devframe/adapters/cac` |
45
+ | Programmatic dev server | `createDevServer(def, opts?)` | `devframe/adapters/dev` |
46
+ | Self-contained static deploy with baked data | `createBuild(def, opts?)` | `devframe/adapters/build` |
47
+ | MCP server for coding agents | `createMcpServer(def, opts?)` | `devframe/adapters/mcp` |
48
+ | Runtime registration into an existing host | `createEmbedded(def, { ctx })` | `devframe/adapters/embedded` |
49
+ | Ride along a Vite dev server (no dock) | `devframeVitePlugin` / `devframeViteBridge` / `devframeVite` | `@devframes/vite/single` |
50
+ | Author one devframe's SPA with Next | `withDevframe` + `createDevframeNextHandler` | `@devframes/next/single` |
51
+ | Author one devframe's SPA with Nuxt | Nuxt module | `@devframes/nuxt/single` |
52
+ | Mount into the Vite DevTools dock | `createPluginFromDevframe(def, opts?)` | `@vitejs/devtools-kit/node` |
53
+
54
+ **Serve many devframes as a devtools host:**
55
+
56
+ | Goal | Entry | Import |
57
+ |------|-------|--------|
58
+ | Compose a hub behind one handler | `initHub({ base, devframes, ui })` | `@devframes/hub/initiate` |
59
+ | Imperatively mount into a hub context | `createHubContext(...)` → `ctx.install(def)` | `@devframes/hub/node` |
60
+ | Reference viewer UI for a hub | `createUi(opts?)` | `@devframes/hub-ui` |
61
+ | Mount a hub inside Vite / Next / Nuxt | `viteDevframeHub` / `nextDevframeHub` / hub module | `@devframes/{vite,next,nuxt}/hub` |
62
+
63
+ `createCac`, `createDevServer`, the `@devframes/vite` bridge, and `@devframes/next` are all assembled from `initDevframe` internally - the standard handler is the one wiring underneath every serving path.
44
64
 
45
65
  ## Minimum viable devframe
46
66
 
47
67
  ```ts
48
68
  import { defineDevframe, defineRpcFunction } from 'devframe'
69
+ import pkg from '../package.json' with { type: 'json' }
49
70
 
50
71
  export default defineDevframe({
51
72
  id: 'my-inspector',
52
- name: 'My Inspector',
53
- version: '1.0.0',
54
- packageName: 'my-inspector',
55
- homepage: 'https://github.com/me/my-inspector',
56
- description: 'Inspects things and reports stats.',
73
+ name: 'My Inspector', // display label — distinct from packageName
74
+ version: pkg.version,
75
+ packageName: pkg.name,
76
+ importMetaUrl: import.meta.url, // resolution base for the tool's own deps (assets, services)
77
+ homepage: pkg.homepage,
78
+ description: pkg.description,
57
79
  icon: 'ph:magnifying-glass-duotone',
58
80
  cli: { distDir: './client/dist' },
59
81
  setup(ctx) {
60
- const my = ctx.scope('my-inspector') // preferred - auto-namespaces ids
82
+ const my = ctx.scope('my-inspector') // preferred — auto-namespaces ids
61
83
  my.rpc.register(defineRpcFunction({
62
84
  name: 'get-stats', // stored as `my-inspector:get-stats`
63
85
  type: 'static',
@@ -67,65 +89,89 @@ export default defineDevframe({
67
89
  })
68
90
  ```
69
91
 
70
- **Recommended:** keep `version` / `packageName` / `homepage` / `description` in sync with your published package by sourcing them from `package.json` rather than hardcoding. The package's `name` maps to `packageName`; the devframe `name` is a separate display label. Use the JSON import-attribute form so it resolves under both bundlers and Node's native TypeScript execution:
92
+ Source `version` / `packageName` / `homepage` / `description` from your published `package.json` (the JSON import-attribute form resolves under both bundlers and Node's native TypeScript execution). Always pass `importMetaUrl: import.meta.url` - it is the base the host resolves the tool's own companion packages against (a `--assets` package holding the built SPA, a wire-service package), so a plugin ships them as its own dependencies and users install nothing extra.
93
+
94
+ `setup(ctx, info?)` runs in **every** runtime and does all devframe-level wiring: RPC functions, shared state, streaming channels, diagnostics, agent surface. Its optional second argument carries runtime metadata (most notably parsed CLI `flags` under `createCac`). Gate per-runtime work on `ctx.mode` (`'dev'` | `'build'`).
95
+
96
+ **A plugin's default export is its `create<X>Devframe` factory, never a pre-built instance** - `export default createMyInspectorDevframe`, so importing the module costs nothing and each consumer calls the factory (with or without options) to get its own instance.
97
+
98
+ See `templates/counter-devframe.ts` for a runnable example, `templates/hub.ts` for composing a hub, and `templates/vite-client.ts` for the author's client entry.
99
+
100
+ ## The standard handler (`initDevframe`)
101
+
102
+ This is the portability trick and the thing to reach for whenever a host can mount a route. `base` is required, so the mount path is explicit at the call site.
71
103
 
72
104
  ```ts
73
- import pkg from '../package.json' with { type: 'json' }
105
+ import { initDevframe } from 'devframe/initiate'
106
+ import myDevframe from './devframe'
74
107
 
75
- export default defineDevframe({
76
- id: 'my-inspector',
77
- name: 'My Inspector',
78
- version: pkg.version,
79
- packageName: pkg.name,
80
- homepage: pkg.homepage,
81
- description: pkg.description,
82
- // …
83
- })
108
+ const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })
109
+ // devtools.base, .handler, .nodeMiddleware, .attach, .handleUpgrade,
110
+ // .ready, .context, .connectionMeta(), .close()
111
+ ```
112
+
113
+ Mount `.handler` (Web-Standard) or `.nodeMiddleware` (Connect-style) on a catch-all route:
114
+
115
+ ```ts
116
+ // Hono — `serve()` returns the node server the socket rides on
117
+ app.all('/__my-tool/*', c => devtools.handler(c.req.raw))
118
+ devtools.attach(serve({ fetch: app.fetch, port: 3000 }))
119
+
120
+ // Vite — connect middleware + Vite's own server for the socket
121
+ server.middlewares.use(initDevframe(myDevframe, {
122
+ base: '/__my-tool/',
123
+ server: server.httpServer ?? undefined,
124
+ }).nodeMiddleware)
84
125
  ```
85
126
 
86
- `setup(ctx)` registers RPC functions, shared state, diagnostics, and any other devframe-level wiring. Host adapters can augment `ctx` with extra surfaces - for example, mounting into Vite DevTools via `createPluginFromDevframe(d)` exposes `docks`, `terminals`, `messages`, and `commands` on the augmented context, and the kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`. For richer host-side behaviour (custom-render, terminals, palette commands) pass `options.setup` to `createPluginFromDevframe`.
127
+ `devtools.base` is the normalized mount base - reference it in route guards instead of repeating the string.
128
+
129
+ **The WebSocket binding** is the host's explicit call. Fetch handlers only hand over `Request`s, so the RPC socket needs its own binding, resolved in precedence order:
130
+
131
+ 1. `ws.port` — a side-car on that exact port.
132
+ 2. `server` — share the host's `node:http` server; the upgrade binds at `<base>__ws`. Zero extra ports, follows the app through proxies/HTTPS.
133
+ 3. `ws: { sidecar: true }` — a side-car on a free port, for hosts whose handlers never see upgrades (Next.js route handlers, Nitro, SvelteKit, Rsbuild).
134
+ 4. **The host's own upgrades** — with none of the above, `devtools.attach(server)` routes a server's `upgrade` events (returns a detach fn) and `devtools.handleUpgrade(req, socket, head)` completes a single one. Built lazily — an instance nobody attaches costs nothing.
135
+
136
+ `ws.url` instead controls the *advertisement* (the tunnel/external-transport pattern). Whichever is active, `__connection.json` describes it and the browser client follows.
87
137
 
88
- See `templates/counter-devframe.ts` for a runnable counter example and `templates/vite-client.ts` for the author's client entry.
138
+ Frameworks that re-evaluate modules in dev (Next, Nitro, SvelteKit) must memoize the instance on `globalThis`, or every reload leaks the previous WebSocket server. The framework packages do this for you.
139
+
140
+ **Auth gates by default** - a handler mounted inside an app server is reachable by anything that can open its socket. The interactive OTP handler is wired automatically and prints its code / magic-link once the public origin is known. Pass `auth: false` only for a single-user localhost setup, or a `DevframeAuthHandler` for a custom scheme.
89
141
 
90
142
  ## Scoped context (preferred)
91
143
 
92
- `ctx.scope(id)` (server) and `client.scope(id)` (browser) return a namespace-scoped view that auto-prefixes every RPC id, shared-state key, and streaming channel with `id:`, and adds a top-level persisted `settings` store. Prefer it over the raw `ctx.rpc` / client - you name the namespace once and register / call by bare name.
144
+ `ctx.scope(id)` (server) and `client.scope(id)` (browser) return a namespace-scoped view that auto-prefixes every RPC id, shared-state key, and streaming channel with `id:`, and adds a top-level persisted `settings` store. Prefer it over raw `ctx.rpc` / client - name the namespace once, register and call by bare name.
93
145
 
94
146
  ```ts
95
- // server - setup(ctx)
147
+ // server — setup(ctx)
96
148
  const my = ctx.scope('my-inspector')
97
- my.rpc.register(getStats) // -> my-inspector:get-stats
98
- await my.rpc.call('get-stats') // invokeLocal, namespaced
149
+ my.rpc.register(getStats) // -> my-inspector:get-stats
150
+ await my.rpc.call('get-stats') // invokeLocal, namespaced
99
151
  const state = await my.rpc.sharedState('view') // -> my-inspector:view
100
152
  await my.settings.project.set('theme', 'dark')
101
153
 
102
- // browser - connectDevframe()
154
+ // browser — connectDevframe()
103
155
  const my = (await connectDevframe()).scope('my-inspector')
104
156
  const stats = await my.rpc.call('get-stats')
105
157
  ```
106
158
 
107
- - **Auto-namespacing.** Bare names get `id:` prepended; a name already containing `:` is treated as fully-qualified and passed through (so `my.rpc.call('other-tool:fn')` works). `register` only accepts bare names - passing a namespaced one throws `DF0034`.
108
- - **Typed bare calls.** Define functions with bare names and augment the registry with `RpcDefinitionsToFunctionsWithNamespace<'my-inspector', typeof serverFunctions>` so the registry keys match the namespaced runtime ids; scoped `call('get-stats')` then stays typed.
109
- - **`base`.** The scoped context keeps the raw context as `my.base` (and re-exposes `views` / `diagnostics` / `agent` / `host` / `cwd` / `mode` on the server).
159
+ - **Auto-namespacing.** Bare names get `id:` prepended; a name already containing `:` is treated as fully-qualified and passed through (so `my.rpc.call('other-tool:fn')` works). `register` only accepts bare names - a namespaced one throws `DF0034`.
160
+ - **Typed bare calls.** Define functions with bare names and augment the registry with `RpcDefinitionsToFunctionsWithNamespace<'my-inspector', typeof serverFunctions>` so registry keys match the namespaced runtime ids; scoped `call('get-stats')` then stays typed.
161
+ - **`base`.** The scoped context keeps the raw context as `my.base` (and re-exposes `views` / `diagnostics` / `agent` / `services` / `host` / `cwd` / `mode` on the server).
110
162
 
111
163
  ### Settings
112
164
 
113
- `my.settings` is a persisted key-value store at the **top level** of the scoped context (a sibling of `my.rpc`, not under it). Two scopes:
114
-
115
- - `project` - per-workspace, persisted under the host's `workspace` storage dir.
116
- - `global` - per-user, persisted under the host's `global` storage dir.
117
-
118
- Both are file-backed on the server and synced to clients over the shared-state protocol, so a `set` on either side propagates everywhere and survives restarts. All methods are async.
165
+ `my.settings` is a persisted key-value store at the **top level** of the scoped context (a sibling of `my.rpc`). Two scopes: `project` (per-workspace) and `global` (per-user). Both are file-backed on the server and synced to clients over the shared-state protocol, so a `set` on either side propagates everywhere and survives restarts. All methods are async.
119
166
 
120
167
  ```ts
121
168
  await my.settings.project.set('theme', 'dark')
122
169
  await my.settings.project.get('theme') // 'dark'
123
170
  await my.settings.global.all()
124
- await my.settings.project.delete('theme')
125
171
  const off = await my.settings.global.onChange(value => apply(value))
126
172
  ```
127
173
 
128
- Type a namespace's settings shape by augmenting `DevframeSettingsRegistry`:
174
+ Type a namespace's settings by augmenting `DevframeSettingsRegistry`:
129
175
 
130
176
  ```ts
131
177
  declare module 'devframe' {
@@ -135,9 +181,37 @@ declare module 'devframe' {
135
181
  }
136
182
  ```
137
183
 
184
+ ## DevframeNodeContext at a glance
185
+
186
+ `setup(ctx)` receives the framework-neutral server-side surface:
187
+
188
+ | Host | Purpose |
189
+ |------|---------|
190
+ | `ctx.scope(id)` | **Preferred** namespace-scoped view — auto-prefixed `rpc` + top-level `settings` store |
191
+ | `ctx.rpc` | Register RPC functions, broadcast, shared state, streaming channels |
192
+ | `ctx.views` | Serve static files via `hostStatic(base, distDir)` |
193
+ | `ctx.diagnostics` | Structured diagnostics host (nostics) — register custom error codes |
194
+ | `ctx.agent` | Expose tools + resources to coding agents |
195
+ | `ctx.services` | Typed cross-plugin service registry (`provide` / `whenAvailable`) |
196
+ | `ctx.staticConfig` | This context's own `ConnectionMeta.configs` — boot-time, read-only-from-browser data |
197
+ | `ctx.host` | Runtime abstraction — `mountStatic`, `resolveOrigin`, `getStorageDir` |
198
+ | `ctx.mode` | `'dev'` or `'build'` — gate setup work per runtime |
199
+
200
+ > Hub adapters augment `ctx` with extra surfaces (`docks`, `terminals`, `messages`, `commands`) — see [The Hub](#the-hub). The Vite DevTools kit exposes the same subsystems via an optional `setup` hook.
201
+
202
+ **Storage scopes** — `ctx.host.getStorageDir(scope)` places persisted state in one of three classes:
203
+
204
+ | Scope | Placement | For |
205
+ |-------|-----------|-----|
206
+ | `workspace` | committable, `<workspaceRoot>/.devframe/` | team-shared presets, shared config |
207
+ | `project` | per-checkout, `<cwd>/node_modules/.<app>/devframe/` | caches, personal settings |
208
+ | `global` | per-user, `~/.<app>/devframe/` | auth tokens, machine-wide prefs |
209
+
210
+ Scoped settings persist their `project` scope through `project` storage and their `global` scope through `global`.
211
+
138
212
  ## Project layout
139
213
 
140
- Once a devframe grows past a handful of RPC functions, split them out - one file per function under `src/rpc/functions/`, with `src/rpc/index.ts` as the barrel. The `functions/` subdirectory leaves room for sibling files like `src/rpc/utils.ts` (helpers, type aliases) as the surface grows. Each function file exports a named const with a **bare** name; the barrel collects them into a `const serverFunctions = [...] as const` and feeds the type-safe client registry recipe with the namespace-aware helper `RpcDefinitionsToFunctionsWithNamespace<'my-tool', typeof serverFunctions>` (it prefixes each bare name to match the namespaced runtime id the scope registers).
214
+ Once a devframe grows past a couple of RPC functions, split them out - one file per function under `src/rpc/functions/`, with `src/rpc/index.ts` as the barrel that collects them into `const serverFunctions = [...] as const` and feeds the type-safe client registry via `RpcDefinitionsToFunctionsWithNamespace<'my-tool', typeof serverFunctions>`.
141
215
 
142
216
  ```ts
143
217
  // src/rpc/functions/list-files.ts
@@ -145,7 +219,7 @@ import { defineRpcFunction } from 'devframe'
145
219
  import { getMyToolContext } from '../../context'
146
220
 
147
221
  export const listFiles = defineRpcFunction({
148
- name: 'list-files', // bare - the scope namespaces it to `my-tool:list-files`
222
+ name: 'list-files', // bare — the scope namespaces it to `my-tool:list-files`
149
223
  type: 'query',
150
224
  jsonSerializable: true,
151
225
  setup: (ctx) => {
@@ -180,6 +254,7 @@ export default defineDevframe({
180
254
  name: 'My Tool',
181
255
  version: pkg.version,
182
256
  packageName: pkg.name,
257
+ importMetaUrl: import.meta.url,
183
258
  homepage: pkg.homepage,
184
259
  description: pkg.description,
185
260
  setup(ctx) {
@@ -190,21 +265,14 @@ export default defineDevframe({
190
265
  })
191
266
  ```
192
267
 
193
- Note `setMyToolContext(ctx, …)` keys off the raw `ctx` (the same object the function `setup(ctx)` receives) - store the per-tool context on `ctx`, register through `my.rpc`.
194
-
195
268
  ### Sharing setup-time state via `src/context.ts`
196
269
 
197
- When per-file RPCs need access to runtime values that `setup(ctx)` constructs once - streaming channels, shared state handles, watchers, loaders, caches - expose them through a `WeakMap<DevframeNodeContext, T>` in a sibling `src/context.ts`. This mirrors the framework's own `internalContextMap` in `packages/devframe/src/node/hub-internals/context.ts`. The WeakMap keys off the existing `DevframeNodeContext` so contexts are garbage-collected automatically when the host tears down.
270
+ When per-file RPCs need runtime values `setup(ctx)` constructs once - channels, shared-state handles, watchers, loaders, caches - expose them through a `WeakMap<DevframeNodeContext, T>` in a sibling `src/context.ts`. The WeakMap keys off the existing `DevframeNodeContext` so contexts are GC'd automatically when the host tears down.
198
271
 
199
272
  ```ts
200
273
  // src/context.ts
201
274
  import type { DevframeNodeContext } from 'devframe'
202
275
 
203
- export interface MyToolContext {
204
- loaders: { list: () => Promise<string[]> }
205
- // …channels, shared state handles, watchers, etc.
206
- }
207
-
208
276
  const map = new WeakMap<DevframeNodeContext, MyToolContext>()
209
277
 
210
278
  export function setMyToolContext(ctx: DevframeNodeContext, value: MyToolContext): void {
@@ -214,49 +282,34 @@ export function setMyToolContext(ctx: DevframeNodeContext, value: MyToolContext)
214
282
  export function getMyToolContext(ctx: DevframeNodeContext): MyToolContext {
215
283
  const value = map.get(ctx)
216
284
  if (!value)
217
- throw new Error('my-tool context not initialised - call setMyToolContext in devframe.setup')
285
+ throw new Error('my-tool context not initialised — call setMyToolContext in devframe.setup')
218
286
  return value
219
287
  }
220
288
  ```
221
289
 
222
- Stateless RPCs and tiny demos can keep the inline shorthand inside `setup(ctx)` - reach for `src/rpc/functions/` and `src/context.ts` once you have more than one or two functions, or any shared setup state.
290
+ Note `setMyToolContext(ctx, …)` keys off the raw `ctx` (the same object `setup(ctx)` receives), while registration goes through `my.rpc`. Stateless RPCs and tiny demos can keep the inline shorthand inside `setup(ctx)`.
223
291
 
224
292
  ## Namespacing
225
293
 
226
- **Always prefix** RPC names, dock IDs, command IDs, shared-state keys, and agent tool IDs with the devframe `id`:
294
+ **Always prefix** RPC names, dock IDs, command IDs, shared-state keys, and agent tool IDs with the devframe `id` - a hub may mount many tools side by side.
227
295
 
228
296
  ```ts
229
297
  'my-inspector:get-modules' // ✓
230
- 'my-inspector:state' // ✓
231
- 'get-modules' // ✗ - may collide with other devframes sharing the host
298
+ 'get-modules' // ✗ — may collide with other devframes sharing the host
232
299
  ```
233
300
 
234
- A [scoped context](#scoped-context-preferred) applies this prefix for you - `ctx.scope('my-inspector').rpc.register({ name: 'get-modules' })` registers `my-inspector:get-modules`. Define and call by bare name through the scope; reach for full ids only via `ctx.base` or when targeting another tool. Dock / command IDs are host-level (not part of the scoped `rpc` surface) - prefix those by hand.
235
-
236
- ## DevframeNodeContext at a glance
237
-
238
- `setup(ctx)` receives the framework-neutral server-side surface. Each host corresponds to a [docs](https://devfra.me/) page:
239
-
240
- | Host | Purpose |
241
- |------|---------|
242
- | `ctx.scope(id)` | **Preferred** namespace-scoped view - auto-prefixed `rpc` + top-level `settings` store |
243
- | `ctx.rpc` | Register RPC functions, broadcast, shared state, streaming channels |
244
- | `ctx.views` | Serve static files via `hostStatic(base, distDir)` |
245
- | `ctx.diagnostics` | Structured diagnostics host (nostics) - register custom error codes |
246
- | `ctx.agent` | Expose tools + resources to coding agents |
247
- | `ctx.host` | Runtime abstraction - `mountStatic`, `resolveOrigin`, `getStorageDir` |
248
- | `ctx.mode` | `'dev'` or `'build'` - gate setup work per runtime |
249
-
250
- > Hosts can augment `ctx` with additional surfaces (e.g. Vite DevTools' `docks`, `terminals`, `messages`, `commands`). Consult the host's docs - for Vite DevTools, see the [`vite-devtools-kit` skill](../../skills/vite-devtools-kit).
301
+ A [scoped context](#scoped-context-preferred) applies this prefix for RPC / shared-state / streaming. Dock and command IDs are host-level (not part of the scoped `rpc` surface) - prefix those by hand.
251
302
 
252
303
  ## RPC contracts
253
304
 
305
+ Built on [birpc](https://github.com/antfu/birpc), validated at runtime against any [Standard Schema](https://standardschema.dev/) validator (valibot, zod, arktype, …). Devframe forces no validator - install whichever you prefer. First-party `@devframes/*` code stays validator-neutral and uses the built-in zero-dep `devframe/utils/simple-schema` builder; for your own tool, valibot is the lightest default, or reuse zod if you already ship it.
306
+
254
307
  ```ts
255
308
  import { defineRpcFunction } from 'devframe'
256
309
  import * as v from 'valibot'
257
310
 
258
311
  const getModules = defineRpcFunction({
259
- name: 'get-modules', // bare - registered via `ctx.scope('my-inspector').rpc.register`
312
+ name: 'get-modules', // bare — registered via `ctx.scope('my-inspector').rpc.register`
260
313
  type: 'query',
261
314
  jsonSerializable: true,
262
315
  args: [v.object({ limit: v.number() })],
@@ -269,27 +322,23 @@ const getModules = defineRpcFunction({
269
322
 
270
323
  | Type | Use when | Cached | Static dump |
271
324
  |------|----------|--------|-------------|
272
- | `'static'` | Data constant for a given input - dump at build time | Indefinitely | Automatic |
325
+ | `'static'` | Data constant for a given input — dump at build time | Indefinitely | Automatic |
273
326
  | `'query'` | Read that may change; optional `dump` for build adapters | Opt-in via `cacheable` | Manual |
274
327
  | `'action'` | Server-state mutation | Never | Never |
275
328
  | `'event'` | Fire-and-forget; no response | Never | Never |
276
329
 
277
- Add valibot schemas when the RPC is user-facing, when you want static dumps, or when you expose it to agents. Prefer a **single object arg** (`args: [v.object({ ... })]`) over positional args - property names self-document and agents rely on them.
330
+ Declared `args` / `returns` schemas are **enforced at runtime** - a failing call is rejected with `DF0043` / `DF0044`. Prefer a **single object arg** (`args: [v.object({ ... })]`) over positional args - property names self-document and agents rely on them.
278
331
 
279
332
  ### `jsonSerializable` (wire + dump format)
280
333
 
281
- `jsonSerializable` declares the on-wire / on-disk shape contract:
282
-
283
334
  | Value | Encoder | Wire prefix | Round-trips |
284
335
  |-------|---------|-------------|-------------|
285
336
  | `false` (default) | `structured-clone-es` | `s:` | `Map`, `Set`, `Date`, `BigInt`, cycles, class instances |
286
337
  | `true` (opt-in) | strict `JSON.stringify` | _(unprefixed)_ | JSON-only |
287
338
 
288
- Set `jsonSerializable: true` when your handler returns plain JSON shapes - the strict serializer **throws `DF0020`** synchronously on the offending call when it sees a value JSON cannot round-trip (Map/Set/Date/BigInt/class instance/`undefined`-in-array). Errors surface in dev next to the call that introduced them, not silently at build time.
339
+ Set `jsonSerializable: true` when your handler returns plain JSON - the strict serializer **throws `DF0020`** synchronously on the offending call when a value can't round-trip through JSON, surfacing next to the call in dev. `agent: {...}` requires `jsonSerializable: true` (registration throws `DF0019` otherwise) - MCP tools speak JSON.
289
340
 
290
- `agent: {...}` requires `jsonSerializable: true` (registration throws `DF0019` otherwise). MCP tools speak JSON - opting into the agent surface is also opting into JSON-only data.
291
-
292
- Through the scope, `my.rpc.broadcast({ method, args, optional?, event?, filter? })` pushes to every connected client (method name namespaced), and `my.rpc.call(name, ...args)` invokes a server function locally without going through transport (the scoped form of `ctx.rpc.invokeLocal`, useful for cross-function composition).
341
+ Through the scope, `my.rpc.broadcast({ method, args, optional?, event?, filter? })` pushes to every connected client (method name namespaced), and `my.rpc.call(name, ...args)` invokes a server function locally without transport (the scoped form of `ctx.rpc.invokeLocal`, for cross-function composition).
293
342
 
294
343
  ## Shared state
295
344
 
@@ -305,13 +354,13 @@ state.mutate((draft) => {
305
354
  })
306
355
  ```
307
356
 
308
- - Values must be serializable - no functions, no circular refs.
357
+ - Values must be serializable — no functions, no circular refs.
309
358
  - Mutations round-trip to all clients; the host tracks `syncIds` to avoid replay loops.
310
359
  - Prefer shared state over ad-hoc RPC events for UI that must reappear after reconnect.
311
360
 
312
361
  ## Streaming channels
313
362
 
314
- For chunk-style data flowing in either direction - LLM deltas, log tails, build progress, file uploads, mic / screen-share frames - use a streaming channel instead of inventing `action + delta/end` events. The same `channel` object handles both directions:
363
+ For chunk-style data in either direction - LLM deltas, log tails, build progress, uploads - use a streaming channel instead of inventing `action + delta/end` events.
315
364
 
316
365
  ```ts
317
366
  const my = ctx.scope('my-inspector')
@@ -319,194 +368,183 @@ const channel = my.rpc.streaming.create<string>('tokens', { // -> my-inspector:t
319
368
  replayWindow: 256, // server keeps last N chunks per stream id
320
369
  closedStreamRetention: 30_000, // ms to hold finished streams for late subscribers
321
370
  })
322
- ```
323
371
 
324
- ### Server-to-client (the common case)
325
-
326
- ```ts
327
- // Server - typically inside an action handler that returns the stream id
372
+ // Server — typically inside an action handler that returns the stream id
328
373
  const stream = channel.start({ id: 'optional-stream-id' })
329
374
  stream.write(token) // imperative
330
- stream.error(err) // terminal failure
331
- stream.close() // terminal success
332
- stream.signal // AbortSignal - flips when consumers cancel or all subscribers drop
333
- stream.writable // WritableStream<T> for `pipeTo`-style consumption
375
+ stream.close() // terminal success; stream.error(err) for terminal failure
376
+ stream.signal // AbortSignal — flips when consumers cancel or all subscribers drop
377
+ await channel.pipeFrom(sourceReadable) // start + pipe in one call
334
378
 
335
- // Convenience - start + pipe in one call:
336
- await channel.pipeFrom(sourceReadable, { id: 'optional' })
337
-
338
- // Client - my = (await connectDevframe()).scope('my-inspector')
339
- const reader = my.rpc.streaming.subscribe<string>('tokens', streamId) // -> my-inspector:tokens
379
+ // Client — my = (await connectDevframe()).scope('my-inspector')
380
+ const reader = my.rpc.streaming.subscribe<string>('tokens', streamId)
340
381
  for await (const token of reader) renderToken(token)
341
- // Or: reader.readable.pipeTo(domWritable)
342
382
  reader.cancel() // server `stream.signal` aborts
343
383
  ```
344
384
 
345
- ### Client-to-server uploads
385
+ The same channel exposes `openInbound()` for the server side of a client→server upload; pair it with an action that returns the id, and the client drives `my.rpc.streaming.upload<T>('files', uploadId)`. Web Streams are the canonical surface (Node 17+ ships `Readable.fromWeb` / `Writable.fromWeb` converters). Producers should poll `stream.signal.aborted` and exit cooperatively.
386
+
387
+ **Streaming vs events vs shared state:** streaming for token/chunk feeds, uploads, per-call lifecycles with cancellation, and replay-on-reconnect; `event`-typed RPC for payload-free notifications and fire-and-forget signals; shared state for long-lived UI that survives reconnect. For chat UIs, keep the conversation log in shared state and stream active responses - working example: [`examples/streaming-chat`](https://github.com/devframes/devframe/tree/main/examples/streaming-chat).
346
388
 
347
- The same channel exposes `openInbound()` for the server side of a client→server upload. Pair it with a normal action that returns the id:
389
+ ## Agent-native surface
390
+
391
+ Once a tool has a structured boundary, its visual panel is no longer the only interface: the same internal state and capabilities are consumable programmatically by coding agents, sharing one source of truth. RPC functions stay **private by default** and explicitly opt into agent exposure with an `agent` field. Agent-exposed functions must declare `jsonSerializable: true`.
348
392
 
349
393
  ```ts
350
- // Server - my = ctx.scope('my-inspector')
351
- my.rpc.register(defineRpcFunction({
352
- name: 'upload-file', // -> my-inspector:upload-file
353
- type: 'action',
354
- args: [v.object({ name: v.string() })],
355
- returns: v.object({ uploadId: v.string() }),
356
- handler: async ({ name }) => {
357
- const reader = channel.openInbound()
358
- ;(async () => {
359
- for await (const chunk of reader) saveChunk(chunk)
360
- })()
361
- return { uploadId: reader.id }
394
+ defineRpcFunction({
395
+ name: 'get-stats',
396
+ type: 'query',
397
+ jsonSerializable: true,
398
+ args: [v.object({ limit: v.number() })],
399
+ returns: v.object({ count: v.number() }),
400
+ agent: {
401
+ description: 'Return the top-N module stats. Safe to call freely.',
402
+ // safety inferred from type: 'query' → 'read'
362
403
  },
363
- }))
364
-
365
- // Client - my = (await connectDevframe()).scope('my-inspector')
366
- const { uploadId } = await my.rpc.call('upload-file', { name: 'foo' })
367
- const upload = my.rpc.streaming.upload<Uint8Array>('files', uploadId) // -> my-inspector:files
368
- fileReadable.pipeTo(upload.writable, { signal: upload.signal })
404
+ setup: () => ({ handler: async ({ limit }) => ({ count: limit }) }),
405
+ })
369
406
  ```
370
407
 
371
- Client disconnect surfaces as `UploadDisconnected` to the server's `for await`. Server-side `reader.cancel()` broadcasts `upload-cancel` to the uploading session, flipping `upload.signal`.
408
+ Or register tools / resources directly on `ctx.agent.registerTool({ id, description, safety, handler })` and `ctx.agent.registerResource({ id, name, mimeType, read })`. Expose the surface over MCP:
372
409
 
373
- ### Lifecycle
410
+ ```ts
411
+ import { createMcpServer } from 'devframe/adapters/mcp'
374
412
 
375
- | Event | Server | Client |
376
- |-------|--------|--------|
377
- | `stream.close()` / `stream.error(err)` | broadcasts `end` to subscribers | `for await` resolves / throws |
378
- | `reader.cancel()` (last subscriber) | `stream.signal` aborts | reader marked cancelled |
379
- | WS disconnects (last subscriber drops) | `stream.signal` aborts | reader auto-resubscribes after re-trust |
413
+ await createMcpServer(devframe, { transport: 'stdio' })
414
+ ```
380
415
 
381
- Producers should always poll `stream.signal.aborted` and exit cooperatively.
416
+ `@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-devframe mcp` (route host logs to stderr - stdout is the transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that agent clients use to prompt for confirmation. In a hub, `ctx.commands` entries opt into the same agent surface with an `agent` field and reach MCP through the aggregate endpoint.
382
417
 
383
- ### Web / Node Streams interop
418
+ ## Author SPA
384
419
 
385
- Web Streams are the canonical surface. Node 17+ ships free converters:
420
+ Authors bring their own SPA (any framework or plain HTML). The client code is **byte-identical** whether the tool runs standalone, embedded, or inside a hub - that is the portability promise.
386
421
 
387
422
  ```ts
388
- import { Readable, Writable } from 'node:stream'
423
+ import { connectDevframe } from 'devframe/client'
389
424
 
390
- sourceNodeReadable.pipe(Writable.fromWeb(stream.writable))
391
- Readable.fromWeb(reader.readable).pipe(targetNodeWritable)
425
+ const client = await connectDevframe()
426
+ const my = client.scope('my-inspector') // preferred — namespaced calls
427
+ const data = await my.rpc.call('get-stats', { limit: 10 })
392
428
  ```
393
429
 
394
- ### When to use streaming vs events vs shared state
430
+ `connectDevframe` auto-detects the backend via `./__connection.json`, resolved relative to the executing script's runtime base (so the SPA never hardcodes its mount path - build with `vite.base: './'`):
431
+
432
+ - **websocket** (dev mode) — full read/write, requires the auth handshake. `await client.ensureTrusted()` blocks until the server accepts; listen for token updates on the `devframe-auth` BroadcastChannel.
433
+ - **static** (build output) — read-only, resolves calls from the baked RPC dump.
395
434
 
396
- | Use streaming for | Use `event`-typed RPC for | Use shared state for |
397
- |-------------------|---------------------------|----------------------|
398
- | Token / chunk feeds, uploads | Notifications without payload (`refresh`) | Long-lived UI state that survives reconnect |
399
- | Per-call lifecycles with cancellation | Cross-cutting fire-and-forget signals | Diff-based sync between clients |
400
- | Replay on reconnect | | |
435
+ Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(...)` to receive server broadcasts, `my.rpc.callOptional(...)` when a missing handler should resolve to `undefined`, and `my.settings.{project,global}` for persisted settings synced from the server.
401
436
 
402
- For chat-style UIs that combine both: keep the **conversation log** in shared state (survives reconnects), and use a streaming channel for **active responses**. The action that starts a response appends a placeholder to shared state; on producer close, commit the joined content back to shared state. Working example: [`examples/streaming-chat`](https://github.com/devframes/devframe/tree/main/examples/streaming-chat).
437
+ ## The Hub
403
438
 
404
- ## Mounting into a host (Vite DevTools example)
439
+ A single devframe is one portable tool; a hub is where many tools **meet and collaborate**. `@devframes/hub` is the framework-neutral composition layer. It adds the orchestration subsystems that only make sense when tools share a UI, and it ships no UI of its own - a viewer product fills the `ui` slot.
405
440
 
406
- A portable devframe can be mounted into any host that ships an adapter for it. Vite DevTools is one supported target - `createPluginFromDevframe` is the Vite-DevTools-specific bridge; other hosts can implement equivalent factories. Example:
441
+ `initHub()` puts the whole collection behind one standard handler with the same surface and mount snippets as `initDevframe`:
407
442
 
408
443
  ```ts
409
- // vite.config.ts
410
- import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
411
- import myInspector from './my-inspector'
412
-
413
- export default {
414
- plugins: [
415
- createPluginFromDevframe(myInspector, {
416
- // Optional kit-only setup - runs after the auto-derived dock entry.
417
- setup(kitCtx) {
418
- kitCtx.commands.register({
419
- id: 'my-inspector:clear-cache',
420
- title: 'Clear Cache',
421
- handler: () => { /* ... */ },
422
- })
423
- },
424
- }),
425
- ],
426
- }
444
+ import { createUi } from '@devframes/hub-ui'
445
+ import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate'
446
+ import createInspectDevframe from '@devframes/plugin-inspect'
447
+ import createTerminalsDevframe from '@devframes/plugin-terminals'
448
+
449
+ export const hub = initHub({
450
+ base: DEVFRAMES_HUB_BASE, // required — the conventional `/__devframes/`
451
+ devframes: [createInspectDevframe(), createTerminalsDevframe()],
452
+ ui: createUi(), // reference viewer + floating dock; `ui: false` for headless
453
+ configure(ctx) {
454
+ ctx.commands.register({ id: 'app:hello', title: 'Hello', handler: () => 'hi' })
455
+ },
456
+ })
457
+
458
+ hub.handler // the whole devtools ecosystem as Request -> Response
427
459
  ```
428
460
 
429
- The kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`. For dock variations (custom-render, launcher, action, json-render), terminals, palette commands, and toasts, use the `options.setup` hook - those APIs live on the host-augmented context, not on the devframe-level `setup`. See the [`vite-devtools-kit` skill](../../skills/vite-devtools-kit) for the Vite-DevTools-specific reference.
461
+ Every mounted devframe runs its `setup()` against **one shared hub context**: a merged RPC registry (frames can call each other's functions), one shared-state store, one WebSocket transport, and one auth gate. Frame ids become URL segments (`<base><id>/`) and are validated (reserved → `DF8000`, non-route-safe → `DF8004`).
430
462
 
431
- ## When clauses
463
+ ### Hub subsystems
432
464
 
433
- Gate kit-side dock / command visibility with VS Code-style expressions. The runtime + types ship bundled from `devframe/utils/when` - no separate install. The consumers (`when` field on docks and commands) live in the kit:
465
+ A hub-aware `DevframeHubContext` extends `DevframeNodeContext` with four subsystems:
434
466
 
435
- ```ts
436
- when: 'clientType == embedded'
437
- when: 'dockOpen && !paletteOpen'
438
- when: 'my-inspector.ready && count >= 10'
439
- ```
467
+ | Subsystem | Surface | Purpose |
468
+ |-----------|---------|---------|
469
+ | `ctx.docks` | `register / update / values / activate` | Dock entries (iframe, launcher, custom-render, group, and opt-in types) and cross-iframe activation. |
470
+ | `ctx.terminals` | `register / startChildProcess` | Aggregate terminal sessions, stream output over a well-known channel. |
471
+ | `ctx.messages` | `add / update / remove / clear` | Server-side toast/notification queue (FIFO, capped 1000). |
472
+ | `ctx.commands` | `register / execute / list` | Hierarchical command palette with keybindings and `when` clauses. |
440
473
 
441
- Built-in context: `clientType` (`'embedded' | 'standalone'`), `dockOpen`, `paletteOpen`, `dockSelectedId`. Plugins can add namespaced keys (`.` or `:` separators). Both the types (`WhenExpression<Ctx, S>`) and runtime (`evaluateWhen`, `resolveContextValue`) come from `devframe/utils/when`.
474
+ The dock union is **open** - opt-in integrations contribute their own entry types (e.g. JSON-Render adds a `json-render` dock type with no JSON-render dependency in the hub). `ctx.docks.activate(dockId, params?)` steers which dock the viewer shows; from a mounted iframe, `rpc.call('hub:docks:activate', { dockId, params })` does the same cross-iframe. A `type: 'launcher'` dock binds a command, streams a `digest` line, and jumps to its terminal session - the pattern that lets an analyzer spawn `vite build` and navigate the user to its output.
442
475
 
443
- ## Agent-native surface
476
+ ### Mounting into a hub
444
477
 
445
- Opt an RPC function into the agent surface with an `agent` field - default-deny otherwise. Agent-exposed functions **must declare `jsonSerializable: true`** (registration throws `DF0019` otherwise):
478
+ `ctx.install(def)` is the framework-neutral primitive - it registers any `DevframeDefinition` as a dock and runs its `setup(ctx)` - and the imperative counterpart to `initHub`'s declarative `devframes` list:
446
479
 
447
480
  ```ts
448
- defineRpcFunction({
449
- name: 'my-inspector:get-stats',
450
- type: 'query',
451
- jsonSerializable: true,
452
- args: [v.object({ limit: v.number() })],
453
- returns: v.object({ count: v.number() }),
454
- agent: {
455
- description: 'Return the top-N module stats. Safe to call freely.',
456
- // safety inferred from type: 'query' → 'read'
457
- },
458
- setup: () => ({ handler: async ({ limit }) => ({ count: limit }) }),
459
- })
481
+ import { createHubContext } from '@devframes/hub/node'
482
+
483
+ const ctx = await createHubContext({ cwd, host, mode: 'dev' })
484
+ await ctx.install(myDevframe)
460
485
  ```
461
486
 
462
- Or register tools / resources directly:
487
+ When a devframe sharing an already-mounted `id` is installed, its `duplicationStrategy` (`'warn'` default / `'silent'` / `'throw'` / `'duplicate'`) decides the outcome.
463
488
 
464
- ```ts
465
- ctx.agent.registerTool({
466
- id: 'my-inspector:summarize',
467
- description: 'Plain-text summary of the current scan.',
468
- safety: 'read',
469
- handler: async () => ({ markdown: buildSummary() }),
470
- })
489
+ ### The protocol — what a viewer sees
471
490
 
472
- ctx.agent.registerResource({
473
- id: 'current-scan',
474
- name: 'Current scan',
475
- mimeType: 'text/markdown',
476
- read: () => ({ text: renderMarkdown(currentScan) }),
477
- })
478
- ```
491
+ A hub-aware UI imports no hub classes; it reads shared-state keys and one RPC method:
479
492
 
480
- Expose via MCP:
493
+ | Channel | Type | Carries |
494
+ |---------|------|---------|
495
+ | `devframe:docks` (shared state) | `DevframeDockEntry[]` | Every registered dock entry. |
496
+ | `devframe:commands` (shared state) | `DevframeServerCommandEntry[]` | Serializable command list (handlers stripped). |
497
+ | `devframe:docks:active` (shared state) | `DevframeDocksActiveState` | Most recent dock-activation request. |
498
+ | `hub:commands:execute` (RPC) | `(id, ...args) => unknown` | Server-side command dispatch. |
499
+ | `hub:docks:activate` (RPC) | `({ dockId, params? }) => void` | Switch the active dock from any client. |
481
500
 
482
- ```ts
483
- import { createMcpServer } from 'devframe/adapters/mcp'
501
+ Plus broadcasts (`devframe:docks:activate`, `devframe:terminals:updated`, `devframe:messages:updated`). The hub also ships a headless browser runtime, `createDevframeClientHost()` from `@devframes/hub/client`: booted in the host page, it assembles the shared client context from this protocol and imports each dock entry's client script into that page - how a plugin like the a11y inspector runs code inside the page being inspected.
484
502
 
485
- await createMcpServer(devframe, { transport: 'stdio' })
486
- ```
503
+ ### The `ui` slot
487
504
 
488
- `@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-devframe mcp` - route host logs to stderr (stdout is the MCP transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that agent clients use to prompt for confirmation.
505
+ The hub is headless; `DevframeHubUi` is pure data (`viewer` / `embedded` / `assets` / `setup`). `@devframes/hub-ui`'s `createUi()` is the reference implementation - a standalone viewer plus a floating dock injected via one `<script type="module" src="/__devframes/embedded.js">` tag. It takes `branding`, `dockPreferences`, and `embeddedVisibility` (`'normal'` / `'passive'` / `'hidden'`). A viewer product supplies a different object to the same slot and reuses all the infrastructure. Renderer modules for opt-in dock types compose at the hub via `initHub({ renderers })`.
489
506
 
490
- ## Author SPA
507
+ Two copyable reference hubs mount every built-in plugin behind an icon dock - the shape Vite DevTools wears, shrunk to the smallest thing you can build your own viewer from: [`examples/hub-vite/`](https://github.com/devframes/devframe/tree/main/examples/hub-vite) (~120-line Vite plugin host, vanilla DOM UI) and [`examples/hub-next/`](https://github.com/devframes/devframe/tree/main/examples/hub-next) (Next.js App Router). The `hub-*-minimal` family shows the default `createUi()` mount across Vite, Next, Nitro, Hono, Fastify, SvelteKit, Deno, and Rsbuild.
491
508
 
492
- Authors bring their own SPA (any framework or plain HTML). Client entry:
509
+ ## Framework packages: two scopes
493
510
 
494
- ```ts
495
- import { connectDevframe } from 'devframe/client'
511
+ `@devframes/vite`, `@devframes/next`, and `@devframes/nuxt` each split into two clearly-scoped subpaths; the bare root import throws with a pointer to both.
496
512
 
497
- const client = await connectDevframe()
498
- // await client.ensureTrusted() // WS mode only - blocks until server accepts
499
- const my = client.scope('my-inspector') // preferred - namespaced calls
513
+ - **`.../single`** — build & dev-serve one devframe's SPA with that tool.
514
+ - Vite: `devframeVitePlugin` (static mount) / `devframeViteBridge` (RPC bridge) / `devframeVite` (wrapper), from `@devframes/vite/single`.
515
+ - Next: `withDevframe` (config) + `createDevframeNextHandler` (route handler), plus a React client at `@devframes/next/single/client` (`RpcProvider`, `useRpc`, `useRpcStatus`).
516
+ - Nuxt: `modules: ['@devframes/nuxt/single']`.
517
+ - **`.../hub`** — mount a whole `@devframes/hub` inside that tool. Wraps `initHub`, defaults the UI to `createUi()`, ships a browser client helper at `.../hub/client`.
518
+ - `viteDevframeHub()` (shares Vite's server, injects the dock), `nextDevframeHub()` (side-car socket, App Router route), the Nuxt hub module.
519
+ - `@devframes/hub` and `@devframes/hub-ui` are optional peers of these packages.
500
520
 
501
- const data = await my.rpc.call('get-stats', { limit: 10 })
521
+ Vite and Nuxt already have native hub viewers ([Vite DevTools](https://devtools.vite.dev/), [Nuxt DevTools](https://devtools.nuxt.com/)), so `@devframes/vite/hub` and `@devframes/nuxt/hub` print a one-time recommendation to prefer those (silence with `{ quiet: true }`); `@devframes/next/hub` has no native counterpart and stays quiet.
522
+
523
+ For Vite DevTools specifically, `createPluginFromDevframe(def, opts?)` from `@vitejs/devtools-kit/node` adapts a definition into the kit's plugin interface (`{ name, devtools: { setup, capabilities } }`), auto-deriving an iframe dock entry from `id` / `name` / `icon` / `basePath`. Pass `options.setup` for richer host-side behaviour (custom-render docks, terminals, palette commands) on the kit-augmented context. The factory lives in the kit, not devframe, so devframe stays free of any `@vitejs/*` dependency.
524
+
525
+ ## When clauses
526
+
527
+ Gate host-side dock / command visibility with VS Code-style expressions. The runtime + types ship bundled from `devframe/utils/when` - no separate install.
528
+
529
+ ```ts
530
+ when: 'clientType == embedded'
531
+ when: 'dockOpen && !paletteOpen'
532
+ when: 'my-inspector.ready && count >= 10'
502
533
  ```
503
534
 
504
- `connectDevframe` auto-detects the backend via `/.devframe/.connection.json`:
535
+ Built-in context: `clientType` (`'embedded' | 'standalone'`), `dockOpen`, `paletteOpen`, `dockSelectedId`. Plugins add namespaced keys (`.` or `:` separators). `when` clauses evaluate **client-side only** and are not enforced for agent calls - only gate a command with `when` if running it outside its UI context is safe.
505
536
 
506
- - **websocket** (dev mode) - full read/write, requires auth handshake. Listen for token updates on the `devframe-auth` BroadcastChannel.
507
- - **static** (build output) - read-only, resolves calls from the baked RPC dump.
537
+ ## CLI adapter subcommands
508
538
 
509
- Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(defineRpcFunction(...))` to receive server broadcasts, `my.rpc.callOptional(...)` when a missing handler should resolve to `undefined` instead of throwing, and `my.settings.{project,global}` for persisted per-workspace / per-user settings synced from the server.
539
+ `createCac(devframe).parse()` gives three subcommands out of the box:
540
+
541
+ | Subcommand | Action |
542
+ |------------|--------|
543
+ | *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `cli.distDir` served at the base |
544
+ | `build` | Static snapshot → `./dist-static/` (`--out-dir`) |
545
+ | `mcp` | stdio MCP server |
546
+
547
+ **Bring your own CLI framework?** `createCac` is a thin cac wrapper around three peer factories - `createDevServer` (`devframe/adapters/dev`), `createBuild` (`devframe/adapters/build`), `createMcpServer` (`devframe/adapters/mcp`). Use them directly with commander/yargs/oclif. `cac` is an optional peer pulled in only through `devframe/adapters/cac`. `createDevServer` returns a `StartedServer` handle (`origin`, `port`, `app`, `ws?`, `close()`) for SIGINT / hot-reload teardown. `parseCliFlags(schema, raw)` and `defineCliFlags(...)` validate an arbitrary flag bag.
510
548
 
511
549
  ## Build dumps
512
550
 
@@ -514,7 +552,7 @@ Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(defineRpcFu
514
552
 
515
553
  ```ts
516
554
  defineRpcFunction({
517
- name: 'my-inspector:get-session',
555
+ name: 'get-session',
518
556
  type: 'query',
519
557
  setup: () => ({
520
558
  handler: async (id: string) => loadSession(id),
@@ -526,19 +564,7 @@ defineRpcFunction({
526
564
  })
527
565
  ```
528
566
 
529
- At runtime, static clients look up the argument hash in the dump; misses resolve to `fallback` (or throw if absent).
530
-
531
- ## CLI adapter subcommands
532
-
533
- `createCac(devframe).parse()` gives the tool three subcommands out of the box:
534
-
535
- | Subcommand | Action |
536
- |------------|--------|
537
- | *(default)* | Dev server on port 9999 (or `--port`) - WebSocket RPC, `cli.distDir` served at `/.devframe/` |
538
- | `build` | Static snapshot → `./dist-static/` (configurable via `--out-dir`) |
539
- | `mcp` | stdio MCP server |
540
-
541
- **Bring your own CLI framework?** `createCac` (`devframe/adapters/cac`) is just a cac wrapper around three peer factories - `createDevServer` (`devframe/adapters/dev`), `createBuild` (`devframe/adapters/build`), and `createMcpServer` (`devframe/adapters/mcp`). Use them directly with commander/yargs/oclif when `createCac`'s baked-in command structure doesn't fit. `cac` is an optional peer dependency pulled in only through `devframe/adapters/cac`, so bring-your-own-CLI tools run without installing it. `createDevServer` returns a `StartedServer` handle (`origin`, `port`, `app`, `wss`, `close()`) so you can wire SIGINT / hot-reload teardown into the surrounding program. `parseCliFlags(schema, raw)` and `defineCliFlags(...)` (both from `devframe/adapters/cac`) validate an arbitrary flag bag against a `CliFlagsSchema` - the helpers are framework-agnostic.
567
+ At runtime, static clients look up the argument hash in the dump; misses resolve to `fallback` (or throw if absent). To bake an RPC this devframe doesn't own (e.g. a wire service's), declare it under the definition's `rpc.snapshot`.
542
568
 
543
569
  ## Bundled utilities
544
570
 
@@ -546,32 +572,31 @@ Devframe re-exports a curated set of helpers under `devframe/utils/*`. They are
546
572
 
547
573
  | Import | Wraps | Use for |
548
574
  |--------|-------|---------|
549
- | `colors` from `devframe/utils/colors` | `ansis` | Terminal ANSI colors (`c.red`, `c.green`, tagged templates) |
550
- | `open` from `devframe/utils/open` | `open` | Open URLs / files in the OS default handler |
551
- | `launchEditor` from `devframe/utils/launch-editor` | `launch-editor` | Open `file:line:column` in the user's editor (optional `editor` arg) |
552
- | `hash` from `devframe/utils/hash` | `ohash` | Stable structural hash - cache keys, dedup |
553
- | `structuredClone{Serialize,Deserialize,Stringify,Parse}` from `devframe/utils/structured-clone` | `structured-clone-es` | JSON-safe round-trip of `Map` / `Set` / `Date` / `BigInt` / cycles |
575
+ | `colors` from `devframe/utils/colors` | `ansis` | Terminal ANSI colors |
576
+ | `open` from `devframe/utils/open` | `open` | Open URLs / files in the OS handler |
577
+ | `launchEditor` from `devframe/utils/launch-editor` | `launch-editor` | Open `file:line:column` in the user's editor |
578
+ | `hash` from `devframe/utils/hash` | `ohash` | Stable structural hash — cache keys, dedup |
579
+ | `structuredClone{Serialize,Deserialize,Stringify,Parse}` from `devframe/utils/structured-clone` | `structured-clone-es` | JSON-safe round-trip of `Map`/`Set`/`Date`/`BigInt`/cycles |
554
580
  | `nanoid` from `devframe/utils/nanoid` | (vendored) | URL-safe random IDs |
555
581
  | `randomToken` / `randomDigits` / `timingSafeEqual` from `devframe/utils/crypto-token` | (native WebCrypto) | CSPRNG bearer tokens, one-time codes, constant-time compare |
556
- | `createEventEmitter` from `devframe/utils/events` | - | Typed event bus |
557
- | `createSharedState` from `devframe/utils/shared-state` | (immer internal) | Immutable state container (see `ctx.rpc.sharedState`) |
558
- | `createStreamSink` / `createStreamReader` from `devframe/utils/streaming-channel` | - | Low-level streaming primitives |
582
+ | `createEventEmitter` from `devframe/utils/events` | — | Typed event bus |
583
+ | `createSharedState` from `devframe/utils/shared-state` | (immer internal) | Immutable state container |
584
+ | `s` from `devframe/utils/simple-schema` | — | Zero-dep Standard Schema builder (`s.object`, `s.string`, …) for validator-neutral first-party code |
559
585
  | `evaluateWhen` / `WhenExpression` from `devframe/utils/when` | `whenexpr` | When-clause expressions |
560
586
 
561
- For "open file in editor" + "reveal in finder", prefer the `@devframes/service-open` wire service (declare `services: [{ package: '@devframes/service-open' }]` on the definition, gate client UI on `rpc.services.has(...)`) - one host-level installation shared by every plugin, with workspace-root path containment. The older `commonRpcFunctions` recipe (`devframe/recipes/common-rpc-functions`) still works but is deprecated.
587
+ For "open file in editor" + "reveal in finder", prefer the `@devframes/service-open` wire service (declare `services: [{ package: '@devframes/service-open' }]` on the definition, gate client UI on `rpc.services.has(...)`) - one host-level installation shared by every plugin.
562
588
 
563
589
  ## Security (secure by default)
564
590
 
565
- RPC handlers run with the full privileges of the host process, so the boundary that matters is who may connect. Keep that boundary tight:
591
+ RPC handlers run with the full privileges of the host process, so the boundary that matters is who may connect.
566
592
 
567
- - **`auth` defaults to `true`** - dev-mode connections must authenticate before calls are accepted. Devframe ships the node primitives (`exchangeTempAuthCode`, `verifyAuthToken` in `devframe/node/auth`); the host adapter (e.g. Vite DevTools) provides the interactive `devframe:anonymous:auth` + `devframe:auth:exchange` handlers and auth UI.
568
- - **`auth: false` trusts every reachable connection.** Use it only for single-user `localhost` tools. Never combine it with a non-loopback bind host, a tunnel, or a shared/CI environment. The default bind host is already `localhost`.
569
- - **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token via `requestTrustWithCode(code)`. The code is single-use, expires in 5 min, compared in constant time, and rotates after repeated failures - show it only in the terminal, never over the network.
570
- - **Magic-link (optional):** print `buildOtpAuthUrl(origin)` - `<origin>/#devframe_otp=<code>`. The code rides the URL **fragment**, which the browser never sends to the server, so it stays out of access logs and `Referer`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself. The standalone CLI's `--open` does this automatically via `DevframeAuthHandler.buildOpenUrl` - the launched tab already carries the OTP, no prompt needed.
571
- - **Tokens are secrets.** The bearer token rides the WS URL (`?devframe_auth_token=…`) - serve over `wss://`/`https://` beyond loopback. Never log the token or code, never bake them into build output. Revoke via `revokeAuthToken(...)`; clients drop to untrusted on `devframe:auth:revoked`.
572
- - **Authorize handlers.** Any trusted client can call any registered function - validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first.
573
- - **Origin-lock remote docks** (`originLock`, on by default) so a dock's session token is honored only on a connection whose `Origin` matches the dock - the connect-time gate enforces it.
574
- - **The MCP route requires an Origin.** The route-based MCP server (`cli.mcp`, `devframeViteBridge`/Next handler `mcp`, `createMcpFetchHandler`) rejects `Origin`-less requests - a request must carry a loopback (or allow-listed) `Origin`, so it isn't reachable by an arbitrary local process. `devframe connect` sends each instance's own loopback origin automatically.
593
+ - **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. In a **hub**, one gate at the one shared transport covers every frame, the hub built-ins, and the MCP route; mounted frames have no gates of their own.
594
+ - **`auth: false` trusts every reachable connection.** Only for single-user `localhost` tools - never with a non-loopback bind host, a tunnel, or a shared/CI environment.
595
+ - **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token. Single-use, expires in 5 min, constant-time compared, rotates after repeated failures.
596
+ - **Magic-link (optional):** the code rides the URL **fragment** (`#devframe_otp=<code>`), never sent to the server; `connectDevframe` reads, exchanges, and strips it. Only the single-use code ever rides a URL, never the bearer.
597
+ - **Tokens are secrets.** Serve over `wss://` / `https://` beyond loopback. Never log or bake them into build output. Revoke via `revokeAuthToken(...)`.
598
+ - **Authorize handlers.** Any trusted client can call any registered function - validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first. `when` clauses are UI-only and not enforced for agent calls.
599
+ - **The MCP route requires an Origin** — the route-based MCP server rejects `Origin`-less requests, so it isn't reachable by an arbitrary local process.
575
600
 
576
601
  See [Security](https://devfra.me/security) for the full reference.
577
602
 
@@ -579,29 +604,25 @@ See [Security](https://devfra.me/security) for the full reference.
579
604
 
580
605
  - Unit-test host classes with fake contexts.
581
606
  - Run `templates/counter-devframe.ts` under each adapter for integration coverage.
582
- - Snapshot the build-static RPC dump (`<outDir>/.devframe/.rpc-dump/index.json`) to catch accidental drift in `static` function outputs.
607
+ - Snapshot the build-static RPC dump (`<outDir>/.devframe/.rpc-dump/index.json`) to catch drift in `static` function outputs.
583
608
 
584
609
  ## Further reading
585
610
 
586
- Devframe-level pages (one-tool, portable surface):
587
-
588
- - [Devframe Definition](https://devfra.me/devframe-definition) - fields, runtime flags, multi-adapter wiring
589
- - [Scoped Context](https://devfra.me/scoped-context) - `ctx.scope(id)`, auto-namespacing, the `settings` store
590
- - [Adapters](https://devfra.me/adapters) - full reference for all deployment adapters
591
- - [RPC](https://devfra.me/rpc) - types, schema, broadcasts, dumps
592
- - [Shared State](https://devfra.me/shared-state) - patches, events, client-side mutation
593
- - [Streaming](https://devfra.me/streaming) - chunked feeds, uploads, replay, Web/Node Streams interop
594
- - [When Clauses](https://devfra.me/when-clauses) - syntax, context, type-safe wrappers
595
- - [Structured Diagnostics](https://devfra.me/diagnostics) - coded errors via `ctx.diagnostics`, register custom codes
596
- - [Utilities](https://devfra.me/utilities) - bundled `devframe/utils/*` helpers (colors, hash, launchEditor, structured-clone, …)
597
- - [Client](https://devfra.me/client) - auth handshake, modes, discovery
598
- - [Security](https://devfra.me/security) - trust model, authentication, secure-by-default practices
599
- - [Agent-Native](https://devfra.me/agent-native) - agent field, tools/resources, MCP + Claude Desktop
600
-
601
- Host-specific extras (when mounting into Vite DevTools - other hosts have their own equivalents):
602
-
603
- - [Vite DevTools Kit overview](https://devtools.vite.dev/kit/)
604
- - [Dock System](https://devtools.vite.dev/kit/dock-system) - every entry type + remote docks
605
- - [Commands](https://devtools.vite.dev/kit/commands) - palette, keybindings, sub-commands
606
- - [Messages & Notifications](https://devtools.vite.dev/kit/messages) - entry fields, positional hints
607
- - [Terminals](https://devtools.vite.dev/kit/terminals) - child processes, external sessions
611
+ Devframe-level (one portable tool):
612
+
613
+ - [Devframe Definition](https://devfra.me/guide/devframe-definition) — fields, `importMetaUrl`, runtime flags
614
+ - [Initiate (standard handler)](https://devfra.me/adapters/initiate) — `initDevframe`, mounting, the WebSocket binding
615
+ - [Scoped Context](https://devfra.me/guide/scoped-context) — `ctx.scope(id)`, auto-namespacing, `settings`
616
+ - [Adapters](https://devfra.me/adapters/) — cli / dev / build / mcp / embedded
617
+ - [RPC](https://devfra.me/guide/rpc) — types, Standard Schema, broadcasts, dumps
618
+ - [Shared State](https://devfra.me/guide/shared-state) · [Streaming](https://devfra.me/guide/streaming) · [When Clauses](https://devfra.me/guide/when-clauses)
619
+ - [Diagnostics](https://devfra.me/guide/diagnostics) · [Services](https://devfra.me/guide/services) · [Client](https://devfra.me/guide/client)
620
+ - [Security](https://devfra.me/guide/security) · [Agent-Native](https://devfra.me/guide/agent-native)
621
+
622
+ Hub & frameworks (composing many tools):
623
+
624
+ - [Hub](https://devfra.me/guide/hub) — subsystems, docks, protocol, `ctx.install`
625
+ - [Serve a Hub Anywhere](https://devfra.me/guide/hub-initiate) — `initHub`, the `ui` slot, one auth
626
+ - [Client Scripts & Client Context](https://devfra.me/guide/client-context) — `createDevframeClientHost`, dock-script contract
627
+ - [Frameworks](https://devfra.me/frameworks/) — [Vite](https://devfra.me/frameworks/vite) · [Next](https://devfra.me/frameworks/next) · [Nuxt](https://devfra.me/frameworks/nuxt)
628
+ - [Vite DevTools adapter](https://devfra.me/adapters/vite) — `createPluginFromDevframe`