@ggui-ai/cli 0.1.0-rc.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.
- package/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/agent-resolution.d.ts +41 -0
- package/dist/agent-resolution.d.ts.map +1 -0
- package/dist/agent-resolution.js +80 -0
- package/dist/api-client.d.ts +59 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +145 -0
- package/dist/auth-keys.d.ts +10 -0
- package/dist/auth-keys.d.ts.map +1 -0
- package/dist/auth-keys.js +382 -0
- package/dist/auth-login.d.ts +11 -0
- package/dist/auth-login.d.ts.map +1 -0
- package/dist/auth-login.js +231 -0
- package/dist/auth-logout.d.ts +3 -0
- package/dist/auth-logout.d.ts.map +1 -0
- package/dist/auth-logout.js +32 -0
- package/dist/auth-store.d.ts +67 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +150 -0
- package/dist/auth-whoami.d.ts +3 -0
- package/dist/auth-whoami.d.ts.map +1 -0
- package/dist/auth-whoami.js +52 -0
- package/dist/blueprint-command.d.ts +3 -0
- package/dist/blueprint-command.d.ts.map +1 -0
- package/dist/blueprint-command.js +203 -0
- package/dist/blueprint-create.d.ts +71 -0
- package/dist/blueprint-create.d.ts.map +1 -0
- package/dist/blueprint-create.js +377 -0
- package/dist/byok-resolver.d.ts +141 -0
- package/dist/byok-resolver.d.ts.map +1 -0
- package/dist/byok-resolver.js +89 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1066 -0
- package/dist/dev-command.d.ts +169 -0
- package/dist/dev-command.d.ts.map +1 -0
- package/dist/dev-command.js +222 -0
- package/dist/gadget-command.d.ts +3 -0
- package/dist/gadget-command.d.ts.map +1 -0
- package/dist/gadget-command.js +181 -0
- package/dist/gadget-create.d.ts +106 -0
- package/dist/gadget-create.d.ts.map +1 -0
- package/dist/gadget-create.js +492 -0
- package/dist/generation-probe.d.ts +135 -0
- package/dist/generation-probe.d.ts.map +1 -0
- package/dist/generation-probe.js +132 -0
- package/dist/internal/artifact-install.d.ts +150 -0
- package/dist/internal/artifact-install.d.ts.map +1 -0
- package/dist/internal/artifact-install.js +1019 -0
- package/dist/internal/artifact-publish.d.ts +204 -0
- package/dist/internal/artifact-publish.d.ts.map +1 -0
- package/dist/internal/artifact-publish.js +1141 -0
- package/dist/internal/artifact-search.d.ts +155 -0
- package/dist/internal/artifact-search.d.ts.map +1 -0
- package/dist/internal/artifact-search.js +539 -0
- package/dist/internal/artifact-uninstall.d.ts +30 -0
- package/dist/internal/artifact-uninstall.d.ts.map +1 -0
- package/dist/internal/artifact-uninstall.js +205 -0
- package/dist/internal/auth-cache.d.ts +40 -0
- package/dist/internal/auth-cache.d.ts.map +1 -0
- package/dist/internal/auth-cache.js +133 -0
- package/dist/internal/auth-strategy.d.ts +89 -0
- package/dist/internal/auth-strategy.d.ts.map +1 -0
- package/dist/internal/auth-strategy.js +132 -0
- package/dist/internal/ggui-json.d.ts +53 -0
- package/dist/internal/ggui-json.d.ts.map +1 -0
- package/dist/internal/ggui-json.js +92 -0
- package/dist/internal/key-store.d.ts +43 -0
- package/dist/internal/key-store.d.ts.map +1 -0
- package/dist/internal/key-store.js +128 -0
- package/dist/internal/oidc-token.d.ts +52 -0
- package/dist/internal/oidc-token.d.ts.map +1 -0
- package/dist/internal/oidc-token.js +351 -0
- package/dist/internal/register-author-key.d.ts +44 -0
- package/dist/internal/register-author-key.d.ts.map +1 -0
- package/dist/internal/register-author-key.js +212 -0
- package/dist/local-keys.d.ts +47 -0
- package/dist/local-keys.d.ts.map +1 -0
- package/dist/local-keys.js +115 -0
- package/dist/mcp-backend.d.ts +356 -0
- package/dist/mcp-backend.d.ts.map +1 -0
- package/dist/mcp-backend.js +668 -0
- package/dist/paths.d.ts +77 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +119 -0
- package/dist/persistent-secrets.d.ts +28 -0
- package/dist/persistent-secrets.d.ts.map +1 -0
- package/dist/persistent-secrets.js +94 -0
- package/dist/persistent-short-code-index.d.ts +14 -0
- package/dist/persistent-short-code-index.d.ts.map +1 -0
- package/dist/persistent-short-code-index.js +39 -0
- package/dist/persistent-stores.d.ts +12 -0
- package/dist/persistent-stores.d.ts.map +1 -0
- package/dist/persistent-stores.js +42 -0
- package/dist/serve-command.d.ts +500 -0
- package/dist/serve-command.d.ts.map +1 -0
- package/dist/serve-command.js +616 -0
- package/dist/serve-fallback.d.ts +64 -0
- package/dist/serve-fallback.d.ts.map +1 -0
- package/dist/serve-fallback.js +122 -0
- package/dist/spawn-cloudflared.d.ts +61 -0
- package/dist/spawn-cloudflared.d.ts.map +1 -0
- package/dist/spawn-cloudflared.js +167 -0
- package/dist/theme-command.d.ts +4 -0
- package/dist/theme-command.d.ts.map +1 -0
- package/dist/theme-command.js +154 -0
- package/dist/theme-file-uploader.d.ts +8 -0
- package/dist/theme-file-uploader.d.ts.map +1 -0
- package/dist/theme-file-uploader.js +58 -0
- package/dist/theme-writer.d.ts +8 -0
- package/dist/theme-writer.d.ts.map +1 -0
- package/dist/theme-writer.js +103 -0
- package/dist/tunnel-provider.d.ts +273 -0
- package/dist/tunnel-provider.d.ts.map +1 -0
- package/dist/tunnel-provider.js +142 -0
- package/package.json +81 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ggui serve` → `@ggui-ai/mcp-server` composition.
|
|
3
|
+
*
|
|
4
|
+
* Split out from `cli.ts` so tests can import the factory without
|
|
5
|
+
* triggering the CLI `main()` side-effect at module load. The banner /
|
|
6
|
+
* signal / lifecycle wiring stays in `cli.ts` / `serve-command.ts`;
|
|
7
|
+
* this module owns exactly one thing: the option bundle that turns
|
|
8
|
+
* `createGguiServer()` into the canonical OSS first-run server.
|
|
9
|
+
*
|
|
10
|
+
* What gets wired by default — see OSS-split §2 (the five server
|
|
11
|
+
* responsibilities):
|
|
12
|
+
*
|
|
13
|
+
* - `sessionChannel: true` — live-channel `/ws` endpoint. Required for
|
|
14
|
+
* MCP Apps iframes AND the console session viewer.
|
|
15
|
+
* - `pairing: true` — `POST /pair` + `POST /admin/pair/init` so
|
|
16
|
+
* remote clients (Portal, third-party) can pair with this server.
|
|
17
|
+
* The default `InMemoryAuthAdapter` registers pairing tokens
|
|
18
|
+
* through `onTokenIssued` automatically.
|
|
19
|
+
* - `console: { sessionCookie: true }` — landing page at `/` +
|
|
20
|
+
* `/s/<shortCode>` viewer + same-origin HTTP-only cookie flow.
|
|
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
|
|
25
|
+
* in memory unless the operator opts into sqlite, so a matching
|
|
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.
|
|
35
|
+
*
|
|
36
|
+
* Embedding hosts that want a different shape (no landing page, no
|
|
37
|
+
* pairing, programmatic control) compose `createGguiServer()` directly
|
|
38
|
+
* rather than going through `ggui serve`.
|
|
39
|
+
*/
|
|
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';
|
|
55
|
+
/**
|
|
56
|
+
* Pick a free localhost TCP port by letting the OS assign one on a
|
|
57
|
+
* throwaway listener. Close the listener immediately and return the
|
|
58
|
+
* chosen number. Exported so the CLI (`cli.ts::runServeCommand`) and
|
|
59
|
+
* the integration tests can resolve `--port 0` to a concrete port
|
|
60
|
+
* BEFORE constructing the backend — {@link buildMcpServerBackend}
|
|
61
|
+
* captures the port in the composed `mcpApps.renderBaseUrl` / `wsUrl`
|
|
62
|
+
* at construction time, so the caller must know it up-front.
|
|
63
|
+
*
|
|
64
|
+
* Tiny race window: another process could claim the port between our
|
|
65
|
+
* `close()` and the caller's `listen()`. The CLI surfaces bind errors
|
|
66
|
+
* as a fatal exit; the OSS first-run path recovers by re-running.
|
|
67
|
+
*/
|
|
68
|
+
export function pickFreePort(host = '127.0.0.1') {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const probe = createNetServer();
|
|
71
|
+
probe.once('error', (err) => {
|
|
72
|
+
probe.close();
|
|
73
|
+
reject(err);
|
|
74
|
+
});
|
|
75
|
+
probe.listen(0, host, () => {
|
|
76
|
+
const addr = probe.address();
|
|
77
|
+
if (addr === null || typeof addr === 'string') {
|
|
78
|
+
probe.close();
|
|
79
|
+
reject(new Error(`pickFreePort: could not resolve a free port (got ${String(addr)})`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const chosen = addr.port;
|
|
83
|
+
probe.close(() => resolve(chosen));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build the `ServeBackend` that `runServe` drives. Returns a thin
|
|
89
|
+
* adapter around `createGguiServer` — the factory is invoked here so
|
|
90
|
+
* preconditions that throw (e.g. missing auth bridge support for
|
|
91
|
+
* `pairing: true`) surface synchronously, before `runServe` bothers
|
|
92
|
+
* binding a port.
|
|
93
|
+
*/
|
|
94
|
+
export function buildMcpServerBackend(opts) {
|
|
95
|
+
const storage = opts.storage ?? {};
|
|
96
|
+
if (!Number.isInteger(opts.port) || opts.port <= 0 || opts.port > 65535) {
|
|
97
|
+
throw new Error(`buildMcpServerBackend: \`port\` must be a concrete TCP port (1..65535), got ${opts.port}. Resolve \`--port 0\` via pickFreePort() before composing.`);
|
|
98
|
+
}
|
|
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.
|
|
106
|
+
const baseUrl = opts.publicBaseUrl ?? `http://${opts.host}:${opts.port}`;
|
|
107
|
+
const wsBaseUrl = baseUrl.replace(/^http(s?):\/\//, 'ws$1://');
|
|
108
|
+
// Router over the mount handler bundle. Computed once; absent
|
|
109
|
+
// (`null`) when no mounts are declared so the server falls through
|
|
110
|
+
// to agent-routed behavior.
|
|
111
|
+
const wiredActionRouter = composeWiredActionRouterFromMounts(opts.mcpMounts, () => ({
|
|
112
|
+
appId: DEFAULT_BUILDER_APP_ID,
|
|
113
|
+
requestId: randomUUID(),
|
|
114
|
+
}));
|
|
115
|
+
// Strict-auth OSS default (`devAllowAll: false`). `createGguiServer`
|
|
116
|
+
// would otherwise fall back to `InMemoryAuthAdapter({ devAllowAll:
|
|
117
|
+
// true })`, which authenticates any non-empty bearer as builder — a
|
|
118
|
+
// permissive seam that makes the `/mcp` ingress indistinguishable
|
|
119
|
+
// from "no auth at all" for operators running `ggui serve`. Pairing
|
|
120
|
+
// is on by default (see `pairing: true` below), so the honest
|
|
121
|
+
// first-run story is: mint an initial pair code → complete `/pair`
|
|
122
|
+
// → use the resulting token. The CLI surfaces that initial code as
|
|
123
|
+
// the `PAIR_CODE` boot beacon + banner line; see `runServe` in
|
|
124
|
+
// `./serve-command.ts`.
|
|
125
|
+
//
|
|
126
|
+
// `--dev-allow-all` flips this to `true` for local-dev / tunnel
|
|
127
|
+
// smoke against MCP custom connectors that probe `/mcp` without
|
|
128
|
+
// bearer (claude.ai, ChatGPT). The banner prints a loud warning
|
|
129
|
+
// and pair-code emission is suppressed (meaningless under allow-all)
|
|
130
|
+
// so operators don't accidentally take the dev posture to production.
|
|
131
|
+
// `--public-demo` shares the auth shape with `--dev-allow-all`
|
|
132
|
+
// (every bearer = builder). The CLI guarantees these are mutually
|
|
133
|
+
// exclusive at parse time; only one is ever true here.
|
|
134
|
+
const auth = new InMemoryAuthAdapter({
|
|
135
|
+
devAllowAll: (opts.devAllowAll ?? false) || (opts.publicDemo ?? false),
|
|
136
|
+
});
|
|
137
|
+
// Per-IP rate limiter — only bound under `--public-demo` so an
|
|
138
|
+
// anonymous visitor can't burn the operator's BYOK budget by pumping
|
|
139
|
+
// ggui_push. Defaults: 30 generations / 10 minutes per IP. Operators
|
|
140
|
+
// who need different ceilings construct their own backend
|
|
141
|
+
// programmatically (calling `createGguiServer({ rateLimiter })`
|
|
142
|
+
// directly). Skipped under `--dev-allow-all` because that's the
|
|
143
|
+
// single-operator escape hatch where rate limits add noise without
|
|
144
|
+
// protection.
|
|
145
|
+
const rateLimiter = opts.publicDemo
|
|
146
|
+
? new FixedWindowRateLimiter({
|
|
147
|
+
store: new InMemoryQuotaStore(),
|
|
148
|
+
limit: 30,
|
|
149
|
+
windowMs: 10 * 60 * 1000,
|
|
150
|
+
})
|
|
151
|
+
: undefined;
|
|
152
|
+
// Local-transformer embedder default. Replaces the server's
|
|
153
|
+
// `MockEmbeddingProvider` last-resort fallback for the user-facing
|
|
154
|
+
// OSS path. `bge-small-en-v1.5` (~33MB ONNX) downloads lazily into
|
|
155
|
+
// `~/.ggui/models/` on first embed; `cacheDir` resolves to that
|
|
156
|
+
// path via the CLI's own paths helper. `warmup: true` (the default
|
|
157
|
+
// inside the provider) embeds a dummy string after the pipeline
|
|
158
|
+
// first loads so the first real embedding doesn't pay the ~200-
|
|
159
|
+
// 400ms transformers cold-start tax.
|
|
160
|
+
//
|
|
161
|
+
// Programmatic embedders calling `createGguiServer` directly still
|
|
162
|
+
// get `MockEmbeddingProvider` if they don't bind an embedder — that
|
|
163
|
+
// keeps the 60MB `@huggingface/transformers` install surface off
|
|
164
|
+
// the unit-test path.
|
|
165
|
+
const embedding = createLocalEmbeddingProvider({
|
|
166
|
+
cacheDir: getEmbeddingCacheDir(),
|
|
167
|
+
});
|
|
168
|
+
// BYOK provider-key store — same `~/.ggui/credentials.json` that
|
|
169
|
+
// `byok-resolver.ts` reads on every `resolve()`. Threading the store
|
|
170
|
+
// here makes the operator-facing `/ggui/console/llm-keys` admin API
|
|
171
|
+
// write to the same file generation later reads, with no in-process
|
|
172
|
+
// sync needed (`PlaintextFileProviderKeyStore` re-reads on every
|
|
173
|
+
// `get()`). Operators who set `GGUI_CONFIG_DIR` for a clean-room get
|
|
174
|
+
// the override automatically via `getCredentialsFile()`.
|
|
175
|
+
const providerKeys = new PlaintextFileProviderKeyStore({
|
|
176
|
+
filename: getCredentialsFile(),
|
|
177
|
+
});
|
|
178
|
+
// Email-login sender selection. Reads `GGUI_EMAIL_SENDER`
|
|
179
|
+
// (default: `console`) and provider-specific env vars
|
|
180
|
+
// (`RESEND_API_KEY`, `SMTP_URL` / `SMTP_HOST` + auth, etc.). Misconfig
|
|
181
|
+
// (e.g. `=resend` without `RESEND_API_KEY`) logs a warning and falls
|
|
182
|
+
// back to console rather than crashing — the rest of the server still
|
|
183
|
+
// boots and operators can fix the env without losing other surfaces.
|
|
184
|
+
// Programmatic embedders that want a different shape compose
|
|
185
|
+
// `createGguiServer({emailLogin: {sender: ...}})` directly.
|
|
186
|
+
const emailSenderSelection = selectEmailSenderFromEnv();
|
|
187
|
+
let emailSender;
|
|
188
|
+
let emailFromAddress;
|
|
189
|
+
const defaultFromAddress = 'ggui-serve <noreply@localhost>';
|
|
190
|
+
if (emailSenderSelection.kind === 'ok') {
|
|
191
|
+
emailSender = emailSenderSelection.sender;
|
|
192
|
+
emailFromAddress = emailSenderSelection.fromAddress ?? defaultFromAddress;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// eslint-disable-next-line no-console -- one-shot boot warning, mirrors banner output
|
|
196
|
+
console.warn(`[ggui-cli] email-login: ${emailSenderSelection.reason}; falling back to ConsoleEmailSender (magic links print to terminal).`);
|
|
197
|
+
const fallback = selectEmailSenderFromEnv({
|
|
198
|
+
env: { GGUI_EMAIL_SENDER: 'console' },
|
|
199
|
+
});
|
|
200
|
+
if (fallback.kind !== 'ok') {
|
|
201
|
+
throw new Error(`[ggui-cli] email-login: console fallback selection failed: ${fallback.reason}`);
|
|
202
|
+
}
|
|
203
|
+
emailSender = fallback.sender;
|
|
204
|
+
emailFromAddress = process.env.GGUI_EMAIL_FROM?.trim() || defaultFromAddress;
|
|
205
|
+
}
|
|
206
|
+
// Resolve cross-restart HMAC secrets. When the caller declared a
|
|
207
|
+
// persistent dir, read-or-mint `bootstrap-secret.hex` +
|
|
208
|
+
// `render-signer-secret.hex` (32 bytes / 64 hex chars, 0600). Both
|
|
209
|
+
// get threaded into `createGguiServer` below so the server stops
|
|
210
|
+
// 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;
|
|
214
|
+
let persistedRenderSignerSecret;
|
|
215
|
+
if (opts.persistentDir !== undefined) {
|
|
216
|
+
persistedBootstrapSecret = readOrMintHexSecret(join(opts.persistentDir, 'bootstrap-secret.hex'));
|
|
217
|
+
persistedRenderSignerSecret = readOrMintHexSecret(join(opts.persistentDir, 'render-signer-secret.hex'));
|
|
218
|
+
}
|
|
219
|
+
// Resolve mcpInstructions: CLI flag wins over env var. Validated
|
|
220
|
+
// env-var values pass through to createGguiServer; invalid values
|
|
221
|
+
// fall through to the no-preset default with a one-line warning.
|
|
222
|
+
const envInstructions = process.env.GGUI_MCP_INSTRUCTIONS?.trim();
|
|
223
|
+
let resolvedMcpInstructions = opts.mcpInstructions;
|
|
224
|
+
if (resolvedMcpInstructions === undefined && envInstructions) {
|
|
225
|
+
if (envInstructions === 'default' ||
|
|
226
|
+
envInstructions === 'aggressive' ||
|
|
227
|
+
envInstructions === 'always' ||
|
|
228
|
+
envInstructions === 'minimal' ||
|
|
229
|
+
envInstructions === 'off') {
|
|
230
|
+
resolvedMcpInstructions = envInstructions;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// eslint-disable-next-line no-console -- one-shot boot warning
|
|
234
|
+
console.warn(`[ggui-cli] GGUI_MCP_INSTRUCTIONS='${envInstructions}' not recognized; falling back to default preset.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Per-app metadata store seeded from `ggui.json#theme.preset` (when
|
|
238
|
+
// the manifest declares a preset variant). Single-tenant OSS: any
|
|
239
|
+
// appId the handlers see picks up this default theme via
|
|
240
|
+
// `InMemoryAppMetadataStore.get`'s defaults fall-through. File-mode
|
|
241
|
+
// themes don't have a registry id to surface here — agents pick from
|
|
242
|
+
// `listThemes()` either way; the file-mode override only affects the
|
|
243
|
+
// server's bound `theme` (rendered CSS variables), not the
|
|
244
|
+
// session/handler-side `themeId` chain.
|
|
245
|
+
const manifestThemePreset = opts.theme?.source === 'preset' ? opts.theme.preset : undefined;
|
|
246
|
+
const appMetadataStore = new InMemoryAppMetadataStore({
|
|
247
|
+
...(manifestThemePreset !== undefined
|
|
248
|
+
? { defaultThemeId: manifestThemePreset }
|
|
249
|
+
: {}),
|
|
250
|
+
// `ggui.json#app.gadgets` declarations (Leaflet, Mapbox, …). When
|
|
251
|
+
// omitted, the InMemoryAppMetadataStore falls back to
|
|
252
|
+
// STDLIB_GADGETS from @ggui-ai/protocol so the first-party hooks
|
|
253
|
+
// remain available without explicit declaration. The constructor
|
|
254
|
+
// expects this on `defaultGadgets`, not `gadgets`.
|
|
255
|
+
...(opts.gadgets !== undefined && opts.gadgets.length > 0
|
|
256
|
+
? { defaultGadgets: opts.gadgets }
|
|
257
|
+
: {}),
|
|
258
|
+
// `ggui.json#app.publicEnv` operator-stamped values for wrapper
|
|
259
|
+
// hooks to read via `getPublicEnv()`. The push gate
|
|
260
|
+
// (`assertPublicEnvSatisfied`) verifies every declared wrapper's
|
|
261
|
+
// `requires` keys are present in this map BEFORE the iframe boots.
|
|
262
|
+
...(opts.publicEnv !== undefined && Object.keys(opts.publicEnv).length > 0
|
|
263
|
+
? { defaultPublicEnv: opts.publicEnv }
|
|
264
|
+
: {}),
|
|
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 }
|
|
270
|
+
: {}),
|
|
271
|
+
});
|
|
272
|
+
// Materialize the vectorStore here so the marketplace-install
|
|
273
|
+
// bridge writes into the SAME instance the matcher reads. Without
|
|
274
|
+
// this, calling `createGguiServer({ vectors })`
|
|
275
|
+
// when no `storage.vectors` is declared leaves vectorStore
|
|
276
|
+
// construction inside the server, hidden from us, so the provider
|
|
277
|
+
// would have no handle to register into. Mint an `InMemoryVectorStore`
|
|
278
|
+
// here on the absent-storage path; threaded back as `vectors` to
|
|
279
|
+
// `createGguiServer` so server + provider share state.
|
|
280
|
+
const vectorStore = storage.vectors ?? new InMemoryVectorStore();
|
|
281
|
+
// Construct the marketplace-install bridge BEFORE `createGguiServer`
|
|
282
|
+
// so we can fold it into `opts.generation.installedBlueprints` below.
|
|
283
|
+
// Per design lock (project_slices_5_6_7_plan.md), the provider
|
|
284
|
+
// lazily compiles every installed-blueprint entry on the first
|
|
285
|
+
// `matchBlueprint` call per scope. Idempotent + best-effort per
|
|
286
|
+
// entry — a broken installed-blueprint never sinks the match flow.
|
|
287
|
+
let installedBlueprintsProvider;
|
|
288
|
+
if (opts.installedBlueprints && opts.installedBlueprints.entries.length > 0) {
|
|
289
|
+
const { projectRoot, entries } = opts.installedBlueprints;
|
|
290
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
291
|
+
const providerOptions = {
|
|
292
|
+
installedBlueprints: () => entries.flatMap((entry) => {
|
|
293
|
+
if (entry.manifest.contract === undefined)
|
|
294
|
+
return [];
|
|
295
|
+
const intent = entry.manifest.description ??
|
|
296
|
+
entry.manifest.name ??
|
|
297
|
+
entry.id;
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
id: entry.id,
|
|
301
|
+
manifestPath: entry.manifestPath,
|
|
302
|
+
contract: entry.manifest.contract,
|
|
303
|
+
intent,
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
}),
|
|
307
|
+
compile: async (entry) => {
|
|
308
|
+
const ui = byId.get(entry.id);
|
|
309
|
+
if (!ui) {
|
|
310
|
+
return { kind: 'missing-entry', tried: [] };
|
|
311
|
+
}
|
|
312
|
+
const result = await compileUiOnDemand({
|
|
313
|
+
projectRoot,
|
|
314
|
+
manifestPath: ui.manifestPath,
|
|
315
|
+
manifest: ui.manifest,
|
|
316
|
+
});
|
|
317
|
+
if (result.kind === 'ok') {
|
|
318
|
+
return { kind: 'ok', code: result.code };
|
|
319
|
+
}
|
|
320
|
+
if (result.kind === 'missing-entry') {
|
|
321
|
+
return { kind: 'missing-entry', tried: result.tried };
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
kind: 'failure',
|
|
325
|
+
errors: result.errors.map((m) => m.text),
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
deps: { embedding, vectorStore },
|
|
329
|
+
onIssue: (issue) => {
|
|
330
|
+
process.stderr.write(`[ggui serve] installed-blueprint ${issue.id}: ${issue.kind}: ${issue.message}\n`);
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
installedBlueprintsProvider =
|
|
334
|
+
createInstalledBlueprintsProvider(providerOptions);
|
|
335
|
+
}
|
|
336
|
+
// Fold the bridge into the generation deps so the push handler +
|
|
337
|
+
// handshake negotiator both consume it via `MatchBlueprintDeps.
|
|
338
|
+
// installedBlueprints`. Two-step pattern keeps the createGguiServer
|
|
339
|
+
// call below unchanged when no provider is wired.
|
|
340
|
+
const generationWithInstalled = opts.generation
|
|
341
|
+
? installedBlueprintsProvider
|
|
342
|
+
? {
|
|
343
|
+
...opts.generation,
|
|
344
|
+
installedBlueprints: installedBlueprintsProvider,
|
|
345
|
+
}
|
|
346
|
+
: opts.generation
|
|
347
|
+
: undefined;
|
|
348
|
+
const server = createGguiServer({
|
|
349
|
+
auth,
|
|
350
|
+
embedding,
|
|
351
|
+
providerKeys,
|
|
352
|
+
// Seeded per-app metadata store. Threaded so the in-process
|
|
353
|
+
// singleton is shared across `ggui_list_gadgets`,
|
|
354
|
+
// `ggui_list_themes`, `ggui_new_session` (theme default
|
|
355
|
+
// resolution), and the handshake (gadgets lookup).
|
|
356
|
+
appMetadataStore,
|
|
357
|
+
// Theme catalog resolver. Reads from `@ggui-ai/design`'s registry
|
|
358
|
+
// each call so runtime additions surface without a restart.
|
|
359
|
+
// Co-binding with `appMetadataStore` mounts `ggui_list_themes` on
|
|
360
|
+
// the agent route.
|
|
361
|
+
themes: () => listThemes(),
|
|
362
|
+
// Cross-restart HMAC secrets. Present iff the caller declared a
|
|
363
|
+
// persistent dir. Both threaded so the next restart can verify
|
|
364
|
+
// tokens minted by the previous run instead of regenerating per
|
|
365
|
+
// process. Render-signer.secret rides on the `renderSigning`
|
|
366
|
+
// discriminated union (the `false` shape disables the layer
|
|
367
|
+
// entirely; we always want it on here, so build the object form).
|
|
368
|
+
...(persistedBootstrapSecret !== undefined
|
|
369
|
+
? { bootstrapSecret: persistedBootstrapSecret }
|
|
370
|
+
: {}),
|
|
371
|
+
...(persistedRenderSignerSecret !== undefined
|
|
372
|
+
? { renderSigning: { secret: persistedRenderSignerSecret } }
|
|
373
|
+
: {}),
|
|
374
|
+
...(resolvedMcpInstructions !== undefined
|
|
375
|
+
? { mcpInstructions: resolvedMcpInstructions }
|
|
376
|
+
: {}),
|
|
377
|
+
// `--multi-tenant` flips the `/ggui/console/llm-keys` gate from
|
|
378
|
+
// admin-token to auth-adapter. Default scope derivation in the
|
|
379
|
+
// server picks up `userId` / `appId` from the resolved identity;
|
|
380
|
+
// operators with composite scopes pass their own `providerKeyScope`
|
|
381
|
+
// by composing `createGguiServer` directly.
|
|
382
|
+
...(opts.multiTenant
|
|
383
|
+
? { providerKeysGate: 'auth-adapter' }
|
|
384
|
+
: {}),
|
|
385
|
+
// Gate OAuth login routes on publicBaseUrl. Without it the
|
|
386
|
+
// redirect_uri can't be composed; the admin transport still mounts
|
|
387
|
+
// so operators can paste credentials in advance.
|
|
388
|
+
...(opts.publicBaseUrl ? { publicBaseUrl: opts.publicBaseUrl } : {}),
|
|
389
|
+
...(rateLimiter ? { rateLimiter } : {}),
|
|
390
|
+
...(storage.sessionStore ? { sessionStore: storage.sessionStore } : {}),
|
|
391
|
+
// Always pass the materialized vectorStore so the install bridge
|
|
392
|
+
// and the matcher share state. Passing only `storage.vectors`
|
|
393
|
+
// (set when the manifest declares sqlite) would, on an absent
|
|
394
|
+
// declaration, leave the server to mint its own internal
|
|
395
|
+
// InMemoryVectorStore that the bridge couldn't see.
|
|
396
|
+
vectors: vectorStore,
|
|
397
|
+
// `threads:` is an opt-in on createGguiServer (no zero-config
|
|
398
|
+
// fallback — threads without an explicit store wouldn't mount
|
|
399
|
+
// routes at all). We only pass it when the manifest declares a
|
|
400
|
+
// store; absent declaration → no thread routes.
|
|
401
|
+
//
|
|
402
|
+
// Durability claim is whatever the resolver surfaced: `'durable'`
|
|
403
|
+
// for sqlite, `'ephemeral'` for memory. The server echoes this
|
|
404
|
+
// through `/ggui/health`, so Portal's caveat logic stays honest.
|
|
405
|
+
...(storage.threadStore
|
|
406
|
+
? {
|
|
407
|
+
threads: {
|
|
408
|
+
store: storage.threadStore,
|
|
409
|
+
...(storage.threadDurability
|
|
410
|
+
? { durability: storage.threadDurability }
|
|
411
|
+
: {}),
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
: {}),
|
|
415
|
+
sessionChannel: true,
|
|
416
|
+
pairing: opts.keysFile
|
|
417
|
+
? { persistencePath: opts.keysFile }
|
|
418
|
+
: true,
|
|
419
|
+
// Email magic-link login. OSS first-run default mirrors
|
|
420
|
+
// `provisionalPreview` — wire a sender so magic links can be
|
|
421
|
+
// delivered. `selectEmailSenderFromEnv` reads
|
|
422
|
+
// `GGUI_EMAIL_SENDER` (default: `console`, terminal-only) and
|
|
423
|
+
// optionally `RESEND_API_KEY` / `SMTP_*` to construct a real
|
|
424
|
+
// sender. Misconfig is non-fatal: we log a warning + fall back
|
|
425
|
+
// to console so the boot path stays alive.
|
|
426
|
+
//
|
|
427
|
+
// Routes only mount when `publicBaseUrl` is set (the verify URL
|
|
428
|
+
// can't resolve otherwise) — the server logs
|
|
429
|
+
// `email_login_skipped` in that case and `/login` falls back to
|
|
430
|
+
// OAuth + pair-code. Hosted + programmatic embedders pass their
|
|
431
|
+
// own sender by composing `createGguiServer` directly.
|
|
432
|
+
emailLogin: {
|
|
433
|
+
sender: emailSender,
|
|
434
|
+
fromAddress: emailFromAddress,
|
|
435
|
+
},
|
|
436
|
+
// OAuth 2.1 + PKCE + DCR. Opt-in via `--oauth` because the routes
|
|
437
|
+
// add an HTML paste-key page + 4 well-known endpoints — not free
|
|
438
|
+
// attack surface for operators who only consume from pure-bearer
|
|
439
|
+
// clients (Claude Desktop, the console). Required for MCP custom-
|
|
440
|
+
// connector hosts (claude.ai, ChatGPT) whose form has no bearer
|
|
441
|
+
// field; flag flips on `oauth: true` defaults (in-memory storage,
|
|
442
|
+
// built-in paste-key consent page).
|
|
443
|
+
...(opts.oauth ? { oauth: true } : {}),
|
|
444
|
+
console: {
|
|
445
|
+
sessionCookie: true,
|
|
446
|
+
...(opts.adminToken !== undefined ? { adminToken: opts.adminToken } : {}),
|
|
447
|
+
// First-run onboarding redirect. When neither the credentials
|
|
448
|
+
// file (`~/.ggui/credentials.json`) nor any provider env var
|
|
449
|
+
// (ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY /
|
|
450
|
+
// OPENROUTER_API_KEY) is present, send `GET /` to the
|
|
451
|
+
// **admin** onboarding flow — the operator running `ggui serve`
|
|
452
|
+
// IS the admin on first-run, so they need the operator-key
|
|
453
|
+
// plane (`/admin/llm-keys`) and the assistant-connection card
|
|
454
|
+
// surfaced there. Cookie-gated end-user `/settings` is for a
|
|
455
|
+
// *separate* user paired with the server later, not the
|
|
456
|
+
// first-run operator. Once any key is configured, the next
|
|
457
|
+
// visit serves the SPA normally. Scoped to the root path;
|
|
458
|
+
// deep links bypass the redirect.
|
|
459
|
+
landingRedirect: () => {
|
|
460
|
+
try {
|
|
461
|
+
const credsFileExists = existsSync(getCredentialsFile());
|
|
462
|
+
const anyEnvKey = !!process.env.ANTHROPIC_API_KEY ||
|
|
463
|
+
!!process.env.OPENAI_API_KEY ||
|
|
464
|
+
!!process.env.GOOGLE_API_KEY ||
|
|
465
|
+
!!process.env.OPENROUTER_API_KEY;
|
|
466
|
+
if (!credsFileExists && !anyEnvKey) {
|
|
467
|
+
return '/admin-login?next=%2Fadmin%2Fllm-keys';
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
// Public welcome page at `/` — operator identification + public
|
|
477
|
+
// deep-link surfaces + operator-login affordance. Resolved from
|
|
478
|
+
// `ggui.json#operator` + `ggui.json#app.name` by serve-command.ts;
|
|
479
|
+
// absent = no welcome page (SPA index handler still owns `/`).
|
|
480
|
+
...(opts.welcomePage ? { welcomePage: opts.welcomePage } : {}),
|
|
481
|
+
// 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.
|
|
486
|
+
shortCodeIndex: opts.shortCodeIndex ?? new InMemoryShortCodeIndex(),
|
|
487
|
+
// Content-addressable componentCode delivery. The OSS dev default
|
|
488
|
+
// is filesystem-backed at `~/.ggui/code-cache/` (override via
|
|
489
|
+
// `GGUI_CODE_CACHE_DIR`). Survives `ggui serve` restart so
|
|
490
|
+
// claude.ai's iframe cache still resolves URLs minted before the
|
|
491
|
+
// restart. Operators can `rm -rf` the cache at any time —
|
|
492
|
+
// immutable URLs guarantee a fresh push repopulates as needed.
|
|
493
|
+
codeStore: new FileSystemCodeStore({ root: getCodeCacheDir() }),
|
|
494
|
+
// Manifest-declared blueprints. Only present when `ggui serve`
|
|
495
|
+
// found UIs in `blueprints.include`; absent = the server's
|
|
496
|
+
// `ggui_list_featured_blueprints` returns an empty catalog (the
|
|
497
|
+
// zero-config default).
|
|
498
|
+
...(opts.blueprintProvider
|
|
499
|
+
? { blueprintProvider: opts.blueprintProvider }
|
|
500
|
+
: {}),
|
|
501
|
+
// Manifest-backed UI registry — resolves blueprint ids to
|
|
502
|
+
// compiled bundles for `ggui_render_blueprint`. Paired with
|
|
503
|
+
// `blueprintProvider` above (metadata vs. bundle resolution on
|
|
504
|
+
// the same source). Absent = render tool not registered.
|
|
505
|
+
...(opts.uiRegistry ? { uiRegistry: opts.uiRegistry } : {}),
|
|
506
|
+
// Manifest-declared primitive catalogs. Present when `ggui serve`
|
|
507
|
+
// resolved `primitives.{packages,local}`; absent = in-memory-only
|
|
508
|
+
// server with an empty `server.primitiveCatalogs`.
|
|
509
|
+
...(opts.primitiveCatalogs
|
|
510
|
+
? { primitiveCatalogs: opts.primitiveCatalogs }
|
|
511
|
+
: {}),
|
|
512
|
+
// Manifest-declared theme. Present when `ggui serve` resolved
|
|
513
|
+
// `ggui.json#theme`; absent = `createGguiServer` falls back to its
|
|
514
|
+
// `lightTheme`-backed default internally.
|
|
515
|
+
...(opts.theme ? { theme: opts.theme } : {}),
|
|
516
|
+
// Console theme picker writer — paired with the admin-gated
|
|
517
|
+
// `/ggui/console/theme` POST. Absent = picker UI surfaces a
|
|
518
|
+
// read-only banner + POST returns 501. CLI sets this when
|
|
519
|
+
// `plan.projectRoot` resolved to a manifest path on disk.
|
|
520
|
+
...(opts.themeWriter ? { themeWriter: opts.themeWriter } : {}),
|
|
521
|
+
// Live-theme wiring — `themeProvider` reads from a shared cell
|
|
522
|
+
// that `onThemeConfigChange` writes to on every console save,
|
|
523
|
+
// so the picker's "Save to ggui.json" reaches the next push's
|
|
524
|
+
// bootstrap envelope without a restart. Both threaded through
|
|
525
|
+
// when present; absent = boot-baked behaviour (legacy).
|
|
526
|
+
...(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).
|
|
534
|
+
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
|
+
wsUrl: `${wsBaseUrl}/ws`,
|
|
543
|
+
},
|
|
544
|
+
// Task #382 — `runtimeUrl` MUST be absolute. The thin-shell HTML
|
|
545
|
+
// served from `ui://ggui/session` is mounted via `srcdoc` in most
|
|
546
|
+
// consumers (`<McpAppIframe>` default path for inline text), which
|
|
547
|
+
// gives the iframe `about:srcdoc` as its URL — a relative path
|
|
548
|
+
// like `/_ggui/iframe-runtime.js` resolves against `about:` and the
|
|
549
|
+
// `<script src>` fetch silently fails. Publishing the absolute
|
|
550
|
+
// URL built from the CLI's own known `baseUrl` makes srcdoc mount
|
|
551
|
+
// work without operator action. The server still serves the bundle
|
|
552
|
+
// at `/_ggui/iframe-runtime.js` under `runtimePath`; this overrides
|
|
553
|
+
// only the URL published on `_meta.ggui.bootstrap.runtimeUrl`.
|
|
554
|
+
runtime: { url: `${baseUrl}${RUNTIME_BUNDLE_URL_PATH}` },
|
|
555
|
+
// OSS first-run default: provisional A2UI preview is on. The
|
|
556
|
+
// deterministic emitter from `@ggui-ai/preview-a2ui/emitters`
|
|
557
|
+
// adapts each push's `story.intent` into a small A2UI surface
|
|
558
|
+
// (heading → shell → teardown) and streams it on
|
|
559
|
+
// `_ggui:preview`. Embedded-ui's `/s/<shortCode>` viewer mounts
|
|
560
|
+
// `<ProvisionalRenderer>` to paint the frames as they arrive,
|
|
561
|
+
// so operators see a live UI build-up the first time they
|
|
562
|
+
// invoke `ggui_push` — before any LLM generation lands on OSS.
|
|
563
|
+
//
|
|
564
|
+
// Hosted + programmatic hosts opt-in by composing
|
|
565
|
+
// `createGguiServer()` directly and supplying their own
|
|
566
|
+
// emitter (e.g. Haiku-backed); the CLI keeps the OSS story
|
|
567
|
+
// honest with a zero-config default that fires.
|
|
568
|
+
provisionalPreview: {
|
|
569
|
+
enabled: true,
|
|
570
|
+
emitter: createDeterministicPreviewEmitter(),
|
|
571
|
+
},
|
|
572
|
+
// Generation wiring. Threaded through to
|
|
573
|
+
// `defaultHandlers.push.generation` → `createGguiPushHandler`.
|
|
574
|
+
// Absent = placeholder mode: push creates sessions + shortCodes
|
|
575
|
+
// + preview but does NOT produce componentCode. The CLI's
|
|
576
|
+
// `runServeCommand` probes BYOK (env → credentials file) at
|
|
577
|
+
// boot and only supplies this opt on a hit. See
|
|
578
|
+
// `./generation-probe.ts`.
|
|
579
|
+
...(generationWithInstalled ? { generation: generationWithInstalled } : {}),
|
|
580
|
+
// Mount aggregation. External SharedHandler bundles appended onto
|
|
581
|
+
// `/mcp` alongside ggui-native tools. No CLI config loader yet —
|
|
582
|
+
// programmatic hosts + integration tests compose mounts through
|
|
583
|
+
// this opt directly.
|
|
584
|
+
...(opts.mcpMounts && opts.mcpMounts.length > 0
|
|
585
|
+
? { mcpMounts: opts.mcpMounts }
|
|
586
|
+
: {}),
|
|
587
|
+
// wiredActionRouter composed from the same mount handlers. With
|
|
588
|
+
// mounts present, every mounted tool becomes wire-dispatchable
|
|
589
|
+
// from a generated UI's `actionSpec[name].dispatch.tool` (when
|
|
590
|
+
// `dispatch.kind === 'tool'`) — the "zero agent code" path.
|
|
591
|
+
// Without mounts, no router is wired and inbound actions stay on
|
|
592
|
+
// the agent-routed path.
|
|
593
|
+
...(wiredActionRouter ? { wiredActionRouter } : {}),
|
|
594
|
+
// Schema-compat mode. Defaults to `'reject'` inside
|
|
595
|
+
// `createGguiServer`; the env override is the operator's escape
|
|
596
|
+
// hatch for scenarios that intentionally ship a blueprint with a
|
|
597
|
+
// schema mismatch (e.g. contract-probe fixtures that test runtime
|
|
598
|
+
// TOOL_THREW / SCHEMA_VIOLATION error paths — `'reject'` would
|
|
599
|
+
// otherwise block the stack item at push/try-live time before
|
|
600
|
+
// runtime gets to emit the envelope under test).
|
|
601
|
+
//
|
|
602
|
+
// Accepted values match `SchemaCompatMode`: `'reject' | 'warn' |
|
|
603
|
+
// 'off'`. Unrecognized values fall through to the server's
|
|
604
|
+
// default. Keeping the parse narrow so a typo yields default
|
|
605
|
+
// 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'
|
|
609
|
+
? {
|
|
610
|
+
schemaCompatCheck: process.env['GGUI_SCHEMA_COMPAT_MODE'],
|
|
611
|
+
}
|
|
612
|
+
: {}),
|
|
613
|
+
// Wired-tool per-call timeout override (default 30 s in
|
|
614
|
+
// `session-channel.ts::DEFAULT_WIRED_TOOL_TIMEOUT_MS`). Mirrors the
|
|
615
|
+
// `GGUI_SCHEMA_COMPAT_MODE` escape-hatch shape: env-only; absent or
|
|
616
|
+
// unparseable falls through to the server's default. Test harnesses
|
|
617
|
+
// exercising TOOL_TIMEOUT envelopes lower this to keep per-test
|
|
618
|
+
// budgets small (the conformance fixture
|
|
619
|
+
// `wired-action-tool-timeout.json` deliberately wires a hanging
|
|
620
|
+
// tool; the default 30 s wait would dominate a Lane-1 spec budget).
|
|
621
|
+
//
|
|
622
|
+
// Parse uses `Number()` + finite + positive-integer checks; a typo
|
|
623
|
+
// (`"abc"`, `"-100"`, `"NaN"`) falls through silently to default.
|
|
624
|
+
// Operators that need a different production timeout SHOULD compose
|
|
625
|
+
// `createGguiServer` directly rather than rely on env coupling.
|
|
626
|
+
...(() => {
|
|
627
|
+
const raw = process.env['GGUI_WIRED_TIMEOUT_MS'];
|
|
628
|
+
if (raw === undefined || raw === '')
|
|
629
|
+
return {};
|
|
630
|
+
const parsed = Number(raw);
|
|
631
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
632
|
+
return {};
|
|
633
|
+
return { wiredActionTimeoutMs: Math.floor(parsed) };
|
|
634
|
+
})(),
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
listen: async (port, host) => {
|
|
638
|
+
const httpServer = await server.listen(port, host);
|
|
639
|
+
const addr = httpServer.address();
|
|
640
|
+
if (!addr || typeof addr === 'string') {
|
|
641
|
+
throw new Error('server.address() returned an unexpected shape');
|
|
642
|
+
}
|
|
643
|
+
return addr.port;
|
|
644
|
+
},
|
|
645
|
+
close: () => server.close(),
|
|
646
|
+
toolCount: server.toolCount,
|
|
647
|
+
serverName: 'ggui-mcp-server',
|
|
648
|
+
serverVersion: opts.cliVersion,
|
|
649
|
+
primitiveCatalogCount: server.primitiveCatalogs.length,
|
|
650
|
+
themeSource: server.theme.source,
|
|
651
|
+
// Always non-null in the CLI bundle because `pairing: true` is
|
|
652
|
+
// wired above. Exposed through the narrow `ServeBackend` contract
|
|
653
|
+
// so `runServe` can pre-mint an initial pair code after bind
|
|
654
|
+
// without reaching into `GguiServer`.
|
|
655
|
+
pairingService: server.pairingService,
|
|
656
|
+
// Always non-null in the CLI bundle because `console` is wired
|
|
657
|
+
// above (sessionCookie: true). Surfaced so the CLI banner can
|
|
658
|
+
// print the operator-facing value alongside PAIR_CODE.
|
|
659
|
+
adminToken: server.adminToken,
|
|
660
|
+
// Surface the embedding model id so `describeServeBanner` can
|
|
661
|
+
// print a `rag` line. Read off the provider's `id` field — local
|
|
662
|
+
// provider returns the model id (e.g. `Xenova/bge-small-en-v1.5`);
|
|
663
|
+
// MockEmbeddingProvider returns `'mock-sine'`. The banner only
|
|
664
|
+
// shows the line for real models (this CLI bundle always wires
|
|
665
|
+
// the local provider, so the line always appears in `ggui serve`).
|
|
666
|
+
embeddingModel: embedding.id,
|
|
667
|
+
};
|
|
668
|
+
}
|