ugly-app 0.1.909 → 0.1.911

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 CHANGED
@@ -1,14 +1,20 @@
1
1
  # ugly-app
2
2
 
3
- A full-stack TypeScript framework for shipping production web apps. Scaffold with `npx ugly-app init my-app` and get an opinionated Express + React + PostgreSQL stack with type-safe RPC over WebSocket and HTTP, real-time document tracking, built-in auth, AI generation, storage, workers, and a CLI for every workflow.
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 auth, infra (PostgreSQL, Qdrant, NATS, S3-compatible object storage), AI provider keys, and deployment. Your app talks to all of it through the project's dev tunnel and its `UGLY_BOT_TOKEN`.
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
 
9
15
  - **Server** — Express + WebSocket with type-safe RPC and Zod validation.
10
16
  - **Client** — React 19 + Vite with typed routing, lazy pages, animated transitions, and a router-owned popup layer.
11
- - **Database** — `TypedDB` over PostgreSQL (JSONB) or Cloudflare D1, with full-text search and vector search.
17
+ - **Database** — `TypedDB` over PostgreSQL (JSONB) or Cloudflare D1, with full-text and vector search.
12
18
  - **Auth** — HttpOnly cookies + JWT. ugly.bot OAuth by default; magic-link / Google in `mode: 'self'`.
13
19
  - **AI** — Text, JSON, image, embeddings, web search — proxied through ugly.bot.
14
20
  - **Realtime** — NATS pub/sub and doc change subscriptions (`trackDoc` / `trackDocs`).
@@ -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 chat, file upload, auth demo, collab editing, and other test pages already wired up.
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
- ## Server — `createApp()`
40
+ ## Server — `createApp()` / `AppConfigurator`
33
41
 
34
- The single server entry point. Returns an `App` that owns Express, the WebSocket server, the typed DB, and the RPC dispatcher.
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 `app.pushSend()` to a per-route typed API. `pages` is optional; apps that only call `configurator.setPages()` still work — they just get the loose `PageRegistry` typing on `pushSend`.
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, NATS connection + KV buckets, data-proxy connection, event-counter flush, TTL cleanup for log tables, console/error capture, and ugly.bot log forwarding. Postgres, NATS, storage, and AI clients are loaded **lazily** — a host without `DATABASE_URL` / `NATS_URL` / `R2_BUCKET` doesn't pay their startup cost.
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
 
@@ -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 imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, etc.).
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,7 +149,9 @@ 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 normal RPC pipeline. App-provided handlers with the same name override the framework's defaults.
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
  |------|---------|
@@ -145,12 +164,14 @@ getMe: async (userId: string, input) => { … }
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. |
147
166
  | `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()` (or `MAINTAIN_BOT_USER_ID`). |
167
+ | `projectPlanList` / `projectPlanCreate` / `projectPlanUpdate` / `projectPlanDelete` | Project plan CRUD used by Studio. |
148
168
 
149
169
  ---
150
170
 
151
171
  ## Shared API definitions
152
172
 
153
- `shared/` is consumed by both server and client. Keep all Zod schemas, types, collections, and route declarations here.
173
+ `shared/` is consumed by both server and client. Keep all Zod schemas, types,
174
+ collections, and route declarations here.
154
175
 
155
176
  ### Requests (`shared/api.ts`)
156
177
 
@@ -179,7 +200,9 @@ export const requests = defineRequests({
179
200
  });
180
201
  ```
181
202
 
182
- Every request is reachable as **both** `socket.request(name, input)` (WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported from Zod for convenience.
203
+ Every request is reachable as **both** `socket.request(name, input)` (WebSocket)
204
+ and `POST /api/:name { input }` (HTTP). `z` is re-exported from Zod for
205
+ convenience.
183
206
 
184
207
  ### Collections (`shared/collections.ts`)
185
208
 
@@ -220,15 +243,21 @@ export const collections = defineCollections({
220
243
  - `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
221
244
  - `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 })`.
222
245
 
223
- All documents extend `DBObject`: `{ _id, version, created, updated }`. Use `dbDefaults()` to stamp `version` / `created` / `updated` on inserts. **Always generate `_id` with `nanoid()`** — never `crypto.randomUUID()`, `Date.now()`, or `Math.random()` (see `CLAUDE.md`).
246
+ All documents extend `DBObject`: `{ _id, version, created, updated }`. Use
247
+ `dbDefaults()` to stamp `version` / `created` / `updated` on inserts. **Always
248
+ generate `_id` with `nanoid()`** — never `crypto.randomUUID()`, `Date.now()`, or
249
+ `Math.random()` (see `CLAUDE.md`).
224
250
 
