@ggui-ai/cli 0.1.0-rc.3 → 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,62 +19,61 @@
19
19
  * - `console: { sessionCookie: true }` — landing page at `/` +
20
20
  * `/s/<shortCode>` viewer + same-origin HTTP-only cookie flow.
21
21
  * - `shortCodeIndex: new InMemoryShortCodeIndex()` — required by
22
- * the `sessionCookie` flow so `POST /ggui/console/session-cookie`
23
- * can resolve a posted shortCode to `{sessionId, appId}`.
24
- * In-memory is correct for the OSS first-run: session state lives
22
+ * the `sessionCookie` flow so `POST /ggui/console/render-cookie`
23
+ * can resolve a posted shortCode to the bound `{renderId, appId}`.
24
+ * In-memory is correct for the OSS first-run: render state lives
25
25
  * in memory unless the operator opts into sqlite, so a matching
26
26
  * index lifetime is what operators expect.
27
- * - `mcpApps: { renderBaseUrl, wsUrl }` — registers the `ggui_push`
28
- * tool with MCP Apps `_meta.ui.*` declaration, serves
29
- * `ui://ggui/session`, and advertises the `io.modelcontextprotocol/ui`
30
- * capability. Without this the console `/s/<shortCode>` viewer
31
- * is orphaned — there's no tool minting shortCodes. Resolved host +
32
- * port are mandatory so the agent-returned `url` resolves to this
33
- * same server's `/s/` endpoint. See §2.4.1 "sole entry-point tool"
34
- * lock in the OSS-split plan.
27
+ * - `mcpApps: { wsUrl }` — registers the `ggui_render` tool with
28
+ * MCP Apps `_meta.ui.*` declaration, serves `ui://ggui/render`,
29
+ * and advertises the `io.modelcontextprotocol/ui` capability.
30
+ * `wsUrl` is the live-channel URL the spawned iframe subscribes
31
+ * to; resolved host + port are mandatory so it points back at
32
+ * this same process. See §2.4.1 "sole entry-point tool" lock
33
+ * in the OSS-split plan.
35
34
  *
36
35
  * Embedding hosts that want a different shape (no landing page, no
37
36
  * pairing, programmatic control) compose `createGguiServer()` directly
38
37
  * rather than going through `ggui serve`.
39
38
  */
