ugly-app 0.1.910 → 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,8 +1,14 @@
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
 
@@ -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
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
  |------|---------|
@@ -151,7 +170,8 @@ getMe: async (userId: string, input) => { … }
151
170
 
152
171
  ## Shared API definitions
153
172
 
154
- `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.
155
175
 
156
176
  ### Requests (`shared/api.ts`)
157
177
 
@@ -180,7 +200,9 @@ export const requests = defineRequests({
180
200
  });
181
201
  ```
182
202
 
183
- 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.
184
206
 
185
207
  ### Collections (`shared/collections.ts`)
186
208
 
@@ -221,15 +243,21 @@ export const collections = defineCollections({
221
243
  - `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
222
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 })`.
223
245
 
224
- 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`).
225
250
 
226
- 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).
227
254
 
228
255
  ---
229
256
 
230
257
  ## Routing
231
258
 
232
- 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? })`.
233
261
 
234
262
  ### `definePage` / `definePages` (`shared/pages.ts`)
235
263
 
@@ -245,16 +273,21 @@ export const pages = definePages({
245
273
  export type AppPages = typeof pages;
246
274
  ```
247
275
 
248
- `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:
249
279
 
250
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.
251
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.
252
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).
253
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.
254
284
 
255
- 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.
256
288
 
257
- `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.
258
291
 
259
292
  ### `createRouter()` (`ugly-app/client`)
260
293
 
@@ -285,9 +318,13 @@ export const {
285
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`).
286
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).
287
320
 
288
- 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.
289
324
 
290
- **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`.
291
328
 
292
329
  ### Page map — `lazyPage` / `lazyPageLoader`
293
330
 
@@ -307,7 +344,10 @@ export const allPages = {
307
344
  - **`lazyPage(factory)`** — lazy-imports a default-exported `React.ComponentType<Params>`. The page receives route params as props.
308
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.
309
346
 
310
- 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.
311
351
 
312
352
  ```ts
313
353
  // pages/SlowPageLoader.tsx
@@ -337,11 +377,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
337
377
  back(); // browser history back
338
378
  ```
339
379
 
340
- 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.
341
383
 
342
384
  ### Popups — `openPopup()`
343
385
 
344
- `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.
345
389
 
346
390
  ```tsx
347
391
  const { openPopup } = useRouter();
@@ -365,13 +409,18 @@ handle.hide(); // dismiss programmatically
365
409
  - **`transient`** — 20% opacity backdrop, **dismisses** on backdrop click.
366
410
  - **`contextMenu`** — same as transient, intended for menus and pickers.
367
411
 
368
- `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.
369
417
 
370
418
  ---
371
419
 
372
420
  ## Client — `bootstrapApp()`
373
421
 
374
- 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.
375
424
 
376
425
  ```tsx
377
426
  // client/main.tsx
@@ -403,22 +452,34 @@ bootstrapApp({
403
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. |
404
453
  | `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
405
454
 
406
- `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.
407
460
 
408
- 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.
409
467
 
410
- 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.
411
470
 
412
471
  ### `AppProvider` & `useApp()`
413
472
 
414
- `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.
415
476
 
416
477
  ```ts
417
478
  const {
418
479
  userId, // current user id (string)
419
480
  user, // UserBase doc
420
481
  socket, // AppSocket — typed RPC client
421
- uglyBotSocket, // UglyBotSocket | null — for direct platform calls (STT/TTS, etc.)
482
+ uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS, etc.
422
483
  showPopup, // AppProvider-owned popup layer (see note below)
423
484
  hidePopup,
424
485
  hideAllPopups,
@@ -428,9 +489,17 @@ const {
428
489
  } = useApp();
429
490
  ```
430
491
 
431
- `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.
432
499
 
433
- 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.
434
503
 
435
504
  ### Direct socket access
436
505
 
@@ -447,20 +516,23 @@ Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
447
516
  | `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
448
517
  | `disconnect()` | Close the connection. |
449
518
 
450
- 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`.
451
521
 
452
522
  ---
453
523
 
454
524
  ## Auth
455
525
 
456
- 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.
457
528
 
458
529
  Two modes, picked from the `auth` block in the project's `.uglyapp` config:
459
530
 
460
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.
461
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`.
462
533
 
463
- `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.
464
536
 
465
537
  ### Mode A — ugly.bot OAuth (default)
466
538
 
@@ -472,7 +544,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
472
544
 
473
545
  ### Mode B — self-issued sessions
474
546
 
475
- 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.
476
550
 
477
551
  - `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
478
552
  - Background ugly.bot silent SSO is a no-op.
@@ -482,7 +556,9 @@ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link pro
482
556
 
483
557
  ### Token-in-URL embed
484
558
 
485
- 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.
486
562
 
487
563
  ### Built-in routes
488
564
 
@@ -515,7 +591,9 @@ configurator.setAuth({
515
591
 
516
592
  ## Database — `TypedDB`
517
593
 
518
- 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.
519
597
 
520
598
  ### Writing
521
599
 
@@ -540,7 +618,10 @@ await db.batch(async () => {
540
618
  });
541
619
  ```
542
620
 
543
- 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.
544
625
 
545
626
  ### Reading
546
627
 
@@ -574,7 +655,8 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
574
655
  await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
575
656
  ```
576
657
 
577
- 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.
578
660
 
579
661
  ### Search
580
662
 
@@ -623,7 +705,9 @@ Imports available from `ugly-app`:
623
705
 
624
706
  ## AI
625
707
 
626
- 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.
627
711
 
628
712
  ### Server-side text generation
629
713
 
@@ -645,7 +729,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
645
729
  });
646
730
  ```
647
731
 
648
- 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.
649
735
 
650
736
  ### Server-side image generation
651
737
 
@@ -656,7 +742,8 @@ const imageGen = createImageGen(userId);
656
742
  const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
657
743
  ```
658
744
 
659
- `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).
660
747
 
661
748
  ### Embeddings
662
749
 
@@ -681,7 +768,8 @@ await search.enrichNews({ query: 'topic' });
681
768
 
682
769
  ### Client-side AI calls
683
770
 
684
- 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:
685
773
 
686
774
  ```ts
687
775
  import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
@@ -693,7 +781,8 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
693
781
 
694
782
  ### STT / TTS
695
783
 
696
- 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`).
697
786
 
698
787
  ```ts
699
788
  import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
@@ -702,7 +791,8 @@ const { start, stop, transcript, isListening } = useSTT(uglyBotSocket, options);
702
791
  const { speak, stop: stopTTS } = useTTS(uglyBotSocket);
703
792
  ```
704
793
 
705
- `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.
706
796
 
707
797
  ---
708
798
 
@@ -726,11 +816,15 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
726
816
  await storage.delete('public', destKey);
727
817
  ```
728
818
 
729
- 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.
730
822
 
731
- `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.
732
825
 
733
- 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).
734
828
 