225
- After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`. The app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as a last resort).
251
+ After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`. The
252
+ app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as
253
+ a last resort).
226
254
 
227
255
  ---
228
256
 
229
257
  ## Routing
230
258
 
231
- Route definitions live in `shared/pages.ts`; the client-side router is created by `createRouter({ pages, allPages?, ssrPages? })`.
259
+ Route definitions live in `shared/pages.ts`; the client-side router is created by
260
+ `createRouter({ pages, allPages?, ssrPages? })`.
232
261
 
233
262
  ### `definePage` / `definePages` (`shared/pages.ts`)
234
263
 
@@ -244,16 +273,21 @@ export const pages = definePages({
244
273
  export type AppPages = typeof pages;
245
274
  ```
246
275
 
247
- `definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object carrying `PageMeta` plus a phantom `_params` used only for TypeScript inference. Options:
276
+ `definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object
277
+ carrying `PageMeta` plus a phantom `_params` used only for TypeScript inference.
278
+ Options:
248
279
 
249
280
  - `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.
250
281
  - `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.
251
282
  - `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).
252
283
  - `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.
253
284
 
254
- Path syntax: `:param` matches a single path segment; `*param` is greedy (captures slashes). Query-string params are declared in `Params` but never appear in the path template.
285
+ Path syntax: `:param` matches a single path segment; `*param` is greedy (captures
286
+ slashes). Query-string params are declared in `Params` but never appear in the
287
+ path template.
255
288
 
256
- `definePages<T>(p)` is a pass-through identity — use it to give the registry a name.
289
+ `definePages<T>(p)` is a pass-through identity — use it to give the registry a
290
+ name.
257
291
 
258
292
  ### `createRouter()` (`ugly-app/client`)
259
293
 
@@ -284,9 +318,13 @@ export const {
284
318
  - **`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`).
285
319
  - **`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).
286
320
 
287
- A standalone `Link` is also exported from `ugly-app/client` for code that can't easily reach the typed one; it accepts an optional `router` prop and falls back to `<RouterProvider>` context.
321
+ A standalone `Link` is also exported from `ugly-app/client` for code that can't
322
+ easily reach the typed one; it accepts an optional `router` prop and falls back
323
+ to `<RouterProvider>` context.
288
324
 
289
- **Never use a bare `<a href="/route">`** for internal navigation — it triggers a full document reload (white flash + repaint). See the "client navigation" rule in `CLAUDE.md`.
325
+ **Never use a bare `<a href="/route">`** for internal navigation — it triggers a
326
+ full document reload (white flash + repaint). See the "client navigation" rule in
327
+ `CLAUDE.md`.
290
328
 
291
329
  ### Page map — `lazyPage` / `lazyPageLoader`
292
330
 