40
- import { createServer as createNetServer } from 'node:net';
41
- import { randomUUID } from 'node:crypto';
42
- import { existsSync } from 'node:fs';
43
- import { composeWiredActionRouterFromMounts, createGguiServer, DEFAULT_BUILDER_APP_ID, FileSystemCodeStore, FixedWindowRateLimiter, InMemoryAuthAdapter, InMemoryQuotaStore, InMemoryShortCodeIndex, selectEmailSenderFromEnv, } from '@ggui-ai/mcp-server';
44
- import { createDeterministicPreviewEmitter } from '@ggui-ai/preview-a2ui/emitters';
45
- import { RUNTIME_BUNDLE_URL_PATH } from '@ggui-ai/iframe-runtime/server';
46
- import { createLocalEmbeddingProvider } from '@ggui-ai/embedding-local';
47
- import { PlaintextFileProviderKeyStore } from '@ggui-ai/mcp-server-core/plaintext';
48
- import { InMemoryAppMetadataStore, InMemoryVectorStore } from '@ggui-ai/mcp-server-core/in-memory';
49
- import { listThemes } from '@ggui-ai/design/themes';
50
- import { createInstalledBlueprintsProvider, } from '@ggui-ai/mcp-server-handlers/session-mutations';
51
- import { compileUiOnDemand } from '@ggui-ai/dev-stack';
52
- import { getCodeCacheDir, getCredentialsFile, getEmbeddingCacheDir, } from './paths.js';
53
- import { readOrMintHexSecret } from './persistent-secrets.js';
54
- import { join } from 'node:path';
39
+ import { listThemes } from "@ggui-ai/design/themes";
40
+ import { compileUiOnDemand } from "@ggui-ai/dev-stack";
41
+ import { createLocalEmbeddingProvider } from "@ggui-ai/embedding-local";
42
+ import { RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
43
+ import { composeWiredActionRouterFromMounts, createGguiServer, DEFAULT_BUILDER_APP_ID, FileSystemCodeStore, FixedWindowRateLimiter, InMemoryAuthAdapter, InMemoryQuotaStore, InMemoryShortCodeIndex, selectEmailSenderFromEnv, } from "@ggui-ai/mcp-server";
44
+ import { InMemoryAppMetadataStore, InMemoryVectorStore } from "@ggui-ai/mcp-server-core/in-memory";
45
+ import { PlaintextFileProviderKeyStore } from "@ggui-ai/mcp-server-core/plaintext";
46
+ import { createInstalledBlueprintsProvider, } from "@ggui-ai/mcp-server-handlers/renders";
47
+ import { createDeterministicPreviewEmitter } from "@ggui-ai/preview-a2ui/emitters";
48
+ import { randomUUID } from "node:crypto";
49
+ import { existsSync } from "node:fs";
50
+ import { createServer as createNetServer } from "node:net";
51
+ import { join } from "node:path";
52
+ import { getCodeCacheDir, getCredentialsFile, getEmbeddingCacheDir } from "./paths.js";
53
+ import { readOrMintHexSecret } from "./persistent-secrets.js";
55
54
  /**
56
55
  * Pick a free localhost TCP port by letting the OS assign one on a
57
56
  * throwaway listener. Close the listener immediately and return the
58
57
  * chosen number. Exported so the CLI (`cli.ts::runServeCommand`) and
59
58
  * the integration tests can resolve `--port 0` to a concrete port
60
59
  * BEFORE constructing the backend — {@link buildMcpServerBackend}
61
- * captures the port in the composed `mcpApps.renderBaseUrl` / `wsUrl`
60
+ * captures the port in the composed `mcpApps.wsUrl` / `runtime.url`
62
61
  * at construction time, so the caller must know it up-front.
63
62
  *
64
63
  * Tiny race window: another process could claim the port between our
65
64
  * `close()` and the caller's `listen()`. The CLI surfaces bind errors
66
65
  * as a fatal exit; the OSS first-run path recovers by re-running.
67
66
  */
68
- export function pickFreePort(host = '127.0.0.1') {
67
+ export function pickFreePort(host = "127.0.0.1") {
69
68
  return new Promise((resolve, reject) => {
70
69
  const probe = createNetServer();
71
- probe.once('error', (err) => {
70
+ probe.once("error", (err) => {
72
71
  probe.close();
73
72
  reject(err);
74
73
  });
75
74
  probe.listen(0, host, () => {
76
75
  const addr = probe.address();
77
- if (addr === null || typeof addr === 'string') {
76
+ if (addr === null || typeof addr === "string") {
78
77
  probe.close();
79
78
  reject(new Error(`pickFreePort: could not resolve a free port (got ${String(addr)})`));
80
79
  return;
@@ -96,15 +95,15 @@ export function buildMcpServerBackend(opts) {
96
95
  if (!Number.isInteger(opts.port) || opts.port <= 0 || opts.port > 65535) {
97
96
  throw new Error(`buildMcpServerBackend: \`port\` must be a concrete TCP port (1..65535), got ${opts.port}. Resolve \`--port 0\` via pickFreePort() before composing.`);
98
97
  }
99
- // `baseUrl` feeds `mcpApps.renderBaseUrl` + `runtime.url`; `wsBaseUrl`
100
- // feeds `mcpApps.wsUrl`. With `--public-base-url`, both derive from
101
- // the operator-provided tunnel URL so the agent emits links that
102
- // resolve from the remote host's perspective. Without it, both fall
103
- // back to the local bind. The shape was validated at flag-parse
104
- // time (`http://`/`https://`, no trailing slash); we just substitute
105
- // the scheme for ws/wss to derive the live-channel URL.
98
+ // `baseUrl` feeds `runtime.url`; `wsBaseUrl` feeds `mcpApps.wsUrl`.
99
+ // With `--public-base-url`, both derive from the operator-provided
100
+ // tunnel URL so the URLs the iframe receives resolve from the remote
101
+ // host's perspective. Without it, both fall back to the local bind.
102
+ // The shape was validated at flag-parse time (`http://`/`https://`,
103
+ // no trailing slash); we just substitute the scheme for ws/wss to
104
+ // derive the live-channel URL.
106
105
  const baseUrl = opts.publicBaseUrl ?? `http://${opts.host}:${opts.port}`;
107
- const wsBaseUrl = baseUrl.replace(/^http(s?):\/\//, 'ws$1://');
106
+ const wsBaseUrl = baseUrl.replace(/^http(s?):\/\//, "ws$1://");
108
107
  // Router over the mount handler bundle. Computed once; absent
109
108
  // (`null`) when no mounts are declared so the server falls through
110
109
  // to agent-routed behavior.
@@ -136,7 +135,7 @@ export function buildMcpServerBackend(opts) {
136
135
  });
137
136
  // Per-IP rate limiter — only bound under `--public-demo` so an
138
137
  // anonymous visitor can't burn the operator's BYOK budget by pumping
139
- // ggui_push. Defaults: 30 generations / 10 minutes per IP. Operators
138
+ // ggui_render. Defaults: 30 generations / 10 minutes per IP. Operators
140
139
  // who need different ceilings construct their own backend
141
140
  // programmatically (calling `createGguiServer({ rateLimiter })`
142
141
  // directly). Skipped under `--dev-allow-all` because that's the
@@ -186,8 +185,8 @@ export function buildMcpServerBackend(opts) {
186
185
  const emailSenderSelection = selectEmailSenderFromEnv();
187
186
  let emailSender;
188
187
  let emailFromAddress;
189
- const defaultFromAddress = 'ggui-serve <noreply@localhost>';
190
- if (emailSenderSelection.kind === 'ok') {
188
+ const defaultFromAddress = "ggui-serve <noreply@localhost>";
189
+ if (emailSenderSelection.kind === "ok") {
191
190
  emailSender = emailSenderSelection.sender;
192
191
  emailFromAddress = emailSenderSelection.fromAddress ?? defaultFromAddress;
193
192
  }
@@ -195,26 +194,26 @@ export function buildMcpServerBackend(opts) {
195
194
  // eslint-disable-next-line no-console -- one-shot boot warning, mirrors banner output
196
195
  console.warn(`[ggui-cli] email-login: ${emailSenderSelection.reason}; falling back to ConsoleEmailSender (magic links print to terminal).`);
197
196
  const fallback = selectEmailSenderFromEnv({
198
- env: { GGUI_EMAIL_SENDER: 'console' },
197
+ env: { GGUI_EMAIL_SENDER: "console" },
199
198
  });
200
- if (fallback.kind !== 'ok') {
199
+ if (fallback.kind !== "ok") {
201
200
  throw new Error(`[ggui-cli] email-login: console fallback selection failed: ${fallback.reason}`);
202
201
  }
203
202
  emailSender = fallback.sender;
204
203
  emailFromAddress = process.env.GGUI_EMAIL_FROM?.trim() || defaultFromAddress;
205
204
  }
206
205
  // Resolve cross-restart HMAC secrets. When the caller declared a
207
- // persistent dir, read-or-mint `bootstrap-secret.hex` +
206
+ // persistent dir, read-or-mint `ws-token-secret.hex` +
208
207
  // `render-signer-secret.hex` (32 bytes / 64 hex chars, 0600). Both
209
208
  // get threaded into `createGguiServer` below so the server stops
210
209
  // minting fresh process-local secrets every boot — the precondition
211
- // for any cached `_meta.ggui.bootstrap.token` or `/r/<code>?sig=&exp=`
212
- // URL surviving a restart. Absent dir = legacy ephemeral behavior.
213
- let persistedBootstrapSecret;
210
+ // for any cached `_meta["ai.ggui/render"].wsToken` surviving a
211
+ // restart. Absent dir = legacy ephemeral behavior.
212
+ let persistedWsTokenSecret;
214
213
  let persistedRenderSignerSecret;
215
214
  if (opts.persistentDir !== undefined) {
216
- persistedBootstrapSecret = readOrMintHexSecret(join(opts.persistentDir, 'bootstrap-secret.hex'));
217
- persistedRenderSignerSecret = readOrMintHexSecret(join(opts.persistentDir, 'render-signer-secret.hex'));
215
+ persistedWsTokenSecret = readOrMintHexSecret(join(opts.persistentDir, "ws-token-secret.hex"));
216
+ persistedRenderSignerSecret = readOrMintHexSecret(join(opts.persistentDir, "render-signer-secret.hex"));
218
217
  }
219
218
  // Resolve mcpInstructions: CLI flag wins over env var. Validated
220
219
  // env-var values pass through to createGguiServer; invalid values
@@ -222,11 +221,11 @@ export function buildMcpServerBackend(opts) {
222
221
  const envInstructions = process.env.GGUI_MCP_INSTRUCTIONS?.trim();
223
222
  let resolvedMcpInstructions = opts.mcpInstructions;
224
223
  if (resolvedMcpInstructions === undefined && envInstructions) {
225
- if (envInstructions === 'default' ||
226
- envInstructions === 'aggressive' ||
227
- envInstructions === 'always' ||
228
- envInstructions === 'minimal' ||
229
- envInstructions === 'off') {
224
+ if (envInstructions === "default" ||
225
+ envInstructions === "aggressive" ||
226
+ envInstructions === "always" ||
227
+ envInstructions === "minimal" ||
228
+ envInstructions === "off") {
230
229
  resolvedMcpInstructions = envInstructions;
231
230
  }
232
231
  else {
@@ -242,11 +241,9 @@ export function buildMcpServerBackend(opts) {
242
241
  // `listThemes()` either way; the file-mode override only affects the
243
242
  // server's bound `theme` (rendered CSS variables), not the
244
243
  // session/handler-side `themeId` chain.
245
- const manifestThemePreset = opts.theme?.source === 'preset' ? opts.theme.preset : undefined;
244
+ const manifestThemePreset = opts.theme?.source === "preset" ? opts.theme.preset : undefined;
246
245
  const appMetadataStore = new InMemoryAppMetadataStore({
247
- ...(manifestThemePreset !== undefined
248
- ? { defaultThemeId: manifestThemePreset }
249
- : {}),
246
+ ...(manifestThemePreset !== undefined ? { defaultThemeId: manifestThemePreset } : {}),
250
247
  // `ggui.json#app.gadgets` declarations (Leaflet, Mapbox, …). When
251
248
  // omitted, the InMemoryAppMetadataStore falls back to
252
249
  // STDLIB_GADGETS from @ggui-ai/protocol so the first-party hooks
@@ -262,11 +259,10 @@ export function buildMcpServerBackend(opts) {
262
259
  ...(opts.publicEnv !== undefined && Object.keys(opts.publicEnv).length > 0
263
260
  ? { defaultPublicEnv: opts.publicEnv }
264
261
  : {}),
265
- // `ggui.json#app.defaultMcpAppsMode`. `'canvas'` makes
266
- // `ggui_new_session` mint a session-scoped iframe + route
267
- // subsequent pushes through the session channel; absent ⇒ inline.
268
- ...(opts.defaultMcpAppsMode !== undefined
269
- ? { defaultMcpAppsMode: opts.defaultMcpAppsMode }
262
+ // `ggui.json#app.defaultDisplayMode`. Stamped per-push as
263
+ // `_meta.ui.displayMode`; controls only host-side presentation.
264
+ ...(opts.defaultDisplayMode !== undefined
265
+ ? { defaultDisplayMode: opts.defaultDisplayMode }
270
266
  : {}),
271
267
  });
272
268
  // Materialize the vectorStore here so the marketplace-install
@@ -292,9 +288,7 @@ export function buildMcpServerBackend(opts) {
292
288
  installedBlueprints: () => entries.flatMap((entry) => {
293
289
  if (entry.manifest.contract === undefined)
294
290
  return [];
295
- const intent = entry.manifest.description ??
296
- entry.manifest.name ??
297
- entry.id;
291
+ const intent = entry.manifest.description ?? entry.manifest.name ?? entry.id;
298
292
  return [
299
293
  {
300
294
  id: entry.id,
@@ -307,21 +301,21 @@ export function buildMcpServerBackend(opts) {
307
301
  compile: async (entry) => {
308
302
  const ui = byId.get(entry.id);
309
303
  if (!ui) {
310
- return { kind: 'missing-entry', tried: [] };
304
+ return { kind: "missing-entry", tried: [] };
311
305
  }
312
306
  const result = await compileUiOnDemand({
313
307
  projectRoot,
314
308
  manifestPath: ui.manifestPath,
315
309
  manifest: ui.manifest,
316
310
  });
317
- if (result.kind === 'ok') {
318
- return { kind: 'ok', code: result.code };
311
+ if (result.kind === "ok") {
312
+ return { kind: "ok", code: result.code };
319
313
  }
320
- if (result.kind === 'missing-entry') {
321
- return { kind: 'missing-entry', tried: result.tried };
314
+ if (result.kind === "missing-entry") {
315
+ return { kind: "missing-entry", tried: result.tried };
322
316
  }
323
317
  return {
324
- kind: 'failure',
318
+ kind: "failure",
325
319
  errors: result.errors.map((m) => m.text),
326
320
  };
327
321
  },
@@ -330,8 +324,7 @@ export function buildMcpServerBackend(opts) {
330
324
  process.stderr.write(`[ggui serve] installed-blueprint ${issue.id}: ${issue.kind}: ${issue.message}\n`);
331
325
  },
332
326
  };
333
- installedBlueprintsProvider =
334
- createInstalledBlueprintsProvider(providerOptions);
327
+ installedBlueprintsProvider = createInstalledBlueprintsProvider(providerOptions);
335
328
  }
336
329
  // Fold the bridge into the generation deps so the push handler +
337
330
  // handshake negotiator both consume it via `MatchBlueprintDeps.
@@ -351,8 +344,8 @@ export function buildMcpServerBackend(opts) {
351
344
  providerKeys,
352
345
  // Seeded per-app metadata store. Threaded so the in-process
353
346
  // singleton is shared across `ggui_list_gadgets`,
354
- // `ggui_list_themes`, `ggui_new_session` (theme default
355
- // resolution), and the handshake (gadgets lookup).
347
+ // `ggui_list_themes`, and the handshake (gadgets + theme default
348
+ // resolution).
356
349
  appMetadataStore,
357
350
  // Theme catalog resolver. Reads from `@ggui-ai/design`'s registry
358
351
  // each call so runtime additions surface without a restart.
@@ -365,29 +358,23 @@ export function buildMcpServerBackend(opts) {
365
358
  // process. Render-signer.secret rides on the `renderSigning`
366
359
  // discriminated union (the `false` shape disables the layer
367
360
  // entirely; we always want it on here, so build the object form).
368
- ...(persistedBootstrapSecret !== undefined
369
- ? { bootstrapSecret: persistedBootstrapSecret }
370
- : {}),
361
+ ...(persistedWsTokenSecret !== undefined ? { wsTokenSecret: persistedWsTokenSecret } : {}),
371
362
  ...(persistedRenderSignerSecret !== undefined
372
363
  ? { renderSigning: { secret: persistedRenderSignerSecret } }
373
364
  : {}),
374
- ...(resolvedMcpInstructions !== undefined
375
- ? { mcpInstructions: resolvedMcpInstructions }
376
- : {}),
365
+ ...(resolvedMcpInstructions !== undefined ? { mcpInstructions: resolvedMcpInstructions } : {}),
377
366
  // `--multi-tenant` flips the `/ggui/console/llm-keys` gate from
378
367
  // admin-token to auth-adapter. Default scope derivation in the
379
368
  // server picks up `userId` / `appId` from the resolved identity;
380
369
  // operators with composite scopes pass their own `providerKeyScope`
381
370
  // by composing `createGguiServer` directly.
382
- ...(opts.multiTenant
383
- ? { providerKeysGate: 'auth-adapter' }
384
- : {}),
371
+ ...(opts.multiTenant ? { providerKeysGate: "auth-adapter" } : {}),
385
372
  // Gate OAuth login routes on publicBaseUrl. Without it the
386
373
  // redirect_uri can't be composed; the admin transport still mounts
387
374
  // so operators can paste credentials in advance.
388
375
  ...(opts.publicBaseUrl ? { publicBaseUrl: opts.publicBaseUrl } : {}),
389
376
  ...(rateLimiter ? { rateLimiter } : {}),
390
- ...(storage.sessionStore ? { sessionStore: storage.sessionStore } : {}),
377
+ ...(storage.renderStore ? { renderStore: storage.renderStore } : {}),
391
378
  // Always pass the materialized vectorStore so the install bridge
392
379
  // and the matcher share state. Passing only `storage.vectors`
393
380
  // (set when the manifest declares sqlite) would, on an absent
@@ -406,16 +393,12 @@ export function buildMcpServerBackend(opts) {
406
393
  ? {
407
394
  threads: {
408
395
  store: storage.threadStore,
409
- ...(storage.threadDurability
410
- ? { durability: storage.threadDurability }
411
- : {}),
396
+ ...(storage.threadDurability ? { durability: storage.threadDurability } : {}),
412
397
  },
413
398
  }
414
399
  : {}),
415
400
  sessionChannel: true,
416
- pairing: opts.keysFile
417
- ? { persistencePath: opts.keysFile }
418
- : true,
401
+ pairing: opts.keysFile ? { persistencePath: opts.keysFile } : true,
419
402
  // Email magic-link login. OSS first-run default mirrors
420
403
  // `provisionalPreview` — wire a sender so magic links can be
421
404
  // delivered. `selectEmailSenderFromEnv` reads
@@ -464,7 +447,7 @@ export function buildMcpServerBackend(opts) {
464
447
  !!process.env.GOOGLE_API_KEY ||
465
448
  !!process.env.OPENROUTER_API_KEY;
466
449
  if (!credsFileExists && !anyEnvKey) {
467
- return '/admin-login?next=%2Fadmin%2Fllm-keys';
450
+ return "/admin-login?next=%2Fadmin%2Fllm-keys";
468
451
  }
469
452
  return null;
470
453
  }
@@ -479,10 +462,10 @@ export function buildMcpServerBackend(opts) {
479
462
  // absent = no welcome page (SPA index handler still owns `/`).
480
463
  ...(opts.welcomePage ? { welcomePage: opts.welcomePage } : {}),
481
464
  // Default to in-memory; caller substitutes a persistent impl
482
- // (SqliteShortCodeIndex under `persistentDir`) so cached
483
- // `/api/bootstrap/<code>` lookups survive a restart. Without
484
- // that, /r/<code> 404s the moment the agent's host (claude.ai)
485
- // replays its cached `_meta.ggui.bootstrap` envelope.
465
+ // (SqliteShortCodeIndex under `persistentDir`) so cached short-code
466
+ // lookups survive a restart. Used by render-URL signing and
467
+ // legacy compat paths; post-R5 the primary mount path is the
468
+ // ui://ggui/render resource (auth-bearer-gated), not shortCodes.
486
469
  shortCodeIndex: opts.shortCodeIndex ?? new InMemoryShortCodeIndex(),
487
470
  // Content-addressable componentCode delivery. The OSS dev default
488
471
  // is filesystem-backed at `~/.ggui/code-cache/` (override via
@@ -495,9 +478,7 @@ export function buildMcpServerBackend(opts) {
495
478
  // found UIs in `blueprints.include`; absent = the server's
496
479
  // `ggui_list_featured_blueprints` returns an empty catalog (the
497
480
  // zero-config default).
498
- ...(opts.blueprintProvider
499
- ? { blueprintProvider: opts.blueprintProvider }
500
- : {}),
481
+ ...(opts.blueprintProvider ? { blueprintProvider: opts.blueprintProvider } : {}),
501
482
  // Manifest-backed UI registry — resolves blueprint ids to
502
483
  // compiled bundles for `ggui_render_blueprint`. Paired with
503
484
  // `blueprintProvider` above (metadata vs. bundle resolution on
@@ -506,9 +487,7 @@ export function buildMcpServerBackend(opts) {
506
487
  // Manifest-declared primitive catalogs. Present when `ggui serve`
507
488
  // resolved `primitives.{packages,local}`; absent = in-memory-only
508
489
  // server with an empty `server.primitiveCatalogs`.
509
- ...(opts.primitiveCatalogs
510
- ? { primitiveCatalogs: opts.primitiveCatalogs }
511
- : {}),
490
+ ...(opts.primitiveCatalogs ? { primitiveCatalogs: opts.primitiveCatalogs } : {}),
512
491
  // Manifest-declared theme. Present when `ggui serve` resolved
513
492
  // `ggui.json#theme`; absent = `createGguiServer` falls back to its
514
493
  // `lightTheme`-backed default internally.
@@ -524,25 +503,18 @@ export function buildMcpServerBackend(opts) {
524
503
  // bootstrap envelope without a restart. Both threaded through
525
504
  // when present; absent = boot-baked behaviour (legacy).
526
505
  ...(opts.themeProvider ? { themeProvider: opts.themeProvider } : {}),
527
- ...(opts.onThemeConfigChange
528
- ? { onThemeConfigChange: opts.onThemeConfigChange }
529
- : {}),
530
- // Register `ggui_push` + serve `ui://ggui/session`. URLs point at
531
- // this server's own console viewer + live channel — same-origin,
532
- // same process. Without this, `ggui_push` is absent and the `/s/`
533
- // viewer can never resolve (nothing mints shortCodes).
506
+ ...(opts.onThemeConfigChange ? { onThemeConfigChange: opts.onThemeConfigChange } : {}),
507
+ // Register `ggui_render` + serve `ui://ggui/render`. The `wsUrl`
508
+ // is the live-channel URL published on `_meta["ai.ggui/render"]`
509
+ // so iframes opened by an MCP Apps host (Claude Desktop, Claude
510
+ // Code, claude.ai) can subscribe back to this same process.
511
+ // First-party hosts (Studio, Portal, console) consume
512
+ // `_meta.ui.resourceUri` instead and never hit the URL directly.
534
513
  mcpApps: {
535
- // The `/r/` route serves the public self-contained shell that
536
- // any MCP Apps host (Claude Desktop, Claude Code, claude.ai)
537
- // can render directly — `structuredContent.url` lands on it
538
- // as a full HTML page. First-party hosts (Studio, Portal,
539
- // console) consume `_meta.ui.resourceUri` and never hit the
540
- // URL directly, so the same prefix works for everyone.
541
- renderBaseUrl: `${baseUrl}/r/`,
542
514
  wsUrl: `${wsBaseUrl}/ws`,
543
515
  },
544
516
  // Task #382 — `runtimeUrl` MUST be absolute. The thin-shell HTML
545
- // served from `ui://ggui/session` is mounted via `srcdoc` in most
517
+ // served from `ui://ggui/render` is mounted via `srcdoc` in most
546
518
  // consumers (`<McpAppIframe>` default path for inline text), which
547
519
  // gives the iframe `about:srcdoc` as its URL — a relative path
548
520
  // like `/_ggui/iframe-runtime.js` resolves against `about:` and the
@@ -550,16 +522,16 @@ export function buildMcpServerBackend(opts) {
550
522
  // URL built from the CLI's own known `baseUrl` makes srcdoc mount
551
523
  // work without operator action. The server still serves the bundle
552
524
  // at `/_ggui/iframe-runtime.js` under `runtimePath`; this overrides
553
- // only the URL published on `_meta.ggui.bootstrap.runtimeUrl`.
525
+ // only the URL published on `_meta["ai.ggui/render"].runtimeUrl`.
554
526
  runtime: { url: `${baseUrl}${RUNTIME_BUNDLE_URL_PATH}` },
555
527
  // OSS first-run default: provisional A2UI preview is on. The
556
528
  // deterministic emitter from `@ggui-ai/preview-a2ui/emitters`
557
- // adapts each push's `story.intent` into a small A2UI surface
529
+ // adapts each render's `story.intent` into a small A2UI surface
558
530
  // (heading → shell → teardown) and streams it on
559
531
  // `_ggui:preview`. Embedded-ui's `/s/<shortCode>` viewer mounts
560
532
  // `<ProvisionalRenderer>` to paint the frames as they arrive,
561
533
  // so operators see a live UI build-up the first time they
562
- // invoke `ggui_push` — before any LLM generation lands on OSS.
534
+ // invoke `ggui_render` — before any LLM generation lands on OSS.
563
535
  //
564
536
  // Hosted + programmatic hosts opt-in by composing
565
537
  // `createGguiServer()` directly and supplying their own
@@ -570,8 +542,8 @@ export function buildMcpServerBackend(opts) {
570
542
  emitter: createDeterministicPreviewEmitter(),
571
543
  },
572
544
  // Generation wiring. Threaded through to
573
- // `defaultHandlers.push.generation` → `createGguiPushHandler`.
574
- // Absent = placeholder mode: push creates sessions + shortCodes
545
+ // `defaultHandlers.render.generation` → `createGguiRenderHandler`.
546
+ // Absent = placeholder mode: render mints renderIds + shortCodes
575
547
  // + preview but does NOT produce componentCode. The CLI's
576
548
  // `runServeCommand` probes BYOK (env → credentials file) at
577
549
  // boot and only supplies this opt on a hit. See
@@ -581,9 +553,7 @@ export function buildMcpServerBackend(opts) {
581
553
  // `/mcp` alongside ggui-native tools. No CLI config loader yet —
582
554
  // programmatic hosts + integration tests compose mounts through
583
555
  // this opt directly.
584
- ...(opts.mcpMounts && opts.mcpMounts.length > 0
585
- ? { mcpMounts: opts.mcpMounts }
586
- : {}),
556
+ ...(opts.mcpMounts && opts.mcpMounts.length > 0 ? { mcpMounts: opts.mcpMounts } : {}),
587
557
  // wiredActionRouter composed from the same mount handlers. With
588
558
  // mounts present, every mounted tool becomes wire-dispatchable
589
559
  // from a generated UI's `actionSpec[name].dispatch.tool` (when
@@ -603,11 +573,11 @@ export function buildMcpServerBackend(opts) {
603
573
  // 'off'`. Unrecognized values fall through to the server's
604
574
  // default. Keeping the parse narrow so a typo yields default
605
575
  // behavior (reject) rather than silent relaxation.
606
- ...(process.env['GGUI_SCHEMA_COMPAT_MODE'] === 'warn' ||
607
- process.env['GGUI_SCHEMA_COMPAT_MODE'] === 'off' ||
608
- process.env['GGUI_SCHEMA_COMPAT_MODE'] === 'reject'
576
+ ...(process.env["GGUI_SCHEMA_COMPAT_MODE"] === "warn" ||
577
+ process.env["GGUI_SCHEMA_COMPAT_MODE"] === "off" ||
578
+ process.env["GGUI_SCHEMA_COMPAT_MODE"] === "reject"
609
579
  ? {
610
- schemaCompatCheck: process.env['GGUI_SCHEMA_COMPAT_MODE'],
580
+ schemaCompatCheck: process.env["GGUI_SCHEMA_COMPAT_MODE"],
611
581
  }
612
582
  : {}),
613
583
  // Wired-tool per-call timeout override (default 30 s in
@@ -624,8 +594,8 @@ export function buildMcpServerBackend(opts) {
624
594
  // Operators that need a different production timeout SHOULD compose
625
595
  // `createGguiServer` directly rather than rely on env coupling.
626
596
  ...(() => {
627
- const raw = process.env['GGUI_WIRED_TIMEOUT_MS'];
628
- if (raw === undefined || raw === '')
597
+ const raw = process.env["GGUI_WIRED_TIMEOUT_MS"];
598
+ if (raw === undefined || raw === "")
629
599
  return {};
630
600
  const parsed = Number(raw);
631
601
  if (!Number.isFinite(parsed) || parsed <= 0)
@@ -637,14 +607,14 @@ export function buildMcpServerBackend(opts) {
637
607
  listen: async (port, host) => {
638
608
  const httpServer = await server.listen(port, host);
639
609
  const addr = httpServer.address();
640
- if (!addr || typeof addr === 'string') {
641
- throw new Error('server.address() returned an unexpected shape');
610
+ if (!addr || typeof addr === "string") {
611
+ throw new Error("server.address() returned an unexpected shape");
642
612
  }
643
613
  return addr.port;
644
614
  },
645
615
  close: () => server.close(),
646
616
  toolCount: server.toolCount,
647
- serverName: 'ggui-mcp-server',
617
+ serverName: "ggui-mcp-server",
648
618
  serverVersion: opts.cliVersion,
649
619
  primitiveCatalogCount: server.primitiveCatalogs.length,
650
620
  themeSource: server.theme.source,
@@ -3,13 +3,13 @@
3
3
  * restart.
4
4
  *
5
5
  * Background: `createGguiServer` mints two HMAC secrets at boot when the
6
- * caller doesn't pass them — `bootstrapSecret` (signs MCP Apps `_meta.
7
- * ggui.bootstrap.token` so iframes can subscribe to the live-channel WS)
8
- * and the render-URL signer secret (signs `/r/<code>?sig=&exp=`). Both
9
- * are process-local `randomBytes(32)` by default — fine for a single
6
+ * caller doesn't pass them — `wsTokenSecret` (signs the
7
+ * `_meta["ai.ggui/render"].wsToken` envelope so iframes can subscribe
8
+ * to the live-channel WS) and the render-URL signer secret. Both are
9
+ * process-local `randomBytes(32)` by default — fine for a single
10
10
  * process lifetime, fatal for "revisit a claude.ai chat history after a
11
- * restart": the cached bootstrap token + cached render URL fail HMAC
12
- * verify against the freshly-minted secret.
11
+ * restart": the cached wsToken fails HMAC verify against the freshly-
12
+ * minted secret.
13
13
  *
14
14
  * Persisting the secrets to a 0600-mode file makes those tokens valid
15
15
  * across restarts, which is the precondition for any rehydrate path
@@ -17,10 +17,10 @@
17
17
  * lowercase hex (32 bytes), no trailing newline, no JSON wrapper. Easy
18
18
  * to inspect, easy to rotate (delete the file, restart, fresh secret).
19
19
  *
20
- * Security posture: these secrets are a session-token signing key, NOT
20
+ * Security posture: these secrets are a render-token signing key, NOT
21
21
  * a long-lived credential like an OAuth client secret. Losing them
22
22
  * means an attacker can mint MCP Apps bootstrap tokens for arbitrary
23
- * `(sessionId, appId)` pairs — same blast radius as before, just
23
+ * `(renderId, appId)` pairs — same blast radius as before, just
24
24
  * surviving restart. Mode 0600 on the file matches `~/.ggui/credentials
25
25
  * .json` (BYOK keys) which has the same threat model.
26
26
  *
@@ -1,5 +1,5 @@
1
- import type { SessionStore, VectorStore } from '@ggui-ai/mcp-server-core';
2
- export declare function createPersistentSessionStore(persistentDir: string): Promise<SessionStore>;
1
+ import type { RenderStore, VectorStore } from '@ggui-ai/mcp-server-core';
2
+ export declare function createPersistentRenderStore(persistentDir: string): Promise<RenderStore>;
3
3
  export declare function createPersistentVectorStore(persistentDir: string): Promise<VectorStore>;
4
4
  /**
5
5
  * Compute the auto-default `keysFile` path. Returns `undefined` when
@@ -1 +1 @@
1
- {"version":3,"file":"persistent-stores.d.ts","sourceRoot":"","sources":["../src/persistent-stores.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAQvB;AAED,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE7D"}
1
+ {"version":3,"file":"persistent-stores.d.ts","sourceRoot":"","sources":["../src/persistent-stores.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED,wBAAsB,2BAA2B,CAC/C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CAQtB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Default-on persistent stores under `<persistentDir>/`. Used when the
3
- * operator hasn't declared `storage.sessions` / `storage.vectors` in
4
- * `ggui.json` AND hasn't passed `--ephemeral`. Closes the SessionStore
3
+ * operator hasn't declared `storage.renders` / `storage.vectors` in
4
+ * `ggui.json` AND hasn't passed `--ephemeral`. Closes the RenderStore
5
5
  * leg of the rehydrate problem: without this, a restart drops every
6
- * session row even though the HMAC secret + ShortCodeIndex now persist.
6
+ * render row even though the HMAC secret + ShortCodeIndex now persist.
7
7
  *
8
8
  * Override precedence (caller enforces):
9
- * 1. Explicit `ggui.json#storage.sessions` / `.vectors` (user wins)
9
+ * 1. Explicit `ggui.json#storage.renders` / `.vectors` (user wins)
10
10
  * 2. These defaults (operator-friendly)
11
11
  * 3. In-memory (`createGguiServer`'s internal fallback) (last resort)
12
12
  *
@@ -16,11 +16,11 @@
16
16
  */
17
17
  import { mkdirSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
- export async function createPersistentSessionStore(persistentDir) {
19
+ export async function createPersistentRenderStore(persistentDir) {
20
20
  mkdirSync(persistentDir, { recursive: true });
21
- const { SqliteSessionStore } = await import('@ggui-ai/mcp-server-core/sqlite');
22
- return new SqliteSessionStore({
23
- filename: join(persistentDir, 'sessions.sqlite'),
21
+ const { SqliteRenderStore } = await import('@ggui-ai/mcp-server-core/sqlite');
22
+ return new SqliteRenderStore({
23
+ filename: join(persistentDir, 'renders.sqlite'),
24
24
  });
25
25
  }
26
26
  export async function createPersistentVectorStore(persistentDir) {