@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.3.0-rc.0

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 (152) hide show
  1. package/dist/admin-blueprints-transport.d.ts.map +1 -1
  2. package/dist/admin-blueprints-transport.js +2 -1
  3. package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
  4. package/dist/admin-oauth-providers-transport.js +7 -5
  5. package/dist/api-renders-routes.d.ts +85 -0
  6. package/dist/api-renders-routes.d.ts.map +1 -0
  7. package/dist/api-renders-routes.js +372 -0
  8. package/dist/build-mcp.d.ts +1 -1
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +39 -5
  11. package/dist/code-routes.d.ts +47 -0
  12. package/dist/code-routes.d.ts.map +1 -0
  13. package/dist/code-routes.js +81 -0
  14. package/dist/code-store-fs.js +2 -2
  15. package/dist/console-auth.d.ts +10 -10
  16. package/dist/console-auth.d.ts.map +1 -1
  17. package/dist/console-auth.js +5 -5
  18. package/dist/console-blueprint-routes.d.ts +71 -0
  19. package/dist/console-blueprint-routes.d.ts.map +1 -0
  20. package/dist/console-blueprint-routes.js +348 -0
  21. package/dist/console-chat-routes.d.ts +80 -0
  22. package/dist/console-chat-routes.d.ts.map +1 -0
  23. package/dist/console-chat-routes.js +182 -0
  24. package/dist/console-config-routes.d.ts +37 -0
  25. package/dist/console-config-routes.d.ts.map +1 -0
  26. package/dist/console-config-routes.js +91 -0
  27. package/dist/console-headers.d.ts +1 -1
  28. package/dist/console-info-routes.d.ts +84 -0
  29. package/dist/console-info-routes.d.ts.map +1 -0
  30. package/dist/console-info-routes.js +135 -0
  31. package/dist/console-keys-routes.d.ts +50 -0
  32. package/dist/console-keys-routes.d.ts.map +1 -0
  33. package/dist/console-keys-routes.js +222 -0
  34. package/dist/console-llm-keys-routes.d.ts +47 -0
  35. package/dist/console-llm-keys-routes.d.ts.map +1 -0
  36. package/dist/console-llm-keys-routes.js +443 -0
  37. package/dist/console-mcp-tools-routes.d.ts +41 -0
  38. package/dist/console-mcp-tools-routes.d.ts.map +1 -0
  39. package/dist/console-mcp-tools-routes.js +60 -0
  40. package/dist/console-registry-routes.d.ts +66 -0
  41. package/dist/console-registry-routes.d.ts.map +1 -0
  42. package/dist/console-registry-routes.js +276 -0
  43. package/dist/console-session-routes.d.ts +89 -0
  44. package/dist/console-session-routes.d.ts.map +1 -0
  45. package/dist/console-session-routes.js +385 -0
  46. package/dist/console-sessions-routes.d.ts +52 -0
  47. package/dist/console-sessions-routes.d.ts.map +1 -0
  48. package/dist/console-sessions-routes.js +106 -0
  49. package/dist/console-static-routes.d.ts +54 -0
  50. package/dist/console-static-routes.d.ts.map +1 -0
  51. package/dist/console-static-routes.js +190 -0
  52. package/dist/console-theme-routes.d.ts +3 -3
  53. package/dist/console-theme-routes.js +1 -1
  54. package/dist/console-timeline.d.ts +5 -5
  55. package/dist/console-timeline.d.ts.map +1 -1
  56. package/dist/console-timeline.js +27 -26
  57. package/dist/console-welcome.js +2 -2
  58. package/dist/email-login.d.ts.map +1 -1
  59. package/dist/email-login.js +2 -3
  60. package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
  61. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
  62. package/dist/ggui-session-channel/action-ingress.js +228 -0
  63. package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
  64. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
  65. package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
  66. package/dist/ggui-session-channel/internal-types.d.ts +102 -0
  67. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
  68. package/dist/ggui-session-channel/internal-types.js +6 -0
  69. package/dist/ggui-session-channel/outbound.d.ts +81 -0
  70. package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
  71. package/dist/ggui-session-channel/outbound.js +174 -0
  72. package/dist/ggui-session-channel/socket-router.d.ts +38 -0
  73. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
  74. package/dist/ggui-session-channel/socket-router.js +213 -0
  75. package/dist/ggui-session-channel/subscribe.d.ts +165 -0
  76. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
  77. package/dist/ggui-session-channel/subscribe.js +370 -0
  78. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
  79. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
  80. package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
  81. package/dist/ggui-session-channel.d.ts +425 -0
  82. package/dist/ggui-session-channel.d.ts.map +1 -0
  83. package/dist/ggui-session-channel.js +262 -0
  84. package/dist/health-routes.d.ts +76 -0
  85. package/dist/health-routes.d.ts.map +1 -0
  86. package/dist/health-routes.js +145 -0
  87. package/dist/index.d.ts +10 -11
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +8 -9
  90. package/dist/instructions-presets.d.ts +3 -3
  91. package/dist/instructions-presets.js +24 -24
  92. package/dist/llm-backed-negotiator.d.ts +68 -67
  93. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  94. package/dist/llm-backed-negotiator.js +82 -221
  95. package/dist/mcp-apps-outbound.d.ts +47 -48
  96. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  97. package/dist/mcp-apps-outbound.js +154 -177
  98. package/dist/mcp-endpoint-routes.d.ts +88 -0
  99. package/dist/mcp-endpoint-routes.d.ts.map +1 -0
  100. package/dist/mcp-endpoint-routes.js +359 -0
  101. package/dist/mcp-mounts.d.ts +2 -76
  102. package/dist/mcp-mounts.d.ts.map +1 -1
  103. package/dist/mcp-mounts.js +0 -76
  104. package/dist/oauth-as-routes.d.ts +60 -0
  105. package/dist/oauth-as-routes.d.ts.map +1 -0
  106. package/dist/oauth-as-routes.js +82 -0
  107. package/dist/oauth-clients-routes.d.ts +39 -0
  108. package/dist/oauth-clients-routes.d.ts.map +1 -0
  109. package/dist/oauth-clients-routes.js +87 -0
  110. package/dist/oauth-login-types.d.ts +1 -20
  111. package/dist/oauth-login-types.d.ts.map +1 -1
  112. package/dist/oauth-login-types.js +30 -7
  113. package/dist/oauth-login.d.ts.map +1 -1
  114. package/dist/oauth-login.js +3 -2
  115. package/dist/oauth-providers-store.d.ts.map +1 -1
  116. package/dist/oauth-providers-store.js +5 -5
  117. package/dist/oauth.d.ts +9 -8
  118. package/dist/oauth.d.ts.map +1 -1
  119. package/dist/oauth.js +41 -19
  120. package/dist/pairing-transport.d.ts.map +1 -1
  121. package/dist/pairing-transport.js +2 -1
  122. package/dist/request-context.d.ts +2 -2
  123. package/dist/request-context.js +2 -2
  124. package/dist/reserved-validators.d.ts.map +1 -1
  125. package/dist/reserved-validators.js +9 -1
  126. package/dist/route-param.d.ts +9 -0
  127. package/dist/route-param.d.ts.map +1 -0
  128. package/dist/route-param.js +10 -0
  129. package/dist/runtime-bundle-route.d.ts +43 -0
  130. package/dist/runtime-bundle-route.d.ts.map +1 -0
  131. package/dist/runtime-bundle-route.js +80 -0
  132. package/dist/schema-compat.d.ts +64 -62
  133. package/dist/schema-compat.d.ts.map +1 -1
  134. package/dist/schema-compat.js +23 -51
  135. package/dist/server.d.ts +179 -193
  136. package/dist/server.d.ts.map +1 -1
  137. package/dist/server.js +644 -3759
  138. package/dist/storage.d.ts +5 -5
  139. package/dist/storage.d.ts.map +1 -1
  140. package/dist/storage.js +5 -5
  141. package/dist/thread-transport.d.ts.map +1 -1
  142. package/dist/thread-transport.js +4 -3
  143. package/dist/user-session-auth.d.ts +7 -21
  144. package/dist/user-session-auth.d.ts.map +1 -1
  145. package/dist/user-session-auth.js +7 -28
  146. package/package.json +16 -15
  147. package/dist/mcp-apps-inbound.d.ts +0 -86
  148. package/dist/mcp-apps-inbound.d.ts.map +0 -1
  149. package/dist/mcp-apps-inbound.js +0 -283
  150. package/dist/render-channel.d.ts +0 -694
  151. package/dist/render-channel.d.ts.map +0 -1
  152. package/dist/render-channel.js +0 -1775
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Console blueprint resolution + try-live routes.
3
+ *
4
+ * GET /ggui/console/blueprint/:id — same-origin HTTP mirror of
5
+ * the `ggui_render_blueprint` MCP tool. Resolves a
6
+ * manifest-declared blueprint id to its compiled bundle +
7
+ * metadata via the wired `UiRegistry`; lets the SPA's
8
+ * `/preview/<id>` route mount the blueprint with a single fetch
9
+ * instead of negotiating a full MCP round-trip from the browser.
10
+ * POST /ggui/console/blueprint/:id/try — create a render, compile
11
+ * the blueprint's componentCode, commit a render with its full
12
+ * contract (actionSpec/streamSpec/propsSpec from the manifest),
13
+ * mint a shortCode, and return `{sessionId, shortCode, url}`.
14
+ * The returned `/s/<shortCode>` lands on the console's render
15
+ * viewer + subscribes to the render over `/ws`.
16
+ *
17
+ * Scope: registered only when a `UiRegistry` is present (same gate
18
+ * as the MCP render handler — no registry = no render path, no
19
+ * endpoint). No bearer auth: console routes are same-origin
20
+ * operator-facing; the operator already has OS access to the TSX
21
+ * sources this endpoint serves back.
22
+ *
23
+ * GET failure shape: { error, message } with a matching HTTP code.
24
+ * - 404 for unknown id OR known-id-no-bundle (source-only /
25
+ * compile-failed). The operator's remediation is the same in
26
+ * both cases (fix the manifest / fix the entry / fix the
27
+ * compile); splitting codes would add noise without signal.
28
+ * - 400 for malformed id parameters (empty, oversized).
29
+ *
30
+ * Shape symmetry: GET matches `GguiRenderBlueprintOutput` exactly, so
31
+ * the browser-side fetch can share a type import with MCP-tool
32
+ * callers without a translation layer.
33
+ *
34
+ * Try-live gates (all three required):
35
+ * - `uiRegistry` — blueprint resolution
36
+ * - `renderStore` — render persistence
37
+ * - `shortCodeIndex` — shortCode → render binding
38
+ *
39
+ * Partial gate (uiRegistry alone) → 503 with a remediation hint.
40
+ */
41
+ import { randomBytes, randomUUID } from "node:crypto";
42
+ import { DEFAULT_BUILDER_APP_ID } from "./auth.js";
43
+ import { applyDevtoolSecurityHeaders } from "./console-headers.js";
44
+ import { checkRenderSchemaCompat, SchemaCompatError, } from "./schema-compat.js";
45
+ /**
46
+ * 18-char URL-safe shortCode for `POST /ggui/console/blueprint/:id/try`
47
+ * — visually distinct from the 16-char render-minted shortCodes in
48
+ * `@ggui-ai/mcp-server-handlers/renders/render.ts` so operators
49
+ * reading logs can tell a try-live render from an agent-rendered one
50
+ * at a glance. Same confusable-free alphabet
51
+ * (`[a-z0-9]` minus `1lI0Oo`) so the code stays hand-typable. Entropy
52
+ * ≈ 18 × log₂(31) ≈ 89 bits.
53
+ */
54
+ function generateTryLiveShortCode() {
55
+ const alphabet = "abcdefghjkmnpqrstuvwxyz23456789";
56
+ const bytes = randomBytes(18);
57
+ let out = "";
58
+ for (let i = 0; i < 18; i += 1) {
59
+ out += alphabet[bytes[i] % alphabet.length];
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * Mount `GET /ggui/console/blueprint/:id` +
65
+ * `POST /ggui/console/blueprint/:id/try` onto the express app.
66
+ * Returns nothing — the routes self-register.
67
+ */
68
+ export function mountConsoleBlueprintRoutes(opts) {
69
+ const { app, uiRegistry, renderStore, shortCodeIndex, handlers, schemaCompatMode, logger } = opts;
70
+ if (renderStore && shortCodeIndex) {
71
+ const renderStoreForTry = renderStore;
72
+ const shortCodeIndexForTry = shortCodeIndex;
73
+ app.post("/ggui/console/blueprint/:id/try", async (req, res) => {
74
+ applyDevtoolSecurityHeaders(res);
75
+ const blueprintId = req.params["id"];
76
+ if (typeof blueprintId !== "string" || blueprintId.length === 0 || blueprintId.length > 256) {
77
+ res.status(400).json({
78
+ error: "invalid_request",
79
+ message: "`id` path parameter must be a non-empty string (≤256 chars)",
80
+ });
81
+ return;
82
+ }
83
+ try {
84
+ const entry = await uiRegistry.get(blueprintId);
85
+ if (!entry) {
86
+ res.status(404).json({
87
+ error: "not_found",
88
+ message: `No blueprint registered with id "${blueprintId}". Check ggui.json#blueprints.include globs + ggui.ui.json#id values.`,
89
+ });
90
+ return;
91
+ }
92
+ const bundle = await uiRegistry.getBundle(blueprintId);
93
+ if (!bundle) {
94
+ res.status(404).json({
95
+ error: "bundle_not_available",
96
+ message: `Blueprint "${blueprintId}" (${entry.manifest.name}) has no bundle available. Either the TSX entry is missing or compile-on-demand failed.`,
97
+ });
98
+ return;
99
+ }
100
+ // Materialize streamed bundles to a string — the render
101
+ // stores componentCode inline. Same rule the sibling GET
102
+ // endpoint applies.
103
+ let code;
104
+ if (typeof bundle.code === "string") {
105
+ code = bundle.code;
106
+ }
107
+ else {
108
+ const reader = bundle.code.getReader();
109
+ const decoder = new TextDecoder();
110
+ let out = "";
111
+ for (;;) {
112
+ const { done, value } = await reader.read();
113
+ if (done)
114
+ break;
115
+ if (typeof value === "string")
116
+ out += value;
117
+ else if (value instanceof Uint8Array)
118
+ out += decoder.decode(value, { stream: true });
119
+ }
120
+ out += decoder.decode();
121
+ code = out;
122
+ }
123
+ // Same default appId the CLI's pairing-authenticated /mcp
124
+ // ingress resolves for single-tenant OSS — the render is
125
+ // scoped to the same tenant.
126
+ const appId = DEFAULT_BUILDER_APP_ID;
127
+ // Phase B: a render IS the addressable unit; the prior
128
+ // (sessionId, stackItemId) pair collapses to a single
129
+ // sessionId. The blueprint id makes a natural slug; a
130
+ // same-blueprint retry replaces the row.
131
+ const sessionId = `try-${blueprintId}-${randomUUID()}`;
132
+ const createdAt = Date.now();
133
+ const contract = entry.manifest.contract ?? {};
134
+ const render = {
135
+ id: sessionId,
136
+ appId,
137
+ type: "component",
138
+ componentCode: code,
139
+ contentType: bundle.contentType,
140
+ eventSequence: 0,
141
+ createdAt,
142
+ lastActivityAt: createdAt,
143
+ expiresAt: createdAt + 24 * 60 * 60 * 1000,
144
+ description: `Blueprint try-live: ${entry.manifest.name}`,
145
+ // Data contract fields from the manifest. Each is
146
+ // conditionally spread — absent on the manifest →
147
+ // absent on the GguiSession (keeps shape honest + avoids
148
+ // an empty-shape contract tripping structural
149
+ // validators downstream).
150
+ ...(contract.propsSpec ? { propsSpec: contract.propsSpec } : {}),
151
+ ...(contract.actionSpec ? { actionSpec: contract.actionSpec } : {}),
152
+ ...(contract.streamSpec ? { streamSpec: contract.streamSpec } : {}),
153
+ };
154
+ // Schema compatibility check. Fires BEFORE the render
155
+ // commits — if the blueprint's pre-declared actionSpec
156
+ // hints at a same-server tool whose schemas don't align,
157
+ // the operator gets a named `SCHEMA_MISMATCH_ERROR`
158
+ // response at registration time instead of an agent-side
159
+ // surprise on the first dispatched action. Mode sourced
160
+ // from `createGguiServer({schemaCompatCheck})`; defaults
161
+ // to `'reject'`. See `./schema-compat.ts`.
162
+ try {
163
+ const report = checkRenderSchemaCompat(render, handlers, schemaCompatMode, `console blueprint-try:${blueprintId}`);
164
+ if (!report.compatible) {
165
+ // Non-throwing path (mode === 'warn'): log with full
166
+ // detail so the operator has an observable surface.
167
+ logger.warn("schema_compat_warn", {
168
+ site: "console_blueprint_try",
169
+ blueprintId,
170
+ findingCount: report.findings.length,
171
+ findings: report.findings.map((f) => ({
172
+ kind: f.kind,
173
+ specName: f.specName,
174
+ toolName: f.toolName,
175
+ reason: f.reason,
176
+ violationCount: f.violations.length,
177
+ })),
178
+ });
179
+ }
180
+ }
181
+ catch (err) {
182
+ if (err instanceof SchemaCompatError) {
183
+ logger.warn("console_blueprint_try_schema_compat_rejected", {
184
+ blueprintId,
185
+ findingCount: err.report.findings.length,
186
+ });
187
+ res.status(422).json({
188
+ error: "SCHEMA_MISMATCH_ERROR",
189
+ message: err.message,
190
+ findings: err.report.findings.map((f) => ({
191
+ kind: f.kind,
192
+ specName: f.specName,
193
+ toolName: f.toolName,
194
+ reason: f.reason,
195
+ violationCount: f.violations.length,
196
+ })),
197
+ });
198
+ return;
199
+ }
200
+ throw err;
201
+ }
202
+ try {
203
+ await renderStoreForTry.commit({ render, appId });
204
+ }
205
+ catch (err) {
206
+ logger.warn("console_blueprint_try_commit_failed", {
207
+ blueprintId,
208
+ sessionId,
209
+ error: String(err),
210
+ });
211
+ res.status(500).json({
212
+ error: "commit_failed",
213
+ message: err instanceof Error ? err.message : String(err),
214
+ });
215
+ return;
216
+ }
217
+ // Mint the shortCode last — if earlier steps failed the
218
+ // client never sees a dangling mapping. Best-effort bind
219
+ // to match render.ts's posture (a put failure shouldn't
220
+ // fail the whole try-live — a 500 here would leave the
221
+ // render behind with no way to resolve from
222
+ // `/s/<shortCode>`, but the operator can still hit the
223
+ // render via `/ggui/console/sessions`).
224
+ const shortCode = generateTryLiveShortCode();
225
+ try {
226
+ await shortCodeIndexForTry.put(shortCode, {
227
+ sessionId,
228
+ appId,
229
+ });
230
+ }
231
+ catch (err) {
232
+ logger.warn("console_blueprint_try_shortcode_failed", {
233
+ blueprintId,
234
+ sessionId,
235
+ shortCode,
236
+ error: String(err),
237
+ });
238
+ // Don't fail the response — the client can reopen via
239
+ // the renders list. Surface the issue in the payload
240
+ // so the SPA can show a degraded banner.
241
+ res.status(200).json({
242
+ sessionId,
243
+ shortCode: null,
244
+ url: null,
245
+ warning: "shortCode minted but not persisted; viewer link unavailable. Open via /ggui/console/sessions.",
246
+ });
247
+ return;
248
+ }
249
+ res.json({
250
+ sessionId,
251
+ shortCode,
252
+ url: `/s/${shortCode}`,
253
+ });
254
+ }
255
+ catch (err) {
256
+ logger.warn("console_blueprint_try_failed", {
257
+ blueprintId,
258
+ error: String(err),
259
+ });
260
+ res.status(500).json({
261
+ error: "try_failed",
262
+ message: err instanceof Error ? err.message : String(err),
263
+ });
264
+ }
265
+ });
266
+ }
267
+ else {
268
+ // Partial wiring — /try would attempt a render create that
269
+ // has nowhere to land. Surface with 503 + specific message
270
+ // so the operator knows which seam to add (console cookie +
271
+ // shortCodeIndex live on `console.sessionCookie: true`).
272
+ app.post("/ggui/console/blueprint/:id/try", (_req, res) => {
273
+ applyDevtoolSecurityHeaders(res);
274
+ res.status(503).json({
275
+ error: "try_not_wired",
276
+ message: "POST /ggui/console/blueprint/:id/try requires `renderChannel: true` + `shortCodeIndex` on createGguiServer. The CLI enables both by default via `console.sessionCookie: true`.",
277
+ });
278
+ });
279
+ }
280
+ app.get("/ggui/console/blueprint/:id", async (req, res) => {
281
+ applyDevtoolSecurityHeaders(res);
282
+ const blueprintId = req.params["id"];
283
+ if (typeof blueprintId !== "string" || blueprintId.length === 0 || blueprintId.length > 256) {
284
+ res.status(400).json({
285
+ error: "invalid_request",
286
+ message: "`id` path parameter must be a non-empty string (≤256 chars)",
287
+ });
288
+ return;
289
+ }
290
+ try {
291
+ const entry = await uiRegistry.get(blueprintId);
292
+ if (!entry) {
293
+ res.status(404).json({
294
+ error: "not_found",
295
+ message: `No blueprint registered with id "${blueprintId}". Check ggui.json#blueprints.include globs + ggui.ui.json#id values.`,
296
+ });
297
+ return;
298
+ }
299
+ const bundle = await uiRegistry.getBundle(blueprintId);
300
+ if (!bundle) {
301
+ res.status(404).json({
302
+ error: "bundle_not_available",
303
+ message: `Blueprint "${blueprintId}" (${entry.manifest.name}) has no bundle available. Either the TSX entry is missing or compile-on-demand failed — check the manifest directory.`,
304
+ });
305
+ return;
306
+ }
307
+ // Same string-materialization rule as the MCP render handler:
308
+ // collapse stream bundles to a plain string so the browser
309
+ // fetch can JSON-parse the response in one shot.
310
+ let code;
311
+ if (typeof bundle.code === "string") {
312
+ code = bundle.code;
313
+ }
314
+ else {
315
+ const reader = bundle.code.getReader();
316
+ const decoder = new TextDecoder();
317
+ let out = "";
318
+ for (;;) {
319
+ const { done, value } = await reader.read();
320
+ if (done)
321
+ break;
322
+ if (typeof value === "string")
323
+ out += value;
324
+ else if (value instanceof Uint8Array)
325
+ out += decoder.decode(value, { stream: true });
326
+ }
327
+ out += decoder.decode();
328
+ code = out;
329
+ }
330
+ res.json({
331
+ blueprintId,
332
+ blueprintName: entry.manifest.name,
333
+ code,
334
+ contentType: bundle.contentType,
335
+ });
336
+ }
337
+ catch (err) {
338
+ logger.warn("console_blueprint_resolve_failed", {
339
+ blueprintId,
340
+ error: String(err),
341
+ });
342
+ res.status(500).json({
343
+ error: "resolve_failed",
344
+ message: err instanceof Error ? err.message : String(err),
345
+ });
346
+ }
347
+ });
348
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Console dev-chat round-trip route.
3
+ *
4
+ * POST /ggui/console/chat/message — OSS dev chat round-trip.
5
+ *
6
+ * Routes the message through `ggui_render` whenever the server was
7
+ * composed with a real generator — turning the chat surface into
8
+ * the cohesive agent experience: every user message commits a
9
+ * render against a thread-scoped render; the render handler owns
10
+ * generation, cache, and provisional preview; the client renders
11
+ * the resulting render inline using `GguiSessionRenderer`.
12
+ *
13
+ * Shape: `{ text, threadId?, sessionId? }` →
14
+ * `{ threadId, userMessage, agentMessage, ui? }`.
15
+ * - `ui` is populated only when the render handler is wired AND
16
+ * the call succeeded. `ui.sessionId` is the render id the client
17
+ * subscribes to over `/ws`; the committed render arrives via
18
+ * the subscribe ack's single `render` field, which the client
19
+ * mounts inline to show the agent's generated component.
20
+ * - When the render handler is NOT wired (no `mcpApps`, placeholder
21
+ * mode, or no BYOK), `ui` is absent and `agentMessage.text`
22
+ * carries an honest text-only acknowledgment. This preserves
23
+ * the text-only round-trip path so operators without a key can
24
+ * still exercise the chat UI end-to-end.
25
+ * - `sessionId` is echoed on every response and should be passed
26
+ * back on subsequent messages so the thread reuses one render.
27
+ *
28
+ * Same-origin only — no bearer auth. The console surface is
29
+ * always the operator's own browser pointing at their own `ggui
30
+ * serve` instance; adding bearer auth here would block the
31
+ * dev-page-is-usable claim without meaningful security gain.
32
+ *
33
+ * The render invocation uses `DEFAULT_BUILDER_APP_ID` for tenant
34
+ * scope — same well-known value the `/mcp` endpoint collapses to
35
+ * in OSS single-user mode. Matches blueprint + vector scoping
36
+ * applied by the generator / cache seams.
37
+ *
38
+ * Handler gate: without a generator wired, a render call would
39
+ * allocate a render + shortCode with empty componentCode
40
+ * (codeReady:false) per turn without any visible UI — honest
41
+ * behavior but useless. Falling through to the canned-text path
42
+ * keeps the chat surface usable without a BYOK key AND preserves
43
+ * the exact Lane-1 chat-page spec assertion (`/OSS agent
44
+ * generation/`) without a copy change.
45
+ */
46
+ import type { SharedHandler } from "@ggui-ai/mcp-server-handlers";
47
+ import type { Express } from "express";
48
+ import type { ZodRawShape } from "zod";
49
+ import type { Logger } from "./logger.js";
50
+ interface MountOptions {
51
+ /** Express app to mount onto. */
52
+ readonly app: Express;
53
+ /**
54
+ * `ggui_render` handler — present only when the composer resolved
55
+ * generation deps (the gate that makes a render turn useful).
56
+ */
57
+ readonly renderHandler?: SharedHandler<ZodRawShape, ZodRawShape>;
58
+ /** `ggui_handshake` handler paired with the render handler. */
59
+ readonly handshakeHandler?: SharedHandler<ZodRawShape, ZodRawShape>;
60
+ /**
61
+ * Console session-cookie wiring. Present only when the cookie flow
62
+ * is enabled — each successful render turn then mints a same-origin
63
+ * HttpOnly cookie so the chat can open the /ws subscription without
64
+ * a separate POST to /ggui/console/session-cookie.
65
+ */
66
+ readonly sessionCookie?: {
67
+ readonly secret: string;
68
+ readonly ttlSec?: number;
69
+ readonly secure: boolean;
70
+ };
71
+ /** Structured logger. */
72
+ readonly logger: Logger;
73
+ }
74
+ /**
75
+ * Mount `POST /ggui/console/chat/message` onto the express app.
76
+ * Returns nothing — the route self-registers.
77
+ */
78
+ export declare function mountConsoleChatRoutes(opts: MountOptions): void;
79
+ export {};
80
+ //# sourceMappingURL=console-chat-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-chat-routes.d.ts","sourceRoot":"","sources":["../src/console-chat-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACpE;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE;QACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAsJ/D"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Console dev-chat round-trip route.
3
+ *
4
+ * POST /ggui/console/chat/message — OSS dev chat round-trip.
5
+ *
6
+ * Routes the message through `ggui_render` whenever the server was
7
+ * composed with a real generator — turning the chat surface into
8
+ * the cohesive agent experience: every user message commits a
9
+ * render against a thread-scoped render; the render handler owns
10
+ * generation, cache, and provisional preview; the client renders
11
+ * the resulting render inline using `GguiSessionRenderer`.
12
+ *
13
+ * Shape: `{ text, threadId?, sessionId? }` →
14
+ * `{ threadId, userMessage, agentMessage, ui? }`.
15
+ * - `ui` is populated only when the render handler is wired AND
16
+ * the call succeeded. `ui.sessionId` is the render id the client
17
+ * subscribes to over `/ws`; the committed render arrives via
18
+ * the subscribe ack's single `render` field, which the client
19
+ * mounts inline to show the agent's generated component.
20
+ * - When the render handler is NOT wired (no `mcpApps`, placeholder
21
+ * mode, or no BYOK), `ui` is absent and `agentMessage.text`
22
+ * carries an honest text-only acknowledgment. This preserves
23
+ * the text-only round-trip path so operators without a key can
24
+ * still exercise the chat UI end-to-end.
25
+ * - `sessionId` is echoed on every response and should be passed
26
+ * back on subsequent messages so the thread reuses one render.
27
+ *
28
+ * Same-origin only — no bearer auth. The console surface is
29
+ * always the operator's own browser pointing at their own `ggui
30
+ * serve` instance; adding bearer auth here would block the
31
+ * dev-page-is-usable claim without meaningful security gain.
32
+ *
33
+ * The render invocation uses `DEFAULT_BUILDER_APP_ID` for tenant
34
+ * scope — same well-known value the `/mcp` endpoint collapses to
35
+ * in OSS single-user mode. Matches blueprint + vector scoping
36
+ * applied by the generator / cache seams.
37
+ *
38
+ * Handler gate: without a generator wired, a render call would
39
+ * allocate a render + shortCode with empty componentCode
40
+ * (codeReady:false) per turn without any visible UI — honest
41
+ * behavior but useless. Falling through to the canned-text path
42
+ * keeps the chat surface usable without a BYOK key AND preserves
43
+ * the exact Lane-1 chat-page spec assertion (`/OSS agent
44
+ * generation/`) without a copy change.
45
+ */
46
+ import { randomUUID } from "node:crypto";
47
+ import { DEFAULT_BUILDER_APP_ID } from "./auth.js";
48
+ import { mintDevtoolCookie } from "./console-auth.js";
49
+ import { applyDevtoolSecurityHeaders } from "./console-headers.js";
50
+ /**
51
+ * Mount `POST /ggui/console/chat/message` onto the express app.
52
+ * Returns nothing — the route self-registers.
53
+ */
54
+ export function mountConsoleChatRoutes(opts) {
55
+ const { app, renderHandler, handshakeHandler, sessionCookie, logger } = opts;
56
+ app.post("/ggui/console/chat/message", async (req, res) => {
57
+ applyDevtoolSecurityHeaders(res);
58
+ const body = (req.body ?? {});
59
+ const text = typeof body.text === "string" ? body.text.trim() : "";
60
+ if (text.length === 0) {
61
+ res.status(400).json({
62
+ error: "invalid_request",
63
+ message: "`text` (non-empty string) is required",
64
+ });
65
+ return;
66
+ }
67
+ if (text.length > 4000) {
68
+ res.status(400).json({
69
+ error: "invalid_request",
70
+ message: "`text` must be <= 4000 chars",
71
+ });
72
+ return;
73
+ }
74
+ const threadId = typeof body.threadId === "string" && body.threadId.length > 0
75
+ ? body.threadId
76
+ : `chat-${randomUUID()}`;
77
+ const now = Date.now();
78
+ const userMessage = {
79
+ id: `msg-${randomUUID()}`,
80
+ role: "user",
81
+ text,
82
+ createdAt: now,
83
+ };
84
+ // Attempt real generation through `ggui_render` when wired.
85
+ // `renderHandler` is undefined when mcpApps was disabled or
86
+ // the operator built a custom handler set without render. The
87
+ // handler itself returns `codeReady:false` when generation deps
88
+ // aren't wired (no BYOK) — we surface that honestly on the
89
+ // agentMessage text without pretending a UI landed.
90
+ let ui;
91
+ let agentText;
92
+ if (renderHandler && handshakeHandler) {
93
+ try {
94
+ const requestId = randomUUID();
95
+ const handshakeInput = {
96
+ story: { intent: text, contract: {} },
97
+ };
98
+ const hsRaw = await handshakeHandler.handler(handshakeInput, {
99
+ appId: DEFAULT_BUILDER_APP_ID,
100
+ requestId,
101
+ });
102
+ const handshakeId = hsRaw.handshakeId;
103
+ const raw = await renderHandler.handler({ handshakeId, contract: {} }, { appId: DEFAULT_BUILDER_APP_ID, requestId });
104
+ const result = raw;
105
+ ui = {
106
+ sessionId: result.sessionId,
107
+ shortCode: result.shortCode,
108
+ codeReady: result.codeReady,
109
+ ...(result.cache ? { cache: result.cache } : {}),
110
+ };
111
+ // If console cookie auth is enabled, mint a session
112
+ // cookie so the chat can open the /ws subscription without
113
+ // a separate POST to /ggui/console/session-cookie. Single round-trip
114
+ // per turn; cookie is same-origin HttpOnly.
115
+ if (sessionCookie) {
116
+ const mint = mintDevtoolCookie({
117
+ sessionId: result.sessionId,
118
+ appId: DEFAULT_BUILDER_APP_ID,
119
+ secret: sessionCookie.secret,
120
+ ...(sessionCookie.ttlSec !== undefined ? { ttlSec: sessionCookie.ttlSec } : {}),
121
+ secure: sessionCookie.secure,
122
+ });
123
+ res.setHeader("Set-Cookie", mint.setCookieHeader);
124
+ }
125
+ if (result.codeReady) {
126
+ agentText = result.cache?.hit
127
+ ? "Reused a matching UI from cache for your request."
128
+ : "Generated a UI for your request.";
129
+ }
130
+ else {
131
+ // Generator ran but produced no code (no BYOK, generator
132
+ // error, or placeholder mode). Honest text so the
133
+ // operator knows why the surface didn't render a UI.
134
+ agentText =
135
+ "I received your message, but generation did not produce a UI " +
136
+ "(no BYOK key configured, or the provider declined). Export " +
137
+ "ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / " +
138
+ "OPENROUTER_API_KEY and retry.";
139
+ // Drop the ui payload — no code ready means nothing to
140
+ // render inline. The agent text carries the diagnosis.
141
+ ui = undefined;
142
+ }
143
+ }
144
+ catch (err) {
145
+ logger.warn?.("console_chat_render_failed", {
146
+ threadId,
147
+ error: err instanceof Error ? err.message : String(err),
148
+ });
149
+ agentText =
150
+ "Generation failed: " +
151
+ (err instanceof Error ? err.message : String(err)) +
152
+ ". The chat surface is still live — retry or try a different prompt.";
153
+ ui = undefined;
154
+ }
155
+ }
156
+ const agentMessage = {
157
+ id: `msg-${randomUUID()}`,
158
+ role: "agent",
159
+ text: agentText ??
160
+ // No render handler at all — text-only fallback. Keeps the
161
+ // Lane-1 chat-page spec green by preserving the exact copy
162
+ // it asserts against.
163
+ "Message received. OSS agent generation is not yet wired — " +
164
+ "this is the text-only dev chat. Full responses " +
165
+ "and generated UIs arrive once the generator port lands.",
166
+ createdAt: now + 1,
167
+ };
168
+ logger.debug?.("console_chat_message", {
169
+ threadId,
170
+ userMessageId: userMessage.id,
171
+ textLength: text.length,
172
+ uiSessionId: ui?.sessionId,
173
+ uiCodeReady: ui?.codeReady,
174
+ });
175
+ res.status(200).json({
176
+ threadId,
177
+ userMessage,
178
+ agentMessage,
179
+ ...(ui ? { ui } : {}),
180
+ });
181
+ });
182
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Console manifest-read route.
3
+ *
4
+ * GET /ggui/console/config — VSCode-settings-style read of the
5
+ * resolved `ggui.json`. Returns the parsed manifest, the raw
6
+ * file contents for display, and the introspected v1 JSON Schema
7
+ * (which carries field descriptions via the `.describe()` calls
8
+ * on `GguiJsonV1`).
9
+ *
10
+ * Source resolution: walks up from `process.cwd()` to find the
11
+ * nearest `ggui.json`. Honest about three states:
12
+ * - found + valid → `{source: {found:true, path}, manifest, raw, schema}`
13
+ * - found + invalid → `{source: {found:true, path, error: {message}},
14
+ * raw, schema}` (no manifest field — the operator inspects the raw
15
+ * bytes + sees the validation error so they can fix the file)
16
+ * - not found → `{source: {found:false, searchedFrom}, schema}`
17
+ * (the schema still ships so operators can browse what would be
18
+ * configurable IF a manifest existed)
19
+ *
20
+ * Read-only. Form controls on the same payload and a PATCH
21
+ * endpoint with atomic write + conflict detection layer on top.
22
+ */
23
+ import type { Express } from "express";
24
+ import type { Logger } from "./logger.js";
25
+ interface MountOptions {
26
+ /** Express app to mount onto. */
27
+ readonly app: Express;
28
+ /** Structured logger for read/conversion warnings. */
29
+ readonly logger: Logger;
30
+ }
31
+ /**
32
+ * Mount `GET /ggui/console/config` onto the express app. Returns
33
+ * nothing — the route self-registers.
34
+ */
35
+ export declare function mountConsoleConfigRoutes(opts: MountOptions): void;
36
+ export {};
37
+ //# sourceMappingURL=console-config-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-config-routes.d.ts","sourceRoot":"","sources":["../src/console-config-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA0DjE"}