@@ -306,7 +344,10 @@ export const allPages = {
306
344
  - **`lazyPage(factory)`** — lazy-imports a default-exported `React.ComponentType<Params>`. The page receives route params as props.
307
345
  - **`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.
308
346
 
309
- Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically imported module"): on the first failure they trigger a single `window.location.reload()` (guarded by `sessionStorage` so a genuinely broken chunk can't loop) to fetch the fresh `index.html` with new chunk hashes.
347
+ Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically
348
+ imported module"): on the first failure they trigger a single
349
+ `window.location.reload()` (guarded by `sessionStorage` so a genuinely broken
350
+ chunk can't loop) to fetch the fresh `index.html` with new chunk hashes.
310
351
 
311
352
  ```ts
312
353
  // pages/SlowPageLoader.tsx
@@ -336,11 +377,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
336
377
  back(); // browser history back
337
378
  ```
338
379
 
339
- Route names and params are fully typed against `pages`. `push` / `replace` no-op with a `console.error` when `buildUrl()` produces a URL that doesn't match a registered route.
380
+ Route names and params are fully typed against `pages`. `push` / `replace` no-op
381
+ with a `console.error` when `buildUrl()` produces a URL that doesn't match a
382
+ registered route.
340
383
 
341
384
  ### Popups — `openPopup()`
342
385
 
343
- `useRouter().openPopup()` is the canonical modal / sheet / menu API. The router owns the popup layer, drives a spring animation, and stacks popups z-index-correctly above every page.
386
+ `useRouter().openPopup()` is the canonical modal / sheet / menu API. The router
387
+ owns the popup layer, drives a spring animation, and stacks popups z-index-
388
+ correctly above every page.
344
389
 
345
390
  ```tsx
346
391
  const { openPopup } = useRouter();
@@ -364,13 +409,18 @@ handle.hide(); // dismiss programmatically
364
409
  - **`transient`** — 20% opacity backdrop, **dismisses** on backdrop click.
365
410
  - **`contextMenu`** — same as transient, intended for menus and pickers.
366
411
 
367
- `renderLayer` receives `{ content, spring, hide }`: `spring` is a `createAnimatedValue()` result driving 0 → 1; `hide` closes the popup. The default layer animates open with `easeOut` (250 ms) and closed with `easeIn` (200 ms). Popups render as siblings of the router's children (managed inside `RouterProvider`), so they stack above every page.
412
+ `renderLayer` receives `{ content, spring, hide }`: `spring` is a
413
+ `createAnimatedValue()` result driving 0 → 1; `hide` closes the popup. The default
414
+ layer animates open with `easeOut` (250 ms) and closed with `easeIn` (200 ms).
415
+ Popups render as siblings of the router's children (managed inside
416
+ `RouterProvider`), so they stack above every page.
368
417
 
369
418
  ---
370
419
 
371
420
  ## Client — `bootstrapApp()`
372
421
 
373
- The recommended entry point. Handles auth detection, socket creation, background account-sync with ugly.bot, and provider wiring.
422
+ The recommended entry point. Handles auth detection, socket creation, background
423
+ account-sync with ugly.bot, and provider wiring.
374
424
 
375
425
  ```tsx
376
426
  // client/main.tsx
@@ -402,22 +452,34 @@ bootstrapApp({
402
452
  | `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. |
403
453
  | `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
404
454
 
405
- `bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie verification). If absent, it renders unauthenticated immediately — the router's per-route auth guard surfaces `<AuthRoot>` only when the user opens a protected route. If the token is present, it connects the socket, mounts `<AppProvider>`, and renders.
455
+ `bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie
456
+ verification). If absent, it renders unauthenticated immediately — the router's
457
+ per-route auth guard surfaces `<AuthRoot>` only when the user opens a protected
458
+ route. If the token is present, it connects the socket, mounts `<AppProvider>`,
459
+ and renders.
406
460
 
407
- After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie is refreshed via `POST /auth/verify`; if the returned account differs from the current session, the page reloads onto the live account (guarded once per from→to pair to prevent loops). Any `?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from ugly.bot) is redeemed at the top of bootstrap before anything renders.
461
+ After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it
462
+ returns a fresh code, the cookie is refreshed via `POST /auth/verify`; if the
463
+ returned account differs from the current session, the page reloads onto the live
464
+ account (guarded once per from→to pair to prevent loops). Any
465
+ `?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from ugly.bot) is
466
+ redeemed at the top of bootstrap before anything renders.
408
467
 
409
- If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders `<MagicLinkCallback>` instead of the regular app shell.
468
+ If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders
469
+ `<MagicLinkCallback>` instead of the regular app shell.
410
470
 
411
471
  ### `AppProvider` & `useApp()`
412
472
 
413
- `bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use `useApp()` inside any page to access the active user, socket, and app-scoped services.
473
+ `bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use
474
+ `useApp()` inside any page to access the active user, socket, and app-scoped
475
+ services.
414
476
 
415
477
  ```ts
