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.
Files changed (35) hide show
  1. package/README.md +179 -64
  2. package/dist/cli/anythingworld.d.ts +39 -0
  3. package/dist/cli/anythingworld.d.ts.map +1 -0
  4. package/dist/cli/anythingworld.js +162 -0
  5. package/dist/cli/anythingworld.js.map +1 -0
  6. package/dist/cli/index.js +22 -0
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/version.d.ts +1 -1
  9. package/dist/cli/version.js +1 -1
  10. package/dist/client/Logger.d.ts.map +1 -1
  11. package/dist/client/Logger.js +16 -1
  12. package/dist/client/Logger.js.map +1 -1
  13. package/dist/client/components/KeyboardProvider.d.ts.map +1 -1
  14. package/dist/client/components/KeyboardProvider.js +22 -0
  15. package/dist/client/components/KeyboardProvider.js.map +1 -1
  16. package/dist/client/createSocket.d.ts.map +1 -1
  17. package/dist/client/createSocket.js +12 -4
  18. package/dist/client/createSocket.js.map +1 -1
  19. package/dist/client/inspect/safearea.d.ts.map +1 -1
  20. package/dist/client/inspect/safearea.js +33 -23
  21. package/dist/client/inspect/safearea.js.map +1 -1
  22. package/dist/inspect/host-bundle.js +8 -8
  23. package/dist/shared/ProxyOps.d.ts +125 -2
  24. package/dist/shared/ProxyOps.d.ts.map +1 -1
  25. package/dist/shared/ProxyOps.js +94 -0
  26. package/dist/shared/ProxyOps.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/cli/anythingworld.ts +233 -0
  29. package/src/cli/index.ts +48 -0
  30. package/src/cli/version.ts +1 -1
  31. package/src/client/Logger.ts +19 -1
  32. package/src/client/components/KeyboardProvider.tsx +31 -0
  33. package/src/client/createSocket.ts +13 -4
  34. package/src/client/inspect/safearea.ts +38 -27
  35. 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 `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
 
@@ -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`. Provide `renderPage(routeName, params) => Promise<string>` to SSR any `ssr: true` pages. |
102
- | `setUserHelper(helper)` | Customize how the framework reads/writes the `user` collection during WS auth. |
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. Prefer `setWorkers()`. |
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 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,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 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
  |------|---------|
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, collections, and route declarations here.
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) and `POST /api/:name { input }` (HTTP). `z` is re-exported from Zod for convenience.
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 `dbDefaults()` to stamp `version` / `created` / `updated` on inserts. **Always generate `_id` with `nanoid()`** — never `crypto.randomUUID()`, `Date.now()`, or `Math.random()` (see `CLAUDE.md`).
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 app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as a last resort).
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 `createRouter({ pages, allPages?, ssrPages? })`.
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 carrying `PageMeta` plus a phantom `_params` used only for TypeScript inference. Options:
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 slashes). Query-string params are declared in `Params` but never appear in the path template.
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 name.
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 easily reach the typed one; it accepts an optional `router` prop and falls back to `<RouterProvider>` context.
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 full document reload (white flash + repaint). See the "client navigation" rule in `CLAUDE.md`.
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 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.
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 with a `console.error` when `buildUrl()` produces a URL that doesn't match a registered route.
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 owns the popup layer, drives a spring animation, and stacks popups z-index-correctly above every page.
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 `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.
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 account-sync with ugly.bot, and provider wiring.
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 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.
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 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.
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 `<MagicLinkCallback>` instead of the regular app shell.
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 `useApp()` inside any page to access the active user, socket, and app-scoped services.
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 — for direct platform calls (STT/TTS, etc.)
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 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.
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, 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.
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? })` from `ugly-app/client`.
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`, no client-side token handling.
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. `<AuthRoot>`) can branch correctly.
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 provider as primary; if `providers.google.clientId` is configured it adds Google as an extra provider on the same router.
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 and 302-redirect to the same URL without the token parameter — useful for embedding any page in an iframe.
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 `defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain collection name string.
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 keys are dot-notation, fully typed against the collection's schema. Partial updates go through optimistic concurrency to prevent lost updates on concurrent writers.
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 `onDelete` callbacks.
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 `UGLY_BOT_TOKEN` in the environment and the framework handles routing, balance tracking, retries, and per-user billing.
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'; // aliased as createTextGenClient
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 `ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq, Fireworks, Kimi, and Kie families.
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'; // aliased as createImageGenClient
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, FAL, Google Imagen, Wavespeed, Kie Kolors).
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 plumbing in the browser:
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 your app server (see the "STT/TTS routes to ugly.bot" rule in `CLAUDE.md`).
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 to the platform.
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 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.
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 isolation.
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 not exposed (browser uploads must go through a Worker endpoint).
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`, `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.
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 Triggers and durable queueing goes through Cloudflare Queues. On the Node adapter, `enqueueWorker` runs the handler inline (in-process queues only).
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 `window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` / `useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
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 unauthenticated users). The framework's `initSession` / `captureEvent` requests automatically tag events with the user's branch assignments.
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`, `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.
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 `import.meta.env.VITE_*`.
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 …` scripts — see `templates/CLAUDE.md`.
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` only as a temporary escape hatch).
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 · NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers + Durable Objects (Adapter B) · ugly.bot platform
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"}