735
829
  ---
736
830
 
@@ -762,9 +856,15 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
762
856
  configurator.setWorkers(cronTasks, cronHandlers);
763
857
  ```
764
858
 
765
- 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.
766
864
 
767
- 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).
768
868
 
769
869
  ---
770
870
 
@@ -779,7 +879,9 @@ configurator.setStrings({
779
879
  });
780
880
  ```
781
881
 
782
- 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.
783
885
 
784
886
  ---
785
887
 
@@ -804,7 +906,9 @@ export const experiments: Experiment[] = [
804
906
  configurator.setExperiments(experiments);
805
907
  ```
806
908
 
807
- 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.
808
912
 
809
913
  ---
810
914
 
@@ -859,7 +963,10 @@ The same developer source compiles for two runtimes via `src/server/adapter/`:
859
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.
860
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.
861
965
 
862
- 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.
863
970
 
864
971
  ---
865
972
 
@@ -888,7 +995,8 @@ Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `
888
995
  | `NEON_PROXY_URL` | Set by `dev:workers` — Neon-wire endpoint the local Neon HTTP driver dials. |
889
996
  | `CLOUDFLARE_WORKERS` | Set to `1` inside the Workers adapter for runtime detection. |
890
997
 
891
- 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_*`.
892
1000
 
893
1001
  ---
894
1002
 
@@ -913,7 +1021,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta
913
1021
  | `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
914
1022
  | `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
915
1023
 
916
- 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`.
917
1026
 
918
1027
  ---
919
1028
 
@@ -926,10 +1035,13 @@ Schema changes must be deliberate:
926
1035
  3. Replace every `REPLACE_ME` with the correct migration logic.
927
1036
  4. Run `npm run db:migrate`.
928
1037
 
929
- 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).
930
1040
 
931
1041
  ---
932
1042
 
933
1043
  ## Tech stack
934
1044
 
935
- 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"}