416
478
  const {
417
479
  userId, // current user id (string)
418
480
  user, // UserBase doc
419
481
  socket, // AppSocket — typed RPC client
420
- uglyBotSocket, // UglyBotSocket | null — for direct platform calls (STT/TTS, etc.)
482
+ uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS, etc.
421
483
  showPopup, // AppProvider-owned popup layer (see note below)
422
484
  hidePopup,
423
485
  hideAllPopups,
@@ -427,9 +489,17 @@ const {
427
489
  } = useApp();
428
490
  ```
429
491
 
430
- `useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a custom option through to a custom `loadingOverlay` element (accepted as an `AppProvider` prop — the framework `cloneElement`s the overlay with `{ label, asyncOptions }` while `runAsync` is pending). `useAppOptional()` returns `null` outside the provider; `useLocalizer()` returns a localizer that prefers `<StringsProvider>` data, falls back to the `AppProvider` `localizer` prop, and finally to identity.
492
+ `useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a
493
+ custom option through to a custom `loadingOverlay` element (accepted as an
494
+ `AppProvider` prop — the framework `cloneElement`s the overlay with
495
+ `{ label, asyncOptions }` while `runAsync` is pending). `useAppOptional()`
496
+ returns `null` outside the provider; `useLocalizer()` returns a localizer that
497
+ prefers `<StringsProvider>` data, falls back to the `AppProvider` `localizer`
498
+ prop, and finally to identity.
431
499
 
432
- Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible, mode-aware — the default choice) and `useApp().showPopup()` (a flat overlay layer for content that must sit *outside* the router transition). Prefer the router's.
500
+ Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
501
+ mode-aware — the default choice) and `useApp().showPopup()` (a flat overlay layer
502
+ for content that must sit *outside* the router transition). Prefer the router's.
433
503
 
434
504
  ### Direct socket access
435
505
 
@@ -446,20 +516,23 @@ Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
446
516
  | `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
447
517
  | `disconnect()` | Close the connection. |
448
518
 
449
- For pure HTTP (no WebSocket), use `createHttpClient({ requests, token?, baseUrl? })` from `ugly-app/client`.
519
+ For pure HTTP (no WebSocket), use `createHttpClient({ requests, token?, baseUrl? })`
520
+ from `ugly-app/client`.
450
521
 
451
522
  ---
452
523
 
453
524
  ## Auth
454
525
 
455
- ugly-app uses HttpOnly cookies and server-side JWT injection — no `localStorage`, no client-side token handling.
526
+ ugly-app uses HttpOnly cookies and server-side JWT injection — no `localStorage`,
527
+ no client-side token handling.
456
528
 
457
529
  Two modes, picked from the `auth` block in the project's `.uglyapp` config:
458
530
 
459
531
  - **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.
460
532
  - **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`.
461
533
 
462
- `window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl. `<AuthRoot>`) can branch correctly.
534
+ `window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl.
535
+ `<AuthRoot>`) can branch correctly.
463
536
 
464
537
  ### Mode A — ugly.bot OAuth (default)
465
538
 
@@ -471,7 +544,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
471
544
 
472
545
  ### Mode B — self-issued sessions
473
546
 
474
- When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link provider as primary; if `providers.google.clientId` is configured it adds Google as an extra provider on the same router.
547
+ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link
548
+ provider as primary; if `providers.google.clientId` is configured it adds Google
549
+ as an extra provider on the same router.
475
550
 
476
551
  - `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
477
552
  - Background ugly.bot silent SSO is a no-op.
@@ -481,7 +556,9 @@ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link pro
481
556
 
482
557
  ### Token-in-URL embed
483
558
 
484
- Any GET request with `?token=<JWT>` will, if the token verifies, set the cookie and 302-redirect to the same URL without the token parameter — useful for embedding any page in an iframe.
559
+ Any GET request with `?token=<JWT>` will, if the token verifies, set the cookie
560
+ and 302-redirect to the same URL without the token parameter — useful for
561
+ embedding any page in an iframe.
485
562
 
486
563
  ### Built-in routes
487
564
 
