@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.
Files changed (117) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +96 -0
  3. package/dist/agent-resolution.d.ts +41 -0
  4. package/dist/agent-resolution.d.ts.map +1 -0
  5. package/dist/agent-resolution.js +80 -0
  6. package/dist/api-client.d.ts +59 -0
  7. package/dist/api-client.d.ts.map +1 -0
  8. package/dist/api-client.js +145 -0
  9. package/dist/auth-keys.d.ts +10 -0
  10. package/dist/auth-keys.d.ts.map +1 -0
  11. package/dist/auth-keys.js +382 -0
  12. package/dist/auth-login.d.ts +11 -0
  13. package/dist/auth-login.d.ts.map +1 -0
  14. package/dist/auth-login.js +231 -0
  15. package/dist/auth-logout.d.ts +3 -0
  16. package/dist/auth-logout.d.ts.map +1 -0
  17. package/dist/auth-logout.js +32 -0
  18. package/dist/auth-store.d.ts +67 -0
  19. package/dist/auth-store.d.ts.map +1 -0
  20. package/dist/auth-store.js +150 -0
  21. package/dist/auth-whoami.d.ts +3 -0
  22. package/dist/auth-whoami.d.ts.map +1 -0
  23. package/dist/auth-whoami.js +52 -0
  24. package/dist/blueprint-command.d.ts +3 -0
  25. package/dist/blueprint-command.d.ts.map +1 -0
  26. package/dist/blueprint-command.js +203 -0
  27. package/dist/blueprint-create.d.ts +71 -0
  28. package/dist/blueprint-create.d.ts.map +1 -0
  29. package/dist/blueprint-create.js +377 -0
  30. package/dist/byok-resolver.d.ts +141 -0
  31. package/dist/byok-resolver.d.ts.map +1 -0
  32. package/dist/byok-resolver.js +89 -0
  33. package/dist/cli.d.ts +3 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +1066 -0
  36. package/dist/dev-command.d.ts +169 -0
  37. package/dist/dev-command.d.ts.map +1 -0
  38. package/dist/dev-command.js +222 -0
  39. package/dist/gadget-command.d.ts +3 -0
  40. package/dist/gadget-command.d.ts.map +1 -0
  41. package/dist/gadget-command.js +181 -0
  42. package/dist/gadget-create.d.ts +106 -0
  43. package/dist/gadget-create.d.ts.map +1 -0
  44. package/dist/gadget-create.js +492 -0
  45. package/dist/generation-probe.d.ts +135 -0
  46. package/dist/generation-probe.d.ts.map +1 -0
  47. package/dist/generation-probe.js +132 -0
  48. package/dist/internal/artifact-install.d.ts +150 -0
  49. package/dist/internal/artifact-install.d.ts.map +1 -0
  50. package/dist/internal/artifact-install.js +1019 -0
  51. package/dist/internal/artifact-publish.d.ts +204 -0
  52. package/dist/internal/artifact-publish.d.ts.map +1 -0
  53. package/dist/internal/artifact-publish.js +1141 -0
  54. package/dist/internal/artifact-search.d.ts +155 -0
  55. package/dist/internal/artifact-search.d.ts.map +1 -0
  56. package/dist/internal/artifact-search.js +539 -0
  57. package/dist/internal/artifact-uninstall.d.ts +30 -0
  58. package/dist/internal/artifact-uninstall.d.ts.map +1 -0
  59. package/dist/internal/artifact-uninstall.js +205 -0
  60. package/dist/internal/auth-cache.d.ts +40 -0
  61. package/dist/internal/auth-cache.d.ts.map +1 -0
  62. package/dist/internal/auth-cache.js +133 -0
  63. package/dist/internal/auth-strategy.d.ts +89 -0
  64. package/dist/internal/auth-strategy.d.ts.map +1 -0
  65. package/dist/internal/auth-strategy.js +132 -0
  66. package/dist/internal/ggui-json.d.ts +53 -0
  67. package/dist/internal/ggui-json.d.ts.map +1 -0
  68. package/dist/internal/ggui-json.js +92 -0
  69. package/dist/internal/key-store.d.ts +43 -0
  70. package/dist/internal/key-store.d.ts.map +1 -0
  71. package/dist/internal/key-store.js +128 -0
  72. package/dist/internal/oidc-token.d.ts +52 -0
  73. package/dist/internal/oidc-token.d.ts.map +1 -0
  74. package/dist/internal/oidc-token.js +351 -0
  75. package/dist/internal/register-author-key.d.ts +44 -0
  76. package/dist/internal/register-author-key.d.ts.map +1 -0
  77. package/dist/internal/register-author-key.js +212 -0
  78. package/dist/local-keys.d.ts +47 -0
  79. package/dist/local-keys.d.ts.map +1 -0
  80. package/dist/local-keys.js +115 -0
  81. package/dist/mcp-backend.d.ts +356 -0
  82. package/dist/mcp-backend.d.ts.map +1 -0
  83. package/dist/mcp-backend.js +668 -0
  84. package/dist/paths.d.ts +77 -0
  85. package/dist/paths.d.ts.map +1 -0
  86. package/dist/paths.js +119 -0
  87. package/dist/persistent-secrets.d.ts +28 -0
  88. package/dist/persistent-secrets.d.ts.map +1 -0
  89. package/dist/persistent-secrets.js +94 -0
  90. package/dist/persistent-short-code-index.d.ts +14 -0
  91. package/dist/persistent-short-code-index.d.ts.map +1 -0
  92. package/dist/persistent-short-code-index.js +39 -0
  93. package/dist/persistent-stores.d.ts +12 -0
  94. package/dist/persistent-stores.d.ts.map +1 -0
  95. package/dist/persistent-stores.js +42 -0
  96. package/dist/serve-command.d.ts +500 -0
  97. package/dist/serve-command.d.ts.map +1 -0
  98. package/dist/serve-command.js +616 -0
  99. package/dist/serve-fallback.d.ts +64 -0
  100. package/dist/serve-fallback.d.ts.map +1 -0
  101. package/dist/serve-fallback.js +122 -0
  102. package/dist/spawn-cloudflared.d.ts +61 -0
  103. package/dist/spawn-cloudflared.d.ts.map +1 -0
  104. package/dist/spawn-cloudflared.js +167 -0
  105. package/dist/theme-command.d.ts +4 -0
  106. package/dist/theme-command.d.ts.map +1 -0
  107. package/dist/theme-command.js +154 -0
  108. package/dist/theme-file-uploader.d.ts +8 -0
  109. package/dist/theme-file-uploader.d.ts.map +1 -0
  110. package/dist/theme-file-uploader.js +58 -0
  111. package/dist/theme-writer.d.ts +8 -0
  112. package/dist/theme-writer.d.ts.map +1 -0
  113. package/dist/theme-writer.js +103 -0
  114. package/dist/tunnel-provider.d.ts +273 -0
  115. package/dist/tunnel-provider.d.ts.map +1 -0
  116. package/dist/tunnel-provider.js +142 -0
  117. package/package.json +81 -0
