ugly-app 0.1.910 → 0.1.912
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/README.md +179 -64
- package/dist/cli/anythingworld.d.ts +39 -0
- package/dist/cli/anythingworld.d.ts.map +1 -0
- package/dist/cli/anythingworld.js +162 -0
- package/dist/cli/anythingworld.js.map +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/Logger.d.ts.map +1 -1
- package/dist/client/Logger.js +16 -1
- package/dist/client/Logger.js.map +1 -1
- package/dist/client/components/KeyboardProvider.d.ts.map +1 -1
- package/dist/client/components/KeyboardProvider.js +22 -0
- package/dist/client/components/KeyboardProvider.js.map +1 -1
- package/dist/client/createSocket.d.ts.map +1 -1
- package/dist/client/createSocket.js +12 -4
- package/dist/client/createSocket.js.map +1 -1
- package/dist/client/inspect/safearea.d.ts.map +1 -1
- package/dist/client/inspect/safearea.js +33 -23
- package/dist/client/inspect/safearea.js.map +1 -1
- package/dist/inspect/host-bundle.js +8 -8
- package/dist/shared/ProxyOps.d.ts +125 -2
- package/dist/shared/ProxyOps.d.ts.map +1 -1
- package/dist/shared/ProxyOps.js +94 -0
- package/dist/shared/ProxyOps.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/anythingworld.ts +233 -0
- package/src/cli/index.ts +48 -0
- package/src/cli/version.ts +1 -1
- package/src/client/Logger.ts +19 -1
- package/src/client/components/KeyboardProvider.tsx +31 -0
- package/src/client/createSocket.ts +13 -4
- package/src/client/inspect/safearea.ts +38 -27
- package/src/shared/ProxyOps.ts +98 -0
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# ugly-app
|
|
2
2
|
|
|
3
|
-
A full-stack TypeScript framework for shipping production web apps. Scaffold with
|
|
3
|
+
A full-stack TypeScript framework for shipping production web apps. Scaffold with
|
|
4
|
+
`npx ugly-app init my-app` and get an opinionated Express + React + PostgreSQL stack
|
|
5
|
+
with type-safe RPC over WebSocket and HTTP, real-time document tracking, built-in
|
|
6
|
+
auth, AI generation, storage, workers, and a CLI for every workflow.
|
|
4
7
|
|
|
5
|
-
ugly-app is designed to run against [ugly.bot](https://ugly.bot), which provides
|
|
8
|
+
ugly-app is designed to run against [ugly.bot](https://ugly.bot), which provides
|
|
9
|
+
auth, infra (PostgreSQL, Qdrant, NATS, S3-compatible object storage), AI provider
|
|
10
|
+
keys, and deployment. Your app talks to all of it through the project's dev tunnel
|
|
11
|
+
and its `UGLY_BOT_TOKEN`.
|
|
6
12
|
|
|
7
13
|
## What's included
|
|
8
14
|
|
|
@@ -25,13 +31,16 @@ cd my-app
|
|
|
25
31
|
npm run dev
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
The scaffold gives you a working app at `http://localhost:4321` with todo CRUD, AI
|
|
34
|
+
The scaffold gives you a working app at `http://localhost:4321` with todo CRUD, AI
|
|
35
|
+
chat, file upload, auth demo, collab editing, and other test pages already wired
|
|
36
|
+
up.
|
|
29
37
|
|
|
30
38
|
---
|
|
31
39
|
|
|
32
40
|
## Server — `createApp()` / `AppConfigurator`
|
|
33
41
|
|
|
34
|
-
The single server entry point. Returns an `App` that owns Express, the WebSocket
|
|
42
|
+
The single server entry point. Returns an `App` that owns Express, the WebSocket
|
|
43
|
+
server, the typed DB, and the RPC dispatcher.
|
|
35
44
|
|
|
36
45
|
```ts
|
|
37
46
|
import {
|
|
@@ -76,7 +85,10 @@ function createApp<
|
|
|
76
85
|
): App<CollectionMap<typeof BUILTIN_DEFS & Defs>, RegistryPages<R>>;
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades
|
|
88
|
+
Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades
|
|
89
|
+
`app.pushSend()` to a per-route typed API. `pages` is optional; apps that only call
|
|
90
|
+
`configurator.setPages()` still work — they just get the loose `PageRegistry`
|
|
91
|
+
typing on `pushSend`.
|
|
80
92
|
|
|
81
93
|
### The returned `App`
|
|
82
94
|
|
|
@@ -90,7 +102,11 @@ Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades `app.
|
|
|
90
102
|
| `registerRoutes(fn)` | Mount additional Express routes after creation. |
|
|
91
103
|
| `pushSend({ targetUserId, title, body, page, query, imageUrl?, requireAuth? })` | Send a push whose click-through target is a route from this app's `pages` table. `page` and `query` are type-checked when the registry carries `pages`; the framework mints a `https://ugly.bot/l/<code>` short link so the click-through is always absolute and dock-app-routable. |
|
|
92
104
|
|
|
93
|
-
Framework services start automatically inside `app.start()`: schema drift check,
|
|
105
|
+
Framework services start automatically inside `app.start()`: schema drift check,
|
|
106
|
+
NATS connection + KV buckets, data-proxy connection, event-counter flush, TTL
|
|
107
|
+
cleanup for log tables, console/error capture, and ugly.bot log forwarding.
|
|
108
|
+
Postgres, NATS, storage, and AI clients are loaded **lazily** — a host without
|
|
109
|
+
`DATABASE_URL` / `NATS_URL` / `R2_BUCKET` doesn't pay their startup cost.
|
|
94
110
|
|
|
95
111
|
### `AppConfigurator`
|
|
96
112
|
|
|
@@ -98,11 +114,11 @@ Every method is optional; `setPages` is what mounts the SPA.
|
|
|
98
114
|
|
|
99
115
|
| Method | Description |
|
|
100
116
|
|--------|-------------|
|
|
101
|
-
| `setPages({ pages, renderPage?, clientDistPath? })` | Mount the SPA. Dev runs Vite in middleware mode; prod serves `dist/client
|
|
102
|
-
| `setUserHelper(helper)` |
|
|
117
|
+
| `setPages({ pages, renderPage?, clientDistPath? })` | Mount the SPA. Dev runs Vite in middleware mode; prod serves `clientDistPath` (default `dist/client`). Provide `renderPage(routeName, params) => Promise<string>` to SSR any `ssr: true` pages. |
|
|
118
|
+
| `setUserHelper(helper)` | Provide a `UserHelper<UserBase>` — required for WebSocket auth (framework reads/writes the `user` collection through it). |
|
|
103
119
|
| `setOnUserCreate(handler)` | `(userId, { email?, phone? }, db) => Promise<void>` — called on first login; create the user record. |
|
|
104
120
|
| `setAuth(provider)` | Replace the default auth provider (`verify(code)`, `authUrl(origin)`, optional `registerRoutes`). |
|
|
105
|
-
| `setOnSocketMessage(handler)` | Single raw-WebSocket handler. `(ws, userId, msg) => boolean` — return `true` to consume, `false` to fall through. |
|
|
121
|
+
| `setOnSocketMessage(handler)` | Single raw-WebSocket handler. `(ws, userId, msg) => boolean` — return `true` to consume, `false` to fall through. Replaces any previously registered handler. |
|
|
106
122
|
| `addSocketMessageHandler(handler)` | Append to the handler chain; first `true` wins. |
|
|
107
123
|
| `setWsPath(path)` | Override the WebSocket path (default `/rpc`). |
|
|
108
124
|
| `setOnWsAuth(handler)` | `(ws, userId, req) => void` — fires after a socket session authenticates. |
|
|
@@ -112,7 +128,7 @@ Every method is optional; `setPages` is what mounts the SPA.
|
|
|
112
128
|
| `setExperiments(experiments)` | Register `Experiment` definitions for `initSession` / `captureEvent` bucketing. |
|
|
113
129
|
| `setIsAdmin(fn)` | `(userId, db) => boolean \| Promise<boolean>` — gate for admin-only framework requests. Defaults to matching `MAINTAIN_BOT_USER_ID`. |
|
|
114
130
|
| `setOnEmail(handler)` | Handle inbound emails routed to `{domain}@ugly.bot` (delivered as internal HTTP). |
|
|
115
|
-
| `setCronTasks(tasks, handlers)` | Legacy cron-only registry
|
|
131
|
+
| `setCronTasks(tasks, handlers)` | **Deprecated.** Legacy cron-only registry — prefer `setWorkers()`. |
|
|
116
132
|
| `setWorkers(workers, handlers)` | Register named async tasks with optional Zod input schema and cron schedule. Powers `GET /_workers/manifest`, `POST /_workers/run`, and the cron orchestrator. Scheduled workers are mirrored into the cron registry automatically. |
|
|
117
133
|
| `setStrings(config)` | Localization config — framework injects language + critical strings into SSR HTML. |
|
|
118
134
|
| `registerRoutes(fn)` | Mount custom Express routes. |
|
|
@@ -120,7 +136,8 @@ Every method is optional; `setPages` is what mounts the SPA.
|
|
|
120
136
|
|
|
121
137
|
### Handler signatures
|
|
122
138
|
|
|
123
|
-
Handlers are plain async functions — no context object. Access state via captured
|
|
139
|
+
Handlers are plain async functions — no context object. Access state via captured
|
|
140
|
+
imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, etc.).
|
|
124
141
|
|
|
125
142
|
```ts
|
|
126
143
|
// req() — public, userId may be null
|
|
@@ -132,18 +149,21 @@ getMe: async (userId: string, input) => { … }
|
|
|
132
149
|
|
|
133
150
|
### Built-in framework requests
|
|
134
151
|
|
|
135
|
-
`createApp` registers several framework handlers reachable from any client via the
|
|
152
|
+
`createApp` registers several framework handlers reachable from any client via the
|
|
153
|
+
normal RPC pipeline. App-provided handlers with the same name override the
|
|
154
|
+
framework's defaults.
|
|
136
155
|
|
|
137
156
|
| Name | Purpose |
|
|
138
157
|
|------|---------|
|
|
139
158
|
| `userGet` | Returns `{ userId, name, avatarUri }` for the given user (or caller). |
|
|
140
|
-
| `initSession` / `captureEvent` | Session + event logging tagged with experiment branches. |
|
|
159
|
+
| `initSession` / `captureEvent` | Session + event logging tagged with experiment branches (public — no auth). |
|
|
141
160
|
| `textGen` / `imageGen` | AI proxies — server-validated, billed through ugly.bot. |
|
|
142
161
|
| `kagiSearch` / `kagiSummarize` / `kagiEnrichWeb` / `kagiEnrichNews` | Web search via ugly.bot. |
|
|
143
162
|
| `uploadUrl` | Issues a presigned PUT for the `temp` bucket. |
|
|
144
163
|
| `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata (see the "sharing links" rule in `CLAUDE.md`). |
|
|
145
164
|
| `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` / `perfSnapshotCaptureNoAuth` | Same-origin, public endpoints used by browser telemetry to write into the project's Postgres. |
|
|
146
165
|
| `submitFeedbackBot` / `feedbackReportResolve` | Bot-persona feedback submission; admin resolve/decline. |
|
|
166
|
+
| `adminGetPerfLogs` | Admin-only perf telemetry read. |
|
|
147
167
|
| `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()` (or `MAINTAIN_BOT_USER_ID`). |
|
|
148
168
|
| `projectPlanList` / `projectPlanCreate` / `projectPlanUpdate` / `projectPlanDelete` | Project plan CRUD used by Studio. |
|
|
149
169
|
|
|
@@ -151,7 +171,8 @@ getMe: async (userId: string, input) => { … }
|
|
|
151
171
|
|
|
152
172
|
## Shared API definitions
|
|
153
173
|
|
|
154
|
-
`shared/` is consumed by both server and client. Keep all Zod schemas, types,
|
|
174
|
+
`shared/` is consumed by both server and client. Keep all Zod schemas, types,
|
|
175
|
+
collections, and route declarations here.
|
|
155
176
|
|
|
156
177
|
### Requests (`shared/api.ts`)
|
|
157
178
|
|
|
@@ -180,7 +201,9 @@ export const requests = defineRequests({
|
|
|
180
201
|
});
|
|
181
202
|
```
|
|
182
203
|
|
|
183
|
-
Every request is reachable as **both** `socket.request(name, input)` (WebSocket)
|
|
204
|
+
Every request is reachable as **both** `socket.request(name, input)` (WebSocket)
|
|
205
|
+
and `POST /api/:name { input }` (HTTP). `z` is re-exported from Zod for
|
|
206
|
+
convenience.
|
|
184
207
|
|
|
185
208
|
### Collections (`shared/collections.ts`)
|
|
186
209
|
|
|
@@ -221,15 +244,21 @@ export const collections = defineCollections({
|
|
|
221
244
|
- `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
|
|
222
245
|
- `vector?: { dimensions, metric?, filterable? }` — ANN index. The vector is supplied out-of-band at write time (`setDoc(c, doc, { vec })`) — never stored in the doc JSON. Query with `getDocs(c, filter, { near })`.
|
|
223
246
|
|
|
224
|
-
All documents extend `DBObject`: `{ _id, version, created, updated }`. Use
|
|
247
|
+
All documents extend `DBObject`: `{ _id, version, created, updated }`. Use
|
|
248
|
+
`dbDefaults()` to stamp `version` / `created` / `updated` on inserts. **Always
|
|
249
|
+
generate `_id` with `nanoid()`** — never `crypto.randomUUID()`, `Date.now()`, or
|
|
250
|
+
`Math.random()` (see `CLAUDE.md`).
|
|
225
251
|
|
|
226
|
-
After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`. The
|
|
252
|
+
After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`. The
|
|
253
|
+
app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as
|
|
254
|
+
a last resort).
|
|
227
255
|
|
|
228
256
|
---
|
|
229
257
|
|
|
230
258
|
## Routing
|
|
231
259
|
|
|
232
|
-
Route definitions live in `shared/pages.ts`; the client-side router is created by
|
|
260
|
+
Route definitions live in `shared/pages.ts`; the client-side router is created by
|
|
261
|
+
`createRouter({ pages, allPages?, ssrPages? })`.
|
|
233
262
|
|
|
234
263
|
### `definePage` / `definePages` (`shared/pages.ts`)
|
|
235
264
|
|
|
@@ -245,16 +274,22 @@ export const pages = definePages({
|
|
|
245
274
|
export type AppPages = typeof pages;
|
|
246
275
|
```
|
|
247
276
|
|
|
248
|
-
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object
|
|
277
|
+
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object
|
|
278
|
+
carrying `PageMeta` plus a phantom `_params` used only for TypeScript inference.
|
|
279
|
+
|
|
280
|
+
Options (all optional):
|
|
249
281
|
|
|
250
282
|
- `auth` (default `true`) — protected route. When `isAuthenticated()` returns false on this route, the router synchronously renders the framework's `<AuthRoot>` in place of the page (dispatches on `window.__UGLY_APP_AUTH_MODE__`: Mode A → `<LoginPopup>`, Mode B → `<MagicLinkForm>` plus optional Google button). Apps cannot override this fallback — the framework owns the login UX so no route can accidentally ship as auth-required with no way to log in.
|
|
251
283
|
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is silently dropped to `ssr: false` with a warning: the SSR document is served from a shared edge cache and must not depend on the viewer.
|
|
252
284
|
- `cacheQuery?: string[]` — query params that participate in the SSR edge-cache key. Anything NOT listed bypasses the cache instead of poisoning it (e.g. `cacheQuery: ['q']` on a search page).
|
|
253
285
|
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is effectively 1 year (`DEFAULT_SSR_CACHE_TIMEOUT`) because the `buildId` is part of the cache key. Override only for pages that drift between deploys.
|
|
254
286
|
|
|
255
|
-
Path syntax: `:param` matches a single path segment; `*param` is greedy (captures
|
|
287
|
+
Path syntax: `:param` matches a single path segment; `*param` is greedy (captures
|
|
288
|
+
slashes). Query-string params are declared in `Params` but never appear in the
|
|
289
|
+
path template.
|
|
256
290
|
|
|
257
|
-
`definePages<T>(p)` is a pass-through identity — use it to give the registry a
|
|
291
|
+
`definePages<T>(p)` is a pass-through identity — use it to give the registry a
|
|
292
|
+
name.
|
|
258
293
|
|
|
259
294
|
### `createRouter()` (`ugly-app/client`)
|
|
260
295
|
|
|
@@ -285,9 +320,13 @@ export const {
|
|
|
285
320
|
- **`Link`** — typed SPA link bound to this router. Renders a real `<a>` (right-click / cmd-click / SEO keep working) but intercepts a plain left-click and routes through `push` / `replace`. Props: `to`, `params` (both type-checked against `pages`), `replace?`, `children`, plus common anchor attrs (`className`, `style`, `target`, `aria-label`, `data-id`, `onClick`).
|
|
286
321
|
- **`setAllPages(map)`** — register the lazy `PageMap<Pages>` after construction (typically from the browser entry, so the Worker's import graph stays free of every lazy page).
|
|
287
322
|
|
|
288
|
-
A standalone `Link` is also exported from `ugly-app/client` for code that can't
|
|
323
|
+
A standalone `Link` is also exported from `ugly-app/client` for code that can't
|
|
324
|
+
easily reach the typed one; it accepts an optional `router` prop and falls back
|
|
325
|
+
to `<RouterProvider>` context.
|
|
289
326
|
|
|
290
|
-
**Never use a bare `<a href="/route">`** for internal navigation — it triggers a
|
|
327
|
+
**Never use a bare `<a href="/route">`** for internal navigation — it triggers a
|
|
328
|
+
full document reload (white flash + repaint). See the "client navigation" rule in
|
|
329
|
+
`CLAUDE.md`.
|
|
291
330
|
|
|
292
331
|
### Page map — `lazyPage` / `lazyPageLoader`
|
|
293
332
|
|
|
@@ -307,7 +346,10 @@ export const allPages = {
|
|
|
307
346
|
- **`lazyPage(factory)`** — lazy-imports a default-exported `React.ComponentType<Params>`. The page receives route params as props.
|
|
308
347
|
- **`lazyPageLoader(factory)`** — lazy-imports an async loader `(params) => Promise<ReactElement>`. Use when a route needs data fetching before render. The loader file is the chunk boundary, so it can statically import its page component.
|
|
309
348
|
|
|
310
|
-
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically
|
|
349
|
+
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically
|
|
350
|
+
imported module"): on the first failure they trigger a single
|
|
351
|
+
`window.location.reload()` (guarded by `sessionStorage` so a genuinely broken
|
|
352
|
+
chunk can't loop) to fetch the fresh `index.html` with new chunk hashes.
|
|
311
353
|
|
|
312
354
|
```ts
|
|
313
355
|
// pages/SlowPageLoader.tsx
|
|
@@ -337,11 +379,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
|
|
|
337
379
|
back(); // browser history back
|
|
338
380
|
```
|
|
339
381
|
|
|
340
|
-
Route names and params are fully typed against `pages`. `push` / `replace` no-op
|
|
382
|
+
Route names and params are fully typed against `pages`. `push` / `replace` no-op
|
|
383
|
+
with a `console.error` when `buildUrl()` produces a URL that doesn't match a
|
|
384
|
+
registered route.
|
|
341
385
|
|
|
342
386
|
### Popups — `openPopup()`
|
|
343
387
|
|
|
344
|
-
`useRouter().openPopup()` is the canonical modal / sheet / menu API. The router
|
|
388
|
+
`useRouter().openPopup()` is the canonical modal / sheet / menu API. The router
|
|
389
|
+
owns the popup layer, drives a spring animation, and stacks popups z-index-
|
|
390
|
+
correctly above every page.
|
|
345
391
|
|
|
346
392
|
```tsx
|
|
347
393
|
const { openPopup } = useRouter();
|
|
@@ -365,13 +411,18 @@ handle.hide(); // dismiss programmatically
|
|
|
365
411
|
- **`transient`** — 20% opacity backdrop, **dismisses** on backdrop click.
|
|
366
412
|
- **`contextMenu`** — same as transient, intended for menus and pickers.
|
|
367
413
|
|
|
368
|
-
`renderLayer` receives `{ content, spring, hide }`: `spring` is a
|
|
414
|
+
`renderLayer` receives `{ content, spring, hide }`: `spring` is a
|
|
415
|
+
`createAnimatedValue()` result driving 0 → 1; `hide` closes the popup. The default
|
|
416
|
+
layer animates open with `easeOut` (250 ms) and closed with `easeIn` (200 ms).
|
|
417
|
+
Popups render as siblings of the router's children (managed inside
|
|
418
|
+
`RouterProvider`), so they stack above every page.
|
|
369
419
|
|
|
370
420
|
---
|
|
371
421
|
|
|
372
422
|
## Client — `bootstrapApp()`
|
|
373
423
|
|
|
374
|
-
The recommended entry point. Handles auth detection, socket creation, background
|
|
424
|
+
The recommended entry point. Handles auth detection, socket creation, background
|
|
425
|
+
account-sync with ugly.bot, and provider wiring.
|
|
375
426
|
|
|
376
427
|
```tsx
|
|
377
428
|
// client/main.tsx
|
|
@@ -403,23 +454,35 @@ bootstrapApp({
|
|
|
403
454
|
| `silentSso?` | Apex-domain apps that use ugly.bot as their auth authority but don't share its cookie. When `true`, a logged-out boot attempts a top-level SSO redirect (once per tab) to adopt an existing ugly.bot session. Leave unset for `*.ugly.bot` subdomains and Mode B apps. |
|
|
404
455
|
| `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
|
|
405
456
|
|
|
406
|
-
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie
|
|
457
|
+
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie
|
|
458
|
+
verification). If absent, it renders unauthenticated immediately — the router's
|
|
459
|
+
per-route auth guard surfaces `<AuthRoot>` only when the user opens a protected
|
|
460
|
+
route. If the token is present, it connects the socket, mounts `<AppProvider>`,
|
|
461
|
+
and renders.
|
|
407
462
|
|
|
408
|
-
After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it
|
|
463
|
+
After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it
|
|
464
|
+
returns a fresh code, the cookie is refreshed via `POST /auth/verify`; if the
|
|
465
|
+
returned account differs from the current session, the page reloads onto the live
|
|
466
|
+
account (guarded once per from→to pair to prevent loops). Any
|
|
467
|
+
`?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from ugly.bot) is
|
|
468
|
+
redeemed at the top of bootstrap before anything renders.
|
|
409
469
|
|
|
410
|
-
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders
|
|
470
|
+
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders
|
|
471
|
+
`<MagicLinkCallback>` instead of the regular app shell.
|
|
411
472
|
|
|
412
473
|
### `AppProvider` & `useApp()`
|
|
413
474
|
|
|
414
|
-
`bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use
|
|
475
|
+
`bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use
|
|
476
|
+
`useApp()` inside any page to access the active user, socket, and app-scoped
|
|
477
|
+
services.
|
|
415
478
|
|
|
416
479
|
```ts
|
|
417
480
|
const {
|
|
418
481
|
userId, // current user id (string)
|
|
419
482
|
user, // UserBase doc
|
|
420
483
|
socket, // AppSocket — typed RPC client
|
|
421
|
-
uglyBotSocket, // UglyBotSocket | null —
|
|
422
|
-
showPopup, // AppProvider-owned popup layer (see note below)
|
|
484
|
+
uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS, etc.
|
|
485
|
+
showPopup, // AppProvider-owned popup layer (see note below) — returns popup id
|
|
423
486
|
hidePopup,
|
|
424
487
|
hideAllPopups,
|
|
425
488
|
runAsync, // (label, async () => {…}, options?) — shows loading overlay while pending
|
|
@@ -428,9 +491,18 @@ const {
|
|
|
428
491
|
} = useApp();
|
|
429
492
|
```
|
|
430
493
|
|
|
431
|
-
`useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a
|
|
494
|
+
`useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a
|
|
495
|
+
custom option through to a custom `loadingOverlay` element (accepted as an
|
|
496
|
+
`AppProvider` prop — the framework `cloneElement`s the overlay with
|
|
497
|
+
`{ label, asyncOptions }` while `runAsync` is pending). `useAppOptional()`
|
|
498
|
+
returns `null` outside the provider; `useLocalizer()` returns a localizer that
|
|
499
|
+
prefers `<StringsProvider>` data, falls back to the `AppProvider` `localizer`
|
|
500
|
+
prop, and finally to identity.
|
|
432
501
|
|
|
433
|
-
Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
502
|
+
Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
503
|
+
mode-aware — the default choice) and `useApp().showPopup()` (a flat overlay layer
|
|
504
|
+
returning a string id, for content that must sit *outside* the router
|
|
505
|
+
transition). Prefer the router's.
|
|
434
506
|
|
|
435
507
|
### Direct socket access
|
|
436
508
|
|
|
@@ -447,20 +519,23 @@ Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
|
447
519
|
| `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
|
|
448
520
|
| `disconnect()` | Close the connection. |
|
|
449
521
|
|
|
450
|
-
For pure HTTP (no WebSocket), use `createHttpClient({ requests, token?, baseUrl? })`
|
|
522
|
+
For pure HTTP (no WebSocket), use `createHttpClient({ requests, token?, baseUrl? })`
|
|
523
|
+
from `ugly-app/client`.
|
|
451
524
|
|
|
452
525
|
---
|
|
453
526
|
|
|
454
527
|
## Auth
|
|
455
528
|
|
|
456
|
-
ugly-app uses HttpOnly cookies and server-side JWT injection — no `localStorage`,
|
|
529
|
+
ugly-app uses HttpOnly cookies and server-side JWT injection — no `localStorage`,
|
|
530
|
+
no client-side token handling.
|
|
457
531
|
|
|
458
532
|
Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
459
533
|
|
|
460
534
|
- **Mode A — `mode: 'uglybot'`** (default when no `auth` block is present). Auth is delegated to ugly.bot OAuth. AI / email / push proxies bill the end user's ugly.bot credits.
|
|
461
535
|
- **Mode B — `mode: 'self'`**. The app issues its own sessions via magic-link email and/or Google OAuth. AI / email / push proxies bill the developer's ugly.bot account using the project's `AI_PROXY_TOKEN`.
|
|
462
536
|
|
|
463
|
-
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl.
|
|
537
|
+
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl.
|
|
538
|
+
`<AuthRoot>`) can branch correctly.
|
|
464
539
|
|
|
465
540
|
### Mode A — ugly.bot OAuth (default)
|
|
466
541
|
|
|
@@ -472,7 +547,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
|
472
547
|
|
|
473
548
|
### Mode B — self-issued sessions
|
|
474
549
|
|
|
475
|
-
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link
|
|
550
|
+
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link
|
|
551
|
+
provider as primary; if `providers.google.clientId` is configured it adds Google
|
|
552
|
+
as an extra provider on the same router.
|
|
476
553
|
|
|
477
554
|
- `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
|
|
478
555
|
- Background ugly.bot silent SSO is a no-op.
|
|
@@ -482,7 +559,9 @@ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link pro
|
|
|
482
559
|
|
|
483
560
|
### Token-in-URL embed
|
|
484
561
|
|
|
485
|
-
Any GET request with `?token=<JWT>` will, if the token verifies, set the cookie
|
|
562
|
+
Any GET request with `?token=<JWT>` will, if the token verifies, set the cookie
|
|
563
|
+
and 302-redirect to the same URL without the token parameter — useful for
|
|
564
|
+
embedding any page in an iframe.
|
|
486
565
|
|
|
487
566
|
### Built-in routes
|
|
488
567
|
|
|
@@ -515,7 +594,9 @@ configurator.setAuth({
|
|
|
515
594
|
|
|
516
595
|
## Database — `TypedDB`
|
|
517
596
|
|
|
518
|
-
Access via `app.db`. All methods accept a `CollectionDef` (from
|
|
597
|
+
Access via `app.db`. All methods accept a `CollectionDef` (from
|
|
598
|
+
`defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain
|
|
599
|
+
collection name string.
|
|
519
600
|
|
|
520
601
|
### Writing
|
|
521
602
|
|
|
@@ -540,7 +621,10 @@ await db.batch(async () => {
|
|
|
540
621
|
});
|
|
541
622
|
```
|
|
542
623
|
|
|
543
|
-
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All
|
|
624
|
+
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All
|
|
625
|
+
keys are dot-notation, fully typed against the collection's schema. Partial
|
|
626
|
+
updates go through optimistic concurrency to prevent lost updates on concurrent
|
|
627
|
+
writers.
|
|
544
628
|
|
|
545
629
|
### Reading
|
|
546
630
|
|
|
@@ -574,7 +658,8 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
|
|
|
574
658
|
await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
|
|
575
659
|
```
|
|
576
660
|
|
|
577
|
-
Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection
|
|
661
|
+
Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection
|
|
662
|
+
`onDelete` callbacks.
|
|
578
663
|
|
|
579
664
|
### Search
|
|
580
665
|
|
|
@@ -623,12 +708,14 @@ Imports available from `ugly-app`:
|
|
|
623
708
|
|
|
624
709
|
## AI
|
|
625
710
|
|
|
626
|
-
AI calls are proxied through ugly.bot — your app never holds a provider key. Pass
|
|
711
|
+
AI calls are proxied through ugly.bot — your app never holds a provider key. Pass
|
|
712
|
+
`UGLY_BOT_TOKEN` in the environment and the framework handles routing, balance
|
|
713
|
+
tracking, retries, and per-user billing.
|
|
627
714
|
|
|
628
715
|
### Server-side text generation
|
|
629
716
|
|
|
630
717
|
```ts
|
|
631
|
-
import { createTextGen } from 'ugly-app';
|
|
718
|
+
import { createTextGen } from 'ugly-app';
|
|
632
719
|
const textGen = createTextGen(userId);
|
|
633
720
|
|
|
634
721
|
const text = await textGen.generate(messages, { model: 'gemini_2_5_flash' });
|
|
@@ -645,18 +732,21 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
|
|
|
645
732
|
});
|
|
646
733
|
```
|
|
647
734
|
|
|
648
|
-
Available models are exposed via `textGenModels` / `textGenModelData` from
|
|
735
|
+
Available models are exposed via `textGenModels` / `textGenModelData` from
|
|
736
|
+
`ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq,
|
|
737
|
+
Fireworks, Kimi, and Kie families.
|
|
649
738
|
|
|
650
739
|
### Server-side image generation
|
|
651
740
|
|
|
652
741
|
```ts
|
|
653
|
-
import { createImageGen } from 'ugly-app';
|
|
742
|
+
import { createImageGen } from 'ugly-app';
|
|
654
743
|
const imageGen = createImageGen(userId);
|
|
655
744
|
|
|
656
745
|
const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
|
|
657
746
|
```
|
|
658
747
|
|
|
659
|
-
`imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
|
|
748
|
+
`imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
|
|
749
|
+
FAL, Google Imagen, Wavespeed, Kie Kolors).
|
|
660
750
|
|
|
661
751
|
### Embeddings
|
|
662
752
|
|
|
@@ -681,7 +771,8 @@ await search.enrichNews({ query: 'topic' });
|
|
|
681
771
|
|
|
682
772
|
### Client-side AI calls
|
|
683
773
|
|
|
684
|
-
Calls from React components go through the framework RPC pipeline — no token
|
|
774
|
+
Calls from React components go through the framework RPC pipeline — no token
|
|
775
|
+
plumbing in the browser:
|
|
685
776
|
|
|
686
777
|
```ts
|
|
687
778
|
import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
|
|
@@ -693,7 +784,8 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
|
|
|
693
784
|
|
|
694
785
|
### STT / TTS
|
|
695
786
|
|
|
696
|
-
Speech goes **directly** from the browser to ugly.bot — never proxied through
|
|
787
|
+
Speech goes **directly** from the browser to ugly.bot — never proxied through
|
|
788
|
+
your app server (see the "STT/TTS routes to ugly.bot" rule in `CLAUDE.md`).
|
|
697
789
|
|
|
698
790
|
```ts
|
|
699
791
|
import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
|
|
@@ -702,7 +794,8 @@ const { start, stop, transcript, isListening } = useSTT(uglyBotSocket, options);
|
|
|
702
794
|
const { speak, stop: stopTTS } = useTTS(uglyBotSocket);
|
|
703
795
|
```
|
|
704
796
|
|
|
705
|
-
`uglyBotSocket` is the `UglyBotSocket` on `useApp()`; it opens a direct WebSocket
|
|
797
|
+
`uglyBotSocket` is the `UglyBotSocket` on `useApp()`; it opens a direct WebSocket
|
|
798
|
+
to the platform.
|
|
706
799
|
|
|
707
800
|
---
|
|
708
801
|
|
|
@@ -726,11 +819,15 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
|
726
819
|
await storage.delete('public', destKey);
|
|
727
820
|
```
|
|
728
821
|
|
|
729
|
-
Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL via
|
|
822
|
+
Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL via
|
|
823
|
+
the built-in `uploadUrl` framework request and streams the upload. In dev,
|
|
824
|
+
uploads go through a same-origin `/_s3` proxy to avoid CORS with local MinIO.
|
|
730
825
|
|
|
731
|
-
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
|
|
826
|
+
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
|
|
827
|
+
isolation.
|
|
732
828
|
|
|
733
|
-
On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are
|
|
829
|
+
On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are
|
|
830
|
+
not exposed (browser uploads must go through a Worker endpoint).
|
|
734
831
|
|
|
735
832
|
---
|
|
736
833
|
|
|
@@ -762,9 +859,15 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
|
|
|
762
859
|
configurator.setWorkers(cronTasks, cronHandlers);
|
|
763
860
|
```
|
|
764
861
|
|
|
765
|
-
Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
|
|
862
|
+
Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
|
|
863
|
+
`description`. Workers without a schedule are still invocable via
|
|
864
|
+
`POST /_workers/run` (auth: localhost in dev, `Authorization: Bearer $CRON_SECRET`
|
|
865
|
+
in prod). Scheduled workers also appear in `/_cron/manifest` for the deploy
|
|
866
|
+
orchestrator.
|
|
766
867
|
|
|
767
|
-
On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
|
|
868
|
+
On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
|
|
869
|
+
Triggers and durable queueing goes through Cloudflare Queues. On the Node
|
|
870
|
+
adapter, `enqueueWorker` runs the handler inline (in-process queues only).
|
|
768
871
|
|
|
769
872
|
---
|
|
770
873
|
|
|
@@ -779,7 +882,9 @@ configurator.setStrings({
|
|
|
779
882
|
});
|
|
780
883
|
```
|
|
781
884
|
|
|
782
|
-
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
|
|
885
|
+
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
|
|
886
|
+
`window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` /
|
|
887
|
+
`useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
|
|
783
888
|
|
|
784
889
|
---
|
|
785
890
|
|
|
@@ -804,7 +909,9 @@ export const experiments: Experiment[] = [
|
|
|
804
909
|
configurator.setExperiments(experiments);
|
|
805
910
|
```
|
|
806
911
|
|
|
807
|
-
Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for
|
|
912
|
+
Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for
|
|
913
|
+
unauthenticated users). The framework's `initSession` / `captureEvent` requests
|
|
914
|
+
automatically tag events with the user's branch assignments.
|
|
808
915
|
|
|
809
916
|
---
|
|
810
917
|
|
|
@@ -859,7 +966,10 @@ The same developer source compiles for two runtimes via `src/server/adapter/`:
|
|
|
859
966
|
- **Adapter A — Node + TCP** (`src/server/adapter/node/`): default for `npm run dev` and any non-Workers deploy. Wraps `pg.Pool`, `nats.js`, AWS S3 SDK.
|
|
860
967
|
- **Adapter B — Cloudflare Workers** (`src/server/adapter/workers/`): used when the Studio publish flow deploys to Cloudflare. Hono router + Durable Objects (one `CollectionDO` per project-collection, plus a `SessionDO` per user WS) + `@neondatabase/serverless` HTTP driver + R2 binding + Cloudflare Cron Triggers + Cloudflare Queues.
|
|
861
968
|
|
|
862
|
-
Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`,
|
|
969
|
+
Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`,
|
|
970
|
+
`setCronTasks`, `setWorkers`) don't change between adapters. Local Workers dev
|
|
971
|
+
boots via `npm run dev:workers`, which starts a small Node HTTP proxy speaking
|
|
972
|
+
Neon's wire format so the Worker can talk to a real Postgres.
|
|
863
973
|
|
|
864
974
|
---
|
|
865
975
|
|
|
@@ -888,7 +998,8 @@ Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `
|
|
|
888
998
|
| `NEON_PROXY_URL` | Set by `dev:workers` — Neon-wire endpoint the local Neon HTTP driver dials. |
|
|
889
999
|
| `CLOUDFLARE_WORKERS` | Set to `1` inside the Workers adapter for runtime detection. |
|
|
890
1000
|
|
|
891
|
-
Browser-visible variables must be prefixed `VITE_` and consumed via
|
|
1001
|
+
Browser-visible variables must be prefixed `VITE_` and consumed via
|
|
1002
|
+
`import.meta.env.VITE_*`.
|
|
892
1003
|
|
|
893
1004
|
---
|
|
894
1005
|
|
|
@@ -913,7 +1024,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta
|
|
|
913
1024
|
| `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
|
|
914
1025
|
| `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
|
|
915
1026
|
|
|
916
|
-
Inside a scaffolded project, the same commands are available via `npm run …`
|
|
1027
|
+
Inside a scaffolded project, the same commands are available via `npm run …`
|
|
1028
|
+
scripts — see `templates/CLAUDE.md`.
|
|
917
1029
|
|
|
918
1030
|
---
|
|
919
1031
|
|
|
@@ -926,10 +1038,13 @@ Schema changes must be deliberate:
|
|
|
926
1038
|
3. Replace every `REPLACE_ME` with the correct migration logic.
|
|
927
1039
|
4. Run `npm run db:migrate`.
|
|
928
1040
|
|
|
929
|
-
The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
|
|
1041
|
+
The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
|
|
1042
|
+
only as a temporary escape hatch).
|
|
930
1043
|
|
|
931
1044
|
---
|
|
932
1045
|
|
|
933
1046
|
## Tech stack
|
|
934
1047
|
|
|
935
|
-
Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant ·
|
|
1048
|
+
Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant ·
|
|
1049
|
+
NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers + Durable
|
|
1050
|
+
Objects (Adapter B) · ugly.bot platform
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anything World 3D pipeline via ugly.bot's `aw*` ops. Rigs and animates ANIMALS — the one thing the
|
|
3
|
+
* other two providers cannot do:
|
|
4
|
+
* find <query> search the 3D library (FREE)
|
|
5
|
+
* models every model this account has processed (FREE)
|
|
6
|
+
* status <modelId> poll a job, and print its clips (FREE)
|
|
7
|
+
* animate <file...> upload → rig + animate (~5 credits, ~$1.25)
|
|
8
|
+
* rig <file...> upload → rig only (~5 credits)
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-07-25 on real four-legged bodies: Meshy returns a 24-joint BIPED (…LeftForeArm,
|
|
11
|
+
* LeftHand) with a clip called `walking_man`, and Tripo FAILS the task on rig_type 'quadruped'.
|
|
12
|
+
* Anything World's library reports `quadruped`, `quadruped_ungulate`, `winged_flyer` and `hopper`,
|
|
13
|
+
* carrying idle + walk + RUN for walkers and fly + glide for flyers.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ TWO BUDGETS. `find` / `models` / `status` are library calls against a free monthly allowance;
|
|
16
|
+
* `animate` / `rig` spend processing credits. Verify and poll on the free side — never by
|
|
17
|
+
* re-submitting. Uploads are multipart, so a local GLB goes straight in with no public hosting.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ "FREE" IS NOT "UNLIMITED", and the allowance is smaller than it looks. Measured 2026-07-25: a
|
|
20
|
+
* dozen or so `find` calls exhausted the month, after which EVERY web API call — polling included —
|
|
21
|
+
* returns `{"code":"Too many requests error"}`. Two consequences worth planning around:
|
|
22
|
+
* · a job in flight cannot be polled once the allowance is gone, so budget calls for the whole
|
|
23
|
+
* lifetime of the work, not just for the submit;
|
|
24
|
+
* · asset URLs inside a response are SIGNED and expire in ~45 minutes. If a response carries a
|
|
25
|
+
* model you want, download it in that session — re-fetching costs another call you may not have.
|
|
26
|
+
*/
|
|
27
|
+
interface AwOpts {
|
|
28
|
+
name?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
output?: string;
|
|
31
|
+
symmetric?: string;
|
|
32
|
+
autoRotate?: string;
|
|
33
|
+
stage?: string;
|
|
34
|
+
generated?: boolean;
|
|
35
|
+
shareForTraining?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function runAnythingWorld(op: string, args: string[], opts: AwOpts): Promise<void>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=anythingworld.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anythingworld.d.ts","sourceRoot":"","sources":["../../src/cli/anythingworld.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,UAAU,MAAM;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAgGD,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CA4Ff"}
|