@@ -514,7 +591,9 @@ configurator.setAuth({
514
591
 
515
592
  ## Database — `TypedDB`
516
593
 
517
- Access via `app.db`. All methods accept a `CollectionDef` (from `defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain collection name string.
594
+ Access via `app.db`. All methods accept a `CollectionDef` (from
595
+ `defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain
596
+ collection name string.
518
597
 
519
598
  ### Writing
520
599
 
@@ -539,7 +618,10 @@ await db.batch(async () => {
539
618
  });
540
619
  ```
541
620
 
542
- Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All keys are dot-notation, fully typed against the collection's schema. Partial updates go through optimistic concurrency to prevent lost updates on concurrent writers.
621
+ Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All
622
+ keys are dot-notation, fully typed against the collection's schema. Partial
623
+ updates go through optimistic concurrency to prevent lost updates on concurrent
624
+ writers.
543
625
 
544
626
  ### Reading
545
627
 
@@ -573,7 +655,8 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
573
655
  await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
574
656
  ```
575
657
 
576
- Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection `onDelete` callbacks.
658
+ Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection
659
+ `onDelete` callbacks.
577
660
 
578
661
  ### Search
579
662
 
@@ -622,7 +705,9 @@ Imports available from `ugly-app`:
622
705
 
623
706
  ## AI
624
707
 
625
- AI calls are proxied through ugly.bot — your app never holds a provider key. Pass `UGLY_BOT_TOKEN` in the environment and the framework handles routing, balance tracking, retries, and per-user billing.
708
+ AI calls are proxied through ugly.bot — your app never holds a provider key. Pass
709
+ `UGLY_BOT_TOKEN` in the environment and the framework handles routing, balance
710
+ tracking, retries, and per-user billing.
626
711
 
627
712
  ### Server-side text generation
628
713
 
@@ -644,7 +729,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
644
729
  });
645
730
  ```
646
731
 
647
- Available models are exposed via `textGenModels` / `textGenModelData` from `ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq, Fireworks, Kimi, and Kie families.
732
+ Available models are exposed via `textGenModels` / `textGenModelData` from
733
+ `ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq,
734
+ Fireworks, Kimi, and Kie families.
648
735
 
649
736
  ### Server-side image generation
650
737
 
@@ -655,7 +742,8 @@ const imageGen = createImageGen(userId);
655
742
  const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
656
743
  ```
657
744
 
658
- `imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX, FAL, Google Imagen, Wavespeed, Kie Kolors).
745
+ `imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
746
+ FAL, Google Imagen, Wavespeed, Kie Kolors).
659
747
 
660
748
  ### Embeddings
661
749
 
@@ -680,7 +768,8 @@ await search.enrichNews({ query: 'topic' });
680
768
 
681
769
  ### Client-side AI calls
682
770
 
683
- Calls from React components go through the framework RPC pipeline — no token plumbing in the browser:
771
+ Calls from React components go through the framework RPC pipeline — no token
772
+ plumbing in the browser:
684
773
 
685
774
  ```ts
686
775
  import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
@@ -692,7 +781,8 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
692
781
 
693
782
  ### STT / TTS
694
783
 
695
- Speech goes **directly** from the browser to ugly.bot — never proxied through your app server (see the "STT/TTS routes to ugly.bot" rule in `CLAUDE.md`).
784
+ Speech goes **directly** from the browser to ugly.bot — never proxied through
785
+ your app server (see the "STT/TTS routes to ugly.bot" rule in `CLAUDE.md`).
696
786
 
697
787
  ```ts
698
788
  import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
@@ -701,7 +791,8 @@ const { start, stop, transcript, isListening } = useSTT(uglyBotSocket, options);
701
791
  const { speak, stop: stopTTS } = useTTS(uglyBotSocket);
702
792
  ```
703
793
 
704
- `uglyBotSocket` is the `UglyBotSocket` on `useApp()`; it opens a direct WebSocket to the platform.
794
+ `uglyBotSocket` is the `UglyBotSocket` on `useApp()`; it opens a direct WebSocket
795
+ to the platform.
705
796
 
706
797
  ---
707
798
 
@@ -725,11 +816,15 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
725
816
  await storage.delete('public', destKey);
726
817
  ```
727
818
 
728
- Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL via the built-in `uploadUrl` framework request and streams the upload. In dev, uploads go through a same-origin `/_s3` proxy to avoid CORS with local MinIO.
819
+ Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL via
820
+ the built-in `uploadUrl` framework request and streams the upload. In dev,
821
+ uploads go through a same-origin `/_s3` proxy to avoid CORS with local MinIO.
729
822
 
730
- `STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment isolation.
823
+ `STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
824
+ isolation.
731
825
 
732
- On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are not exposed (browser uploads must go through a Worker endpoint).
826
+ On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are
827
+ not exposed (browser uploads must go through a Worker endpoint).
733
828
 
734
829
  ---
735
830
 
@@ -761,9 +856,15 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
761
856
  configurator.setWorkers(cronTasks, cronHandlers);
762
857
  ```
763
858
 
764
- Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`, `description`. Workers without a schedule are still invocable via `POST /_workers/run` (auth: localhost in dev, `Authorization: Bearer $CRON_SECRET` in prod). Scheduled workers also appear in `/_cron/manifest` for the deploy orchestrator.
859
+ Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
860
+ `description`. Workers without a schedule are still invocable via
861
+ `POST /_workers/run` (auth: localhost in dev, `Authorization: Bearer $CRON_SECRET`
862
+ in prod). Scheduled workers also appear in `/_cron/manifest` for the deploy
863
+ orchestrator.
765
864
 