@@ -0,0 +1,616 @@
1
+ import { launchBrowser } from './dev-command.js';
2
+ /**
3
+ * Default bind port. Distinct from `ggui dev`'s 6780 so both can run
4
+ * simultaneously without colliding. Not a registered port; chosen for
5
+ * memorability + non-collision with the dev hub.
6
+ */
7
+ export const DEFAULT_SERVE_PORT = 6781;
8
+ /** Default bind host. Loopback only — matches `ggui dev`. */
9
+ export const DEFAULT_SERVE_HOST = '127.0.0.1';
10
+ /**
11
+ * Parse the raw `ggui serve` argv tail. Returns a discriminated
12
+ * shape — `error` field is non-undefined on malformed input.
13
+ *
14
+ * Flag surface per the §10.2a lock:
15
+ * - `--port <n>` / `--host <addr>`: bind target.
16
+ * - `--mcp-only`: opt-down to MCP-only.
17
+ * - `--all`: REJECTED with a pointer to the real default. See doc lock.
18
+ * - `--help` / `-h`: help sentinel.
19
+ */
20
+ export function parseServeFlags(args) {
21
+ const out = {
22
+ port: DEFAULT_SERVE_PORT,
23
+ host: DEFAULT_SERVE_HOST,
24
+ mcpOnly: false,
25
+ devAllowAll: false,
26
+ publicDemo: false,
27
+ multiTenant: false,
28
+ oauth: false,
29
+ };
30
+ for (let i = 0; i < args.length; i++) {
31
+ const arg = args[i];
32
+ if (arg === '--port') {
33
+ const v = args[++i];
34
+ if (v === undefined) {
35
+ return { ...out, error: '--port requires a value' };
36
+ }
37
+ const n = Number(v);
38
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
39
+ return {
40
+ ...out,
41
+ error: `--port must be an integer in [0, 65535], got "${v}"`,
42
+ };
43
+ }
44
+ out.port = n;
45
+ continue;
46
+ }
47
+ if (arg === '--host') {
48
+ const v = args[++i];
49
+ if (v === undefined || v === '') {
50
+ return { ...out, error: '--host requires a value' };
51
+ }
52
+ out.host = v;
53
+ continue;
54
+ }
55
+ if (arg === '--mcp-only') {
56
+ out.mcpOnly = true;
57
+ continue;
58
+ }
59
+ if (arg === '--dev-allow-all') {
60
+ out.devAllowAll = true;
61
+ continue;
62
+ }
63
+ if (arg === '--public-demo') {
64
+ out.publicDemo = true;
65
+ continue;
66
+ }
67
+ if (arg === '--multi-tenant') {
68
+ out.multiTenant = true;
69
+ continue;
70
+ }
71
+ if (arg === '--oauth') {
72
+ out.oauth = true;
73
+ continue;
74
+ }
75
+ if (arg === '--public-base-url') {
76
+ const v = args[++i];
77
+ if (v === undefined || v === '') {
78
+ return { ...out, error: '--public-base-url requires a value' };
79
+ }
80
+ if (!/^https?:\/\//.test(v)) {
81
+ return {
82
+ ...out,
83
+ error: `--public-base-url must start with http:// or https://, got "${v}"`,
84
+ };
85
+ }
86
+ // Strip trailing slash so callers don't have to think about
87
+ // `${baseUrl}/s/` vs `${baseUrl}//s/` — both are common typos.
88
+ out.publicBaseUrl = v.replace(/\/+$/, '');
89
+ continue;
90
+ }
91
+ if (arg === '--public') {
92
+ out.publicAutoTunnel = true;
93
+ continue;
94
+ }
95
+ if (arg === '--i-know-its-public') {
96
+ out.iKnowItsPublic = true;
97
+ continue;
98
+ }
99
+ if (arg === '--no-open') {
100
+ out.noOpen = true;
101
+ continue;
102
+ }
103
+ if (arg === '--mcp-instructions') {
104
+ const v = args[++i];
105
+ if (v === undefined || v === '') {
106
+ return { ...out, error: '--mcp-instructions requires a value' };
107
+ }
108
+ if (v !== 'default' &&
109
+ v !== 'aggressive' &&
110
+ v !== 'always' &&
111
+ v !== 'minimal' &&
112
+ v !== 'off') {
113
+ return {
114
+ ...out,
115
+ error: `--mcp-instructions must be one of [default, aggressive, always, minimal, off], got "${v}"`,
116
+ };
117
+ }
118
+ out.mcpInstructions = v;
119
+ continue;
120
+ }
121
+ if (arg === '--keys-file') {
122
+ const v = args[++i];
123
+ if (v === undefined || v === '') {
124
+ return { ...out, error: '--keys-file requires a value' };
125
+ }
126
+ out.keysFile = v;
127
+ continue;
128
+ }
129
+ if (arg === '--admin-token') {
130
+ const v = args[++i];
131
+ if (v === undefined || v === '') {
132
+ return { ...out, error: '--admin-token requires a value' };
133
+ }
134
+ out.adminToken = v;
135
+ continue;
136
+ }
137
+ if (arg === '--ephemeral') {
138
+ out.ephemeral = true;
139
+ continue;
140
+ }
141
+ if (arg === '--all') {
142
+ // §10.2a lock: --all is NOT a first-class flag. The default
143
+ // already runs everything. Reject with a pointer so users
144
+ // learn the real mental model in one interaction.
145
+ return {
146
+ ...out,
147
+ error: '--all is not a flag. The default already runs everything (MCP + agent). ' +
148
+ 'Use --mcp-only to run just the MCP server.',
149
+ };
150
+ }
151
+ if (arg === '--help' || arg === '-h') {
152
+ return { ...out, error: '__help__' };
153
+ }
154
+ return { ...out, error: `unknown option "${arg ?? ''}"` };
155
+ }
156
+ if (out.devAllowAll && out.publicDemo) {
157
+ return {
158
+ ...out,
159
+ error: '--dev-allow-all and --public-demo are mutually exclusive. ' +
160
+ '--dev-allow-all is for local-only development (no rate limit, ' +
161
+ '"DEV ALLOW-ALL" warning); --public-demo is for an operator-paid ' +
162
+ 'public demo (rate-limited, "PUBLIC DEMO" copy). Pick one.',
163
+ };
164
+ }
165
+ if (out.multiTenant && (out.devAllowAll || out.publicDemo)) {
166
+ return {
167
+ ...out,
168
+ error: '--multi-tenant is incompatible with --dev-allow-all and --public-demo. ' +
169
+ 'Multi-tenant requires real per-user identities (kind:"user" / kind:"app"); ' +
170
+ 'the any-bearer modes collapse every caller to kind:"builder" which the ' +
171
+ 'multi-tenant gate rejects. Drop the other flag and pair with real bearers.',
172
+ };
173
+ }
174
+ if (out.publicAutoTunnel && out.publicBaseUrl !== undefined) {
175
+ return {
176
+ ...out,
177
+ error: '--public and --public-base-url are mutually exclusive. ' +
178
+ '--public auto-spawns a cloudflared quick-tunnel and sets the URL; ' +
179
+ '--public-base-url pins an explicit URL. Pick one.',
180
+ };
181
+ }
182
+ if (out.publicAutoTunnel && out.devAllowAll && !out.iKnowItsPublic) {
183
+ return {
184
+ ...out,
185
+ error: '--public + --dev-allow-all exposes any-bearer auth to the open ' +
186
+ 'internet via cloudflared. Add --i-know-its-public to acknowledge ' +
187
+ 'this, or pair real bearers and drop --dev-allow-all.',
188
+ };
189
+ }
190
+ return out;
191
+ }
192
+ export function describeServeBanner(input) {
193
+ const httpUrl = `http://${input.host}:${input.port}`;
194
+ const lines = [
195
+ '',
196
+ ` ${input.serverName} v${input.serverVersion} — community edition · open self-hosted personal-mode`,
197
+ '',
198
+ // Operator-facing URL goes first — it's the "open a browser here"
199
+ // signal that closes the first-run story. Landing page lives at `/`
200
+ // by default when `ggui serve` is the entry point (see
201
+ // `cli.ts::buildMcpServerBackend`).
202
+ ` open → ${httpUrl}/`,
203
+ ` mcp → ${httpUrl}/mcp`,
204
+ ` health → ${httpUrl}/ggui/health`,
205
+ ` tools → ${input.toolCount} registered`,
206
+ ` agent → ${describeAgentStatus(input.agent)}`,
207
+ ...(input.embeddingModel
208
+ ? [` rag → ${input.embeddingModel} (cached at ~/.ggui/models/)`]
209
+ : []),
210
+ ];
211
+ if (input.publicBaseUrl) {
212
+ lines.push(` public → ${input.publicBaseUrl}/`);
213
+ }
214
+ if (input.pairCode && !input.devAllowAll && !input.publicDemo) {
215
+ // Pair code is meaningless under any-bearer-auth modes (every
216
+ // bearer already authenticates); skip it to keep the banner
217
+ // truthful. Visual + machine-readable pairing affordance. The
218
+ // machine-readable `PAIR_CODE <code>` line is emitted separately
219
+ // by `runServe` before the banner; this line is operator-facing.
220
+ lines.push(` pair code → ${input.pairCode} (valid ~10m)`);
221
+ }
222
+ if (input.adminToken) {
223
+ // Admin token gates the console `/keys` plane (list / mint /
224
+ // revoke). Operator pastes it into the `/admin-login` page on
225
+ // first visit. Printed in the same column as the pair-code so
226
+ // both bearers are obvious in the boot banner.
227
+ lines.push(` admin token → ${input.adminToken} (console /keys gate — paste at /admin-login)`);
228
+ }
229
+ if (input.noLlmKey) {
230
+ // First-run nudge — without an LLM key, ggui_push falls back to
231
+ // codeReady:false and the iframe never bootstraps. Surface the
232
+ // /settings URL alongside admin-token so the operator can paste a
233
+ // key without restarting (the file store is hot-read on every
234
+ // generation; setting through /settings takes effect immediately).
235
+ const settingsUrl = input.publicBaseUrl
236
+ ? `${input.publicBaseUrl}/settings`
237
+ : `${httpUrl}/settings`;
238
+ lines.push(` ⚠ no LLM key configured. paste one at ${settingsUrl}`, ` (admin-gated — use the admin token above) or export`, ` ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY /`, ` OPENROUTER_API_KEY before re-running.`);
239
+ }
240
+ lines.push('');
241
+ if (input.devAllowAll) {
242
+ // Loud warning — `--dev-allow-all` accepts any bearer (including
243
+ // the no-bearer probe MCP custom connectors send) as builder.
244
+ // Operators should never expose this surface to the open internet
245
+ // outside an ephemeral tunnel for connector smoke testing.
246
+ lines.push(` ⚠ DEV ALLOW-ALL: every bearer authenticates as builder.`, ` For local dev / tunnel smoke ONLY. Never expose to the open`, ` internet. Drop --dev-allow-all to switch back to strict auth.`);
247
+ }
248
+ else if (input.publicDemo) {
249
+ // Distinct from --dev-allow-all: public-demo is a deliberate
250
+ // operator posture, not an escape hatch. Surfaces (a) cost
251
+ // attribution (the operator's BYOK funds every visitor's
252
+ // generations) and (b) the rate-limit ceiling so the operator
253
+ // remembers what protection is in place if the demo blows up.
254
+ lines.push(` ⚠ PUBLIC DEMO: every bearer authenticates as builder.`, ` End-user generations bill the operator's LLM key in`, ` ~/.ggui/credentials.json. Per-IP rate limit on ggui_push`, ` bounds abuse. Drop --public-demo for strict auth.`);
255
+ }
256
+ else {
257
+ lines.push(` auth: strict — only pair-minted bearer tokens authenticate /mcp.`, ` Pair via the code above (landing page or \`POST /pair\`).`, ` Override the adapter via GguiServer({ auth }) in code for`, ` custom deployments (OIDC, Cognito, etc.).`);
258
+ if (input.multiTenant) {
259
+ lines.push(``, ` multi-tenant: each authenticated user manages their OWN LLM`, ` keys at /settings — the gate uses the request's bearer`, ` (Authorization: Bearer their-paired-token), NOT the`, ` admin token. Per-user scope is auto-derived from the`, ` identity (userId / appId).`, ` Note: ggui_push generation still resolves BYOK at the`, ` operator scope (env + credentials-file 'global'). Per-`, ` user generation-time BYOK is a follow-up — for now,`, ` multi-tenant lets each user MANAGE their key plane`, ` without granting them the operator's admin token.`);
260
+ }
261
+ if (input.oauth) {
262
+ lines.push(``, ` oauth: enabled — MCP custom-connector hosts (claude.ai etc.)`, ` can discover via /.well-known/oauth-* and complete the`, ` auth dance against /oauth/{authorize,token,register}.`, ` Paste the paired bearer on the consent page.`);
263
+ }
264
+ }
265
+ lines.push('', ` Ctrl-C to stop.`, '');
266
+ return lines;
267
+ }
268
+ /**
269
+ * Banner cell for the `agent` line. Pure — tests pin the copy.
270
+ */
271
+ export function describeAgentStatus(status) {
272
+ if (status.kind === 'running') {
273
+ const runner = status.language === 'ts' ? 'node --import=tsx' : 'node';
274
+ return `${status.entry} (${runner}) — running`;
275
+ }
276
+ switch (status.reason) {
277
+ case '--mcp-only':
278
+ return `disabled (--mcp-only)`;
279
+ case 'no ggui.json':
280
+ return `disabled (no ggui.json)`;
281
+ case 'ggui.json has no agent.entry':
282
+ return `disabled (ggui.json has no agent.entry)`;
283
+ }
284
+ }
285
+ /**
286
+ * Boot the backend, start the agent (if any), print the banner, then
287
+ * block until the shutdown signal fires. On signal, stops both
288
+ * components gracefully and resolves.
289
+ *
290
+ * Agent-crash policy (§10.2a lock): a crash AFTER boot surfaces as
291
+ * an `onEvent({type:'status', status:'crashed'})` delivery — the
292
+ * serve loop keeps MCP running and waits for shutdown. No auto-
293
+ * restart in v1. Operators compose restart policies via their own
294
+ * supervisor.
295
+ *
296
+ * MCP failure surfaces as a thrown error from `backendFactory()` /
297
+ * `backend.listen()` and propagates to `cli.ts` (exit 1).
298
+ *
299
+ * Pure enough to unit-test: the HTTP binding, signal wiring, and
300
+ * banner render are all either injected or pinned by
301
+ * {@link describeServeBanner} / {@link describeAgentStatus}.
302
+ */
303
+ export async function runServe(opts) {
304
+ const backend = opts.backendFactory();
305
+ const boundPort = await backend.listen(opts.flags.port, opts.flags.host);
306
+ // Start the agent AFTER the MCP server is listening. Rationale:
307
+ // most agents call MCP as soon as they boot; binding first means
308
+ // the agent's first connect always succeeds. If the agent's
309
+ // start() throws, we've still bound MCP and can surface the agent
310
+ // error cleanly without leaving an orphan HTTP server.
311
+ let agentHandle = null;
312
+ let unsubscribeAgent = null;
313
+ if (opts.agent) {
314
+ const supervision = opts.agent;
315
+ try {
316
+ agentHandle = await supervision.adapter.start(supervision.startInput);
317
+ if (supervision.onEvent) {
318
+ unsubscribeAgent = agentHandle.subscribe(supervision.onEvent);
319
+ }
320
+ }
321
+ catch (err) {
322
+ // Start-time failure is a permanent config error per the
323
+ // AgentRuntimeAdapter contract. Clean up the bound server so
324
+ // we don't leak a socket.
325
+ await backend.close();
326
+ throw err;
327
+ }
328
+ }
329
+ // Pre-mint an initial pairing code so a fresh operator's first-run
330
+ // flow has a concrete starting point. Without this, `/admin/pair/
331
+ // init` is gated by a builder bearer the operator doesn't have yet
332
+ // under strict auth — a chicken-and-egg that only devAllowAll hid.
333
+ // `activeInit()` on the landing page + the `PAIR_CODE` boot beacon
334
+ // below both read from the resulting pending code. Absent pairing
335
+ // (future embedding host opts out) → skip the mint + beacon + the
336
+ // banner line, leaving the operator on whatever bearer path their
337
+ // custom adapter provides.
338
+ //
339
+ // Skipped under `--dev-allow-all` — every bearer authenticates as
340
+ // builder there, so a pair code would be cargo-cult dressing on a
341
+ // permissive auth surface and harnesses parsing `PAIR_CODE` would
342
+ // get a value that's not actually load-bearing.
343
+ let initialPair;
344
+ if (backend.pairingService && !opts.flags.devAllowAll && !opts.flags.publicDemo) {
345
+ initialPair = await backend.pairingService.initPairing();
346
+ }
347
+ // Machine-readable ready beacon. Stable single-line format for
348
+ // test harnesses + supervisors; goes out BEFORE the pretty banner
349
+ // so consumers can gate on it without grepping multi-line output.
350
+ // Format: `READY <baseUrl>\n`.
351
+ opts.stdout.write(`READY http://${opts.flags.host}:${boundPort}\n`);
352
+ if (initialPair) {
353
+ // Paired beacon for harnesses — same stable-line contract as READY.
354
+ // Format: `PAIR_CODE <code>\n`. Expiry is implicit via the ~10m
355
+ // TTL the `InMemoryPairingService` ships with; harnesses that
356
+ // need it can read `GET /ggui/console/info#pairing.pending`.
357
+ opts.stdout.write(`PAIR_CODE ${initialPair.code}\n`);
358
+ }
359
+ if (backend.adminToken) {
360
+ // Machine-readable admin-token beacon — same stable-line contract
361
+ // as READY + PAIR_CODE. Harnesses pre-populate the console admin
362
+ // cookie by reading this off stdout.
363
+ opts.stdout.write(`ADMIN_TOKEN ${backend.adminToken}\n`);
364
+ }
365
+ const bannerLines = describeServeBanner({
366
+ port: boundPort,
367
+ host: opts.flags.host,
368
+ toolCount: backend.toolCount,
369
+ serverName: backend.serverName,
370
+ serverVersion: backend.serverVersion,
371
+ agent: opts.agentStatus,
372
+ devAllowAll: opts.flags.devAllowAll,
373
+ publicDemo: opts.flags.publicDemo,
374
+ multiTenant: opts.flags.multiTenant,
375
+ oauth: opts.flags.oauth,
376
+ ...(opts.flags.publicBaseUrl !== undefined
377
+ ? { publicBaseUrl: opts.flags.publicBaseUrl }
378
+ : {}),
379
+ ...(initialPair
380
+ ? {
381
+ pairCode: initialPair.code,
382
+ pairCodeExpiresAt: initialPair.codeExpiresAt,
383
+ }
384
+ : {}),
385
+ ...(backend.adminToken ? { adminToken: backend.adminToken } : {}),
386
+ ...(opts.noLlmKey ? { noLlmKey: true } : {}),
387
+ ...(backend.embeddingModel ? { embeddingModel: backend.embeddingModel } : {}),
388
+ });
389
+ opts.stdout.write(`${bannerLines.join('\n')}\n`);
390
+ // Auto-open the operator's browser at the local URL. The server's
391
+ // `landingRedirect` then routes first-run operators (no LLM creds)
392
+ // to the admin onboarding flow at `/admin-login?next=/admin/llm-keys`,
393
+ // so the URL bar lands on the right page without the CLI needing
394
+ // to know which target to pick. Skipped for: `--no-open`, non-TTY
395
+ // (CI / supervised / piped), `--mcp-only` (no UI surface to open).
396
+ // Errors are swallowed — the banner already shows the URL so the
397
+ // operator can copy/paste if launchBrowser fails (xdg-open missing,
398
+ // etc.).
399
+ const shouldAutoOpenServe = !opts.flags.noOpen
400
+ && !opts.flags.mcpOnly
401
+ && opts.stdout === process.stdout
402
+ && process.stdout.isTTY === true;
403
+ if (shouldAutoOpenServe) {
404
+ const localUrl = `http://${opts.flags.host}:${boundPort}`;
405
+ const result = launchBrowser(localUrl);
406
+ opts.stdout.write(result.ok
407
+ ? `\n opening browser → ${localUrl}\n`
408
+ : `\n (couldn't auto-open browser: ${result.error})\n`);
409
+ }
410
+ const stopAll = async () => {
411
+ // Stop the agent BEFORE the server so the agent doesn't try
412
+ // one last call into an already-torn-down MCP surface. Swallow
413
+ // stop errors — the log has already delivered the crash signal
414
+ // if there was one, and the server close is the priority.
415
+ if (unsubscribeAgent)
416
+ unsubscribeAgent();
417
+ unsubscribeAgent = null;
418
+ if (agentHandle) {
419
+ try {
420
+ await agentHandle.stop();
421
+ }
422
+ catch {
423
+ /* best-effort */
424
+ }
425
+ agentHandle = null;
426
+ }
427
+ await backend.close();
428
+ };
429
+ if (opts.shutdownSignal.aborted) {
430
+ await stopAll();
431
+ return 0;
432
+ }
433
+ await new Promise((resolve) => {
434
+ opts.shutdownSignal.addEventListener('abort', () => {
435
+ resolve();
436
+ }, { once: true });
437
+ });
438
+ await stopAll();
439
+ return 0;
440
+ }
441
+ export const SERVE_HELP = `ggui serve — run the open self-hosted personal-mode app
442
+
443
+ Boots the @ggui-ai/mcp-server binding with persistent defaults AND
444
+ (by default) supervises your agent alongside it. The current MCP
445
+ handler surface is the blueprint-read family (search / list_featured
446
+ / render); more tools come online as @ggui-ai/mcp-server-handlers
447
+ grows.
448
+
449
+ First-run bundle (all on by default):
450
+ - Landing page at / — operator opens a browser and sees
451
+ server identity + pair-code card.
452
+ - Session viewer at /r/<code> — same-origin viewer for sessions
453
+ minted by ggui_push; same-origin
454
+ HTTP-only cookie authenticates the
455
+ live-channel /ws upgrade.
456
+ - POST /pair + POST /admin/pair/init — pairing endpoint for MCP
457
+ hosts and third-party clients.
458
+ - Live-channel /ws — live session plane for MCP Apps
459
+ iframes and the console.
460
+
461
+ Embedding hosts that want a different shape (no landing page, no
462
+ pairing, programmatic control) should compose createGguiServer()
463
+ directly in code rather than invoking \`ggui serve\`.
464
+
465
+ Usage:
466
+ ggui serve [options]
467
+
468
+ Options:
469
+ --port <n> Bind port (default: ${DEFAULT_SERVE_PORT}, 0 = OS-assigned).
470
+ --host <addr> Bind host (default: ${DEFAULT_SERVE_HOST}).
471
+ --mcp-only Run just the MCP server; do not supervise an agent.
472
+ --dev-allow-all Accept any non-empty bearer (incl. no bearer) as
473
+ builder. Local-dev / tunnel smoke ONLY — never
474
+ expose to the open internet.
475
+ --public-demo Public-facing demo posture: same any-bearer auth
476
+ as --dev-allow-all, plus per-IP rate limit on
477
+ ggui_push (30 generations / 10 min) and a
478
+ "PUBLIC DEMO — operator pays" banner. Mutually
479
+ exclusive with --dev-allow-all.
480
+ --multi-tenant Multi-tenant posture: switches the /settings LLM-
481
+ keys gate from admin-token to auth-adapter so
482
+ each authenticated end-user manages their OWN
483
+ key plane (scope = userId/appId). kind:'builder'
484
+ identities are rejected at the gate. Strict-auth
485
+ only; mutually exclusive with --dev-allow-all
486
+ and --public-demo. (Generation-time BYOK still
487
+ resolves at the operator 'global' scope — a
488
+ follow-up will thread per-user BYOK through
489
+ ggui_push.)
490
+ --public-base-url <u> Override the public base URL used to compose
491
+ iframe-runtime + shortCode URLs. Set this to a
492
+ tunnel URL (https://<random>.trycloudflare.com)
493
+ when testing against a remote MCP host so URLs
494
+ resolve from the host's perspective. Without
495
+ this, URLs derive from --host:--port and only
496
+ work from the same machine.
497
+ --oauth Mount OAuth 2.1 + PKCE + Dynamic Client
498
+ Registration routes (.well-known/oauth-* +
499
+ /oauth/{authorize,token,register}). Required
500
+ for MCP custom-connector hosts (claude.ai,
501
+ ChatGPT) whose Add Connector form has no field
502
+ for a pre-shared bearer; OAuth lets them do
503
+ a discovery → DCR → consent dance ending in
504
+ the same paired bearer the strict-auth flow
505
+ already mints. Pure-bearer clients (Claude
506
+ Desktop with bearer in config) work without it.
507
+ --admin-token <t> Pin the admin bearer that gates the console
508
+ /keys plane (list / mint / revoke pairings +
509
+ /admin-login route). Without this, the server
510
+ mints a fresh ggui_admin_* token per boot and
511
+ prints it on the banner. Pin a stable value
512
+ when you want the bearer to survive restarts
513
+ (e.g. paste once into a password manager).
514
+ --ephemeral Disable cross-restart persistence. Without
515
+ this flag, the HMAC secrets that sign MCP
516
+ Apps bootstrap tokens and signed render URLs
517
+ are read or minted under .ggui/persistent/
518
+ (project-local if a ggui.json was resolved,
519
+ else ~/.ggui/persistent/) so cached tokens
520
+ survive a server restart. Pass this flag to
521
+ keep restarts ephemeral (tests / CI / nuclear-
522
+ revoke). Override the dir via GGUI_PERSISTENT_DIR.
523
+ --help, -h Show this help.
524
+
525
+ Agent runtime:
526
+ The default boots your agent alongside MCP. The agent's entry file
527
+ comes from ggui.json:
528
+
529
+ { "agent": { "entry": "./agent.ts" } }
530
+
531
+ Supported extensions: .js / .mjs / .cjs (node), .ts / .tsx / .mts
532
+ (node --import=tsx). No ggui.json, or no agent.entry, falls back
533
+ to MCP-only with a warning. Malformed ggui.json or a bad entry
534
+ is a hard error.
535
+
536
+ Agent crashes after startup are logged; the MCP server keeps
537
+ running. No auto-restart — compose that with your own supervisor.
538
+
539
+ Persistent storage (default-on; opt-out via --ephemeral):
540
+ By default \`ggui serve\` writes a small bundle under .ggui/persistent/
541
+ (project-local if a ggui.json was resolved, else ~/.ggui/persistent/)
542
+ so cached MCP Apps tokens, signed render URLs, shortCodes, sessions,
543
+ vectors, and paired bearers survive a restart — claude.ai chat-history
544
+ revisits keep working.
545
+
546
+ Bundle layout:
547
+
548
+ .ggui/persistent/
549
+ ├── bootstrap-secret.hex (HMAC, 0600)
550
+ ├── render-signer-secret.hex (HMAC, 0600)
551
+ ├── short-codes.sqlite (/r/<code> + /api/bootstrap/<code> resolution)
552
+ ├── sessions.sqlite (SessionStore — stack items + event history)
553
+ ├── vectors.sqlite (RAG corpus)
554
+ └── keys.json (paired bearers)
555
+
556
+ Override the dir with \`GGUI_PERSISTENT_DIR\`. Pass \`--ephemeral\` to
557
+ skip the bundle entirely (tests, CI loops, nuclear-revoke).
558
+
559
+ Explicit ggui.json#storage declarations always win — declare
560
+ \`{ "sessions": { "driver": "memory" } }\` to opt sessions back to
561
+ in-memory while keeping other surfaces persistent. Custom sqlite
562
+ paths via ggui.json#storage are honored verbatim:
563
+
564
+ {
565
+ "storage": {
566
+ "sessions": { "driver": "sqlite", "path": "./ggui-sessions.sqlite" },
567
+ "vectors": { "driver": "sqlite", "path": "./ggui-vectors.sqlite" },
568
+ "threads": { "driver": "sqlite", "path": "./ggui-threads.sqlite" }
569
+ }
570
+ }
571
+
572
+ Threads: the persistent-chat route family (/threads) is opt-in.
573
+ Absent \`storage.threads\` leaves the routes unmounted — pairs
574
+ with Portal's self-hosted origins only when declared. Driver
575
+ semantics:
576
+ - \`driver: "sqlite"\` — durable. Threads survive restart.
577
+ /ggui/health reports \`threads.durability: "durable"\`;
578
+ Portal hides its non-durable caveat.
579
+ - \`driver: "memory"\` — routes mount but data is lost on
580
+ restart. /ggui/health reports \`threads.durability:
581
+ "ephemeral"\`; Portal shows its caveat.
582
+
583
+ Paths resolve relative to the ggui.json directory. The sqlite
584
+ driver needs \`better-sqlite3\` installed in your project.
585
+
586
+ Recommended setups:
587
+
588
+ Local-only smoke (no tunnel, no remote MCP host):
589
+ ggui serve
590
+
591
+ claude.ai custom connector (over a public tunnel):
592
+ 1. Tunnel: cloudflared tunnel --url http://localhost:6781
593
+ 2. ggui serve --oauth \\
594
+ --public-base-url https://<tunnel>.trycloudflare.com
595
+ 3. Note PAIR_CODE from boot. Visit the landing page (the public URL
596
+ above), complete pairing, save the bearer.
597
+ 4. claude.ai → Settings → Connectors → Add custom connector.
598
+ URL: https://<tunnel>.trycloudflare.com/mcp
599
+ Client ID / Secret: leave empty (server uses Dynamic Client
600
+ Registration; no pre-shared secret needed).
601
+ 5. Click Connect → claude.ai redirects you to a paste-key page.
602
+ Paste the bearer from step 3. Done.
603
+
604
+ Quick local-dev WITHOUT auth (NEVER over a public tunnel):
605
+ ggui serve --dev-allow-all
606
+
607
+ Current limits:
608
+ - Strict-auth only: /mcp rejects any bearer that isn't pair-minted.
609
+ The CLI pre-mints one pairing code on boot (printed as
610
+ \`PAIR_CODE <code>\` and shown in the banner + on the landing
611
+ page). Complete \`POST /pair {code, deviceName}\` to receive a
612
+ bearer you can use with MCP. Embed @ggui-ai/mcp-server in your
613
+ own entrypoint to inject a different AuthAdapter (OIDC, Cognito,
614
+ etc.) for production.
615
+ - Single-tenant: every request scopes to one \`builder\` app id.
616
+ `;
@@ -0,0 +1,64 @@
1
+ import { GguiJsonLoadError, type GguiJsonV1 } from '@ggui-ai/project-config/node';
2
+ import type { AgentStatus, ServeAgentSupervision } from './serve-command.js';
3
+ /**
4
+ * Resolved state of the agent component at `ggui serve` boot time.
5
+ *
6
+ * - `status.kind === 'running'` → `supervision` populated; the
7
+ * runner supervises the agent.
8
+ * - `status.kind === 'disabled'` → `supervision` absent; the
9
+ * runner boots MCP-only with the banner surfacing the reason.
10
+ *
11
+ * `warnings` carries non-fatal messages the CLI prints BEFORE the
12
+ * banner (stderr). Hard errors (malformed ggui.json, bad entry
13
+ * extension) are thrown and caught by the CLI wrapper (exit 1).
14
+ */
15
+ export interface ResolvedAgentPlan {
16
+ readonly status: AgentStatus;
17
+ readonly supervision?: ServeAgentSupervision;
18
+ readonly warnings: readonly string[];
19
+ /**
20
+ * Fully-parsed `ggui.json` when one was found (regardless of whether
21
+ * `agent.entry` is present). `null` when no manifest was found on the
22
+ * ancestor walk. CLI callers use this to resolve the storage block
23
+ * into concrete adapters via `resolveStorageFromConfig`.
24
+ */
25
+ readonly manifest: GguiJsonV1 | null;
26
+ /**
27
+ * Absolute path to the manifest's parent directory — the canonical
28
+ * root for resolving relative paths declared in the manifest
29
+ * (`storage.sessions.path`, `agent.entry`, future additive fields).
30
+ * `null` when no manifest was found.
31
+ */
32
+ readonly projectRoot: string | null;
33
+ }
34
+ export interface ResolveAgentPlanOptions {
35
+ /** `true` = operator opted out of agent supervision. */
36
+ readonly mcpOnly: boolean;
37
+ /** Starting directory for the ancestor walk to `ggui.json`. */
38
+ readonly cwd: string;
39
+ /**
40
+ * Event sink for the agent supervision. Tests pass a collector;
41
+ * production passes a function that writes `[agent …]` lines
42
+ * to `process.stderr`. Absent = silent.
43
+ */
44
+ readonly onAgentEvent?: ServeAgentSupervision['onEvent'];
45
+ }
46
+ /**
47
+ * Apply the §10.2a matrix. Throws `GguiJsonLoadError` for cases 3
48
+ * (malformed JSON, schema failure) and a generic `Error` for case
49
+ * 4 (unsupported agent.entry extension / missing file hint).
50
+ *
51
+ * Returns a `ResolvedAgentPlan` for cases 1, 2, 5, and 6:
52
+ *
53
+ * 1. `--mcp-only` → disabled, silent
54
+ * 2. no ggui.json found → disabled, warn
55
+ * 5. ggui.json present, no agent.entry → disabled, warn
56
+ * 6. valid agent.entry → running, no warn
57
+ */
58
+ export declare function resolveAgentPlan(opts: ResolveAgentPlanOptions): ResolvedAgentPlan;
59
+ /**
60
+ * Re-export for tests that want to assert on malformed-config
61
+ * handling without importing project-config transitively.
62
+ */
63
+ export { GguiJsonLoadError };
64
+ //# sourceMappingURL=serve-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-fallback.d.ts","sourceRoot":"","sources":["../src/serve-fallback.ts"],"names":[],"mappings":"AAaA,OAAO,EAEL,iBAAiB,EAEjB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AAKtC,OAAO,KAAK,EACV,WAAW,EACX,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,qBAAqB,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,uBAAuB,GAC5B,iBAAiB,CA+EnB;AAqBD;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,CAAC"}