766
- On the Workers adapter, scheduled workers dispatch through Cloudflare Cron Triggers and durable queueing goes through Cloudflare Queues. On the Node adapter, `enqueueWorker` runs the handler inline (in-process queues only).
865
+ On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
866
+ Triggers and durable queueing goes through Cloudflare Queues. On the Node
867
+ adapter, `enqueueWorker` runs the handler inline (in-process queues only).
767
868
 
768
869
  ---
769
870
 
@@ -778,7 +879,9 @@ configurator.setStrings({
778
879
  });
779
880
  ```
780
881
 
781
- The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and `window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` / `useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
882
+ The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
883
+ `window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` /
884
+ `useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
782
885
 
783
886
  ---
784
887
 
@@ -803,7 +906,9 @@ export const experiments: Experiment[] = [
803
906
  configurator.setExperiments(experiments);
804
907
  ```
805
908
 
806
- Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for unauthenticated users). The framework's `initSession` / `captureEvent` requests automatically tag events with the user's branch assignments.
909
+ Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for
910
+ unauthenticated users). The framework's `initSession` / `captureEvent` requests
911
+ automatically tag events with the user's branch assignments.
807
912
 
808
913
  ---
809
914
 
@@ -858,7 +963,10 @@ The same developer source compiles for two runtimes via `src/server/adapter/`:
858
963
  - **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.
859
964
  - **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.
860
965
 
861
- Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `setCronTasks`, `setWorkers`) don't change between adapters. Local Workers dev boots via `npm run dev:workers`, which starts a small Node HTTP proxy speaking Neon's wire format so the Worker can talk to a real Postgres.
966
+ Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`,
967
+ `setCronTasks`, `setWorkers`) don't change between adapters. Local Workers dev
968
+ boots via `npm run dev:workers`, which starts a small Node HTTP proxy speaking
969
+ Neon's wire format so the Worker can talk to a real Postgres.
862
970
 
863
971
  ---
864
972
 
@@ -887,7 +995,8 @@ Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `
887
995
  | `NEON_PROXY_URL` | Set by `dev:workers` — Neon-wire endpoint the local Neon HTTP driver dials. |
888
996
  | `CLOUDFLARE_WORKERS` | Set to `1` inside the Workers adapter for runtime detection. |
889
997
 
890
- Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta.env.VITE_*`.
998
+ Browser-visible variables must be prefixed `VITE_` and consumed via
999
+ `import.meta.env.VITE_*`.
891
1000
 
892
1001
  ---
893
1002
 
@@ -912,7 +1021,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta
912
1021
  | `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
913
1022
  | `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
914
1023
 
915
- Inside a scaffolded project, the same commands are available via `npm run …` scripts — see `templates/CLAUDE.md`.
1024
+ Inside a scaffolded project, the same commands are available via `npm run …`
1025
+ scripts — see `templates/CLAUDE.md`.
916
1026
 
917
1027
  ---
918
1028
 
@@ -925,10 +1035,13 @@ Schema changes must be deliberate:
925
1035
  3. Replace every `REPLACE_ME` with the correct migration logic.
926
1036
  4. Run `npm run db:migrate`.
927
1037
 
928
- The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as a temporary escape hatch).
1038
+ The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
1039
+ only as a temporary escape hatch).
929
1040
 
930
1041
  ---
931
1042
 
932
1043
  ## Tech stack
933
1044
 
934
- Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant · NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers + Durable Objects (Adapter B) · ugly.bot platform
1045
+ Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant ·
1046
+ NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers + Durable
1047
+ Objects (Adapter B) · ugly.bot platform
@@ -0,0 +1,31 @@
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
+ interface AwOpts {
20
+ name?: string;
21
+ type?: string;
22
+ output?: string;
23
+ symmetric?: string;
24
+ autoRotate?: string;
25
+ stage?: string;
26
+ generated?: boolean;
27
+ shareForTraining?: boolean;
28
+ }
29
+ export declare function runAnythingWorld(op: string, args: string[], opts: AwOpts): Promise<void>;
30
+ export {};
31
+ //# sourceMappingURL=anythingworld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anythingworld.d.ts","sourceRoot":"","sources":["../../src/cli/anythingworld.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;GAiBG;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,CAwFf"}