diff --git a/.agents/skills/remix/SKILL.md b/.agents/skills/remix/SKILL.md
new file mode 100644
index 00000000..eb9e7d42
--- /dev/null
+++ b/.agents/skills/remix/SKILL.md
@@ -0,0 +1,561 @@
+---
+name: remix
+description:
+ Build and review Remix 3 applications using the `remix` npm package and
+ subpath imports. Use when working on Remix app structure, routes, controllers,
+ middleware, validation, data access, auth, sessions, file uploads, server
+ setup, UI components, hydration, navigation, or tests.
+---
+
+# Build a Remix App
+
+Use this skill for end-to-end Remix app work. It should help the agent choose
+the right layer first, reach for the right package, and avoid the most common
+Remix-specific mistakes.
+
+## What Remix Is
+
+Remix 3 is a server-first web framework built on Web APIs such as `Request`,
+`Response`, `URL`, and `FormData`. All packages ship from a single npm package,
+`remix`, and are imported via subpath. There is no top-level `remix` import.
+
+A Remix app has four main pieces:
+
+- **Routes** in `app/routes.ts` define the typed URL contract and power `href()`
+ generation.
+- **Controllers and actions** implement that contract and return `Response`
+ objects.
+- **Middleware** composes request lifecycle behavior and populates typed context
+ via `context.set(Key, value)`.
+- **Components** render UI with `remix/ui`. This is not React. A component
+ receives a `handle`, reads current props from `handle.props`, and returns a
+ render function.
+
+## When To Use This Skill
+
+Use this skill for:
+
+- new features or refactors that touch routing, controllers, middleware, data,
+ auth, sessions, UI, or tests
+- reviewing Remix app code for correctness, architecture, or framework usage
+- answering "how should this be structured in Remix?" questions
+- finding the right package, reference doc, or default pattern for a task
+
+## Load Only The References You Need
+
+Classify the task first, then load the smallest useful reference set. Each
+reference file starts with a "What This Covers" section that lists the topics
+inside it — read that first to confirm the file is relevant before reading the
+rest.
+
+Use the table below to find candidates. Loading more than two or three files at
+once is usually a sign that the task hasn't been narrowed enough yet.
+
+| Task involves... | Start with |
+| ----------------------------------------------------------------------------- | ------------------------------------------- |
+| Defining URLs, writing controllers and actions, returning responses | `references/routing-and-controllers.md` |
+| Composing the request lifecycle, ordering middleware, bridging to a server | `references/middleware-and-server.md` |
+| Compiling and serving browser modules, asset URL namespaces, preloads | `references/assets-and-browser-modules.md` |
+| Parsing input, validating with schemas, defining tables, querying, migrations | `references/data-and-validation.md` |
+| Per-browser state, login flows, route protection, identity | `references/auth-and-sessions.md` |
+| Component setup, state, lifecycle, updates, `queueTask`, context | `references/component-model.md` |
+| Event handlers, styles, refs, click/key behavior, simple animations | `references/mixins-styling-events.md` |
+| `clientEntry`, `run`, ``, navigation, `
` | `references/hydration-frames-navigation.md` |
+| Router tests, component tests, test isolation | `references/testing-patterns.md` |
+| Spring physics, tweens, layout transitions | `references/animate-elements.md` |
+| Authoring custom reusable mixins | `references/create-mixins.md` |
+
+Common bundles:
+
+- **Form or CRUD feature** -> routing, data and validation, testing; add auth if
+ user-specific
+- **Protected area** -> auth and sessions, routing, testing
+- **Interactive widget** -> component model, mixins and styling; add hydration
+ only if it runs in the browser
+- **Browser asset pipeline** -> assets and browser modules, hydration,
+ middleware and server
+- **File upload** -> middleware and server, data and validation, testing
+- **Navigation or frames** -> hydration, frames, navigation
+
+## Default Workflow
+
+1. **Classify the change.** Decide whether it changes the route contract,
+ request lifecycle, data model, auth or session behavior, or only UI.
+2. **Start from the server contract.** Add or update `app/routes.ts` before
+ wiring handlers or UI.
+3. **Put code in the narrowest owner.** Favor route-local code first, then
+ promote only when reuse is real.
+4. **Make the server path correct before adding browser behavior.** A route
+ should return the right `Response` via `router.fetch(...)` before you add
+ `clientEntry(...)`, animations, or DOM effects.
+5. **Add middleware deliberately.** Keep fast-exit middleware early and
+ request-enriching middleware later. Export a typed `AppContext` from the root
+ middleware stack and use it in controllers.
+6. **Validate input at the boundary.** Parse and validate `Request`, `FormData`,
+ params, cookies, and external payloads before they reach rendering or
+ persistence logic.
+7. **Hydrate only when necessary.** Prefer server-rendered UI. Use
+ `clientEntry(...)` and `run(...)` only for real browser interactivity or
+ browser-only APIs.
+8. **Test the narrowest meaningful layer.** Prefer router tests for route
+ behavior. Use component tests when the behavior is truly interactive or
+ DOM-specific.
+9. **Finish with verification.** Re-read the route flow, confirm auth and
+ authorization boundaries, and run the smallest relevant test and typecheck
+ loop.
+
+## Project Layout
+
+Use these root directories consistently:
+
+- `app/` for runtime application code
+- `db/` for migrations and local database files
+- `public/` for static assets served as-is
+- `test/` for shared helpers, fixtures, and integration coverage
+- `tmp/` for uploads, caches, local session files, and other scratch data
+
+Inside `app/`, organize by responsibility:
+
+- `assets/` for client entrypoints and client-owned browser behavior
+- `controllers/` for route-owned handlers and route-local UI
+- `data/` for schema, queries, persistence setup, migrations, and runtime data
+ initialization
+- `middleware/` for request lifecycle concerns such as auth, sessions, uploads,
+ and database injection
+- `ui/` for shared cross-route UI primitives
+- `utils/` only for genuinely cross-layer helpers that do not clearly belong
+ elsewhere
+- `routes.ts` for the route contract
+- `router.ts` for router setup and wiring
+
+### Placement Precedence
+
+When code could live in multiple places:
+
+1. Put it in the narrowest owner first.
+2. If it belongs to one route, keep it with that route.
+3. If it is shared UI across route areas, move it to `app/ui/`.
+4. If it is request lifecycle setup, keep it in `app/middleware/`.
+5. If it is schema, query, persistence, or startup data logic, keep it in
+ `app/data/`.
+6. Use `app/utils/` only as a last resort for truly cross-layer helpers.
+
+### Route Ownership
+
+- Use a flat file in `app/controllers/` for a simple leaf action, such as
+ `app/controllers/home.tsx`
+- Use a folder with `controller.tsx` when a route owns nested routes or multiple
+ actions, such as `app/controllers/account/controller.tsx`
+- Mirror nested route structure on disk, such as
+ `app/controllers/auth/login/controller.tsx`
+- Keep route-local UI next to its owner, such as
+ `app/controllers/contact/page.tsx`
+- Move shared UI to `app/ui/`
+- If a flat leaf grows child routes or multiple actions, promote it to a
+ controller folder
+
+### Layout Anti-Patterns
+
+- Do not create `app/lib/` as a generic dumping ground
+- Do not create `app/components/` as a second shared UI bucket when `app/ui/`
+ already owns that role
+- Do not put shared cross-route UI in `app/controllers/`
+- Do not put middleware or persistence helpers in `app/utils/` when they have a
+ clearer home
+- Do not create folders for simple leaf actions unless they are real controllers
+
+## Core Remix Rules
+
+- Import from `remix/`, never `import { ... } from 'remix'`
+- Treat `app/routes.ts` as the source of truth for URLs. Use
+ `routes..href(...)` for redirects, links, tests, and internal URL
+ construction
+- Controllers and actions should return explicit `Response` objects, including
+ redirects, 404s, and validation failures. At the route boundary, prefer
+ returning a `Response` for expected outcomes (validation errors, conflicts,
+ not found) over throwing for control flow
+- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules,
+ and content types are part of the route contract
+- Make the server route correct first. A POST should already return the right
+ HTML, redirect, or error response on its own before `clientEntry(...)` layers
+ interactivity on top
+- Validate input at the boundary using `remix/data-schema` (and
+ `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a
+ return value instead of an exception
+- Derive `AppContext` from the root middleware stack so `get(Database)`,
+ `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller
+ never reads from context, it doesn't need the harness
+- Outside actions and controllers, only use `getContext()` when `asyncContext()`
+ is in the middleware stack
+- Remix Component is not React: read props from `handle.props`, keep state in
+ setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive
+ work in event handlers or `queueTask(...)`, not in render
+- Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling
+ instead of inventing custom host prop conventions. Use `mix={[...]}` only when
+ composing multiple mixins
+- Hydrated `clientEntry(...)` props must be serializable. Do not pass functions,
+ class instances, or opaque runtime objects
+
+## Security And Session Defaults
+
+- Never ship demo secrets. In non-test environments, require session and
+ provider secrets from the environment and fail fast if they are missing
+- Use hardened cookies: `httpOnly` always, `sameSite` by default, and `secure`
+ when serving over HTTPS
+- Regenerate session IDs on login, logout, and privilege changes
+- Use `requireAuth()` to protect authenticated route areas, but still authorize
+ resource ownership inside handlers and data writes
+- Add CSRF protection when browser forms mutate state using cookie-backed
+ sessions
+- Add CORS only for endpoints that must be called cross-origin. Prefer
+ same-origin by default
+- Prefer JSX or `remix/html-template` for HTML generation so escaping stays
+ correct
+- Validate uploads for size, type, and destination. Treat filenames and content
+ as untrusted input
+
+## Testing Defaults
+
+- Prefer server and router tests first. Drive the app with
+ `router.fetch(new Request(...))` and assert on the returned `Response`
+- Build a fresh router per test or per suite so sessions, in-memory storage, and
+ database state stay isolated
+- Use `routes..href(...)` in tests so URLs stay coupled to the route
+ contract
+- For auth or session scenarios, use a test cookie and
+ `createMemorySessionStorage()` instead of production storage
+- Use component tests only for interactive or DOM-specific behavior. Render with
+ `createRoot(...)`, interact with the real DOM, and call `root.flush()` between
+ steps
+- Prefer one representative behavior test over many repetitive assertion
+ variants
+
+## Common Mistakes To Avoid
+
+- Treating Remix Component like React and reaching for hooks or implicit
+ rerendering
+- Importing from a top-level `remix` entry instead of a subpath
+- Adding `clientEntry(...)` before the server-rendered route behavior is correct
+- Passing non-serializable props into `clientEntry(...)`
+- Calling `getContext()` without `asyncContext()` in the middleware stack
+- Getting middleware order wrong; fast exits like static files belong early,
+ request enrichment later
+- Skipping boundary validation and trusting raw `FormData`, params, cookies, or
+ external payloads
+- Letting route-local domain errors leak out of the controller. Translate
+ expected outcomes (validation, conflicts, not-found) into the HTTP `Response`
+ the route means to return rather than throwing a custom `Error` subclass and
+ catching it elsewhere
+- Reaching for `createCookie` when a tamper-sensitive or server-managed
+ per-browser fact really wants `remix/session`. If editing the value would be a
+ bug, use a session
+- Building a JSON-only RPC layer when a normal form POST, redirect, or resource
+ route would be simpler. Fetch-from-the-client is a layer on top of sound route
+ behavior, not a replacement for it
+- Treating JSON state endpoints and `` reloads as mutually exclusive
+ patterns. Pick the lightest sync mechanism that fits the UX; small widgets may
+ reasonably poll a JSON endpoint
+- Assuming authentication is enough without per-resource authorization checks
+- Dropping shared code into vague buckets like `utils.ts`, `helpers.ts`, or
+ `common.ts` when ownership is known
+- Writing only component tests for a feature whose main behavior is really an
+ HTTP route concern
+
+## Package Map
+
+Use this map to find the right package quickly. Each entry says what the package
+is for, not just what it exports. Open the linked reference file when you need
+full examples.
+
+### Routing, Server, and Responses
+
+- `remix/fetch-router` — the router itself. Use for `createRouter`, controller
+ and middleware types, and registering routes
+- `remix/fetch-router/routes` — declarative route builders. Use for `route`,
+ `get`, `post`, `put`, `del`, `form`, `resources` when defining `app/routes.ts`
+- `remix/node-fetch-server` — adapter from Node's `http` module to a Fetch-style
+ router. Use for `createRequestListener` in `server.ts`
+- `remix/assets` — browser asset server. Use for `createAssetServer` when
+ serving compiled scripts and styles, getting public hrefs, and emitting
+ preloads. Shared compiler options such as `target`, `sourceMaps`,
+ `sourceMapSourcePaths`, and `minify` live at the top level
+- `remix/headers` — typed header parsers and builders. Use when reading
+ `Accept`, `Cookie`, or setting `CacheControl`, `Vary`, etc., instead of
+ hand-formatting strings
+- `remix/response/redirect` — `redirect(href, status?)`. Use for the canonical
+ "POST then redirect" pattern and other location changes
+- `remix/response/html` — `createHtmlResponse`. Use when you need an HTML
+ `Response` from a string or stream without rendering through `remix/ui`
+- `remix/response/compress` — `compressResponse`. Use when compressing one-off
+ responses outside the global `compression()` middleware
+- `remix/response/file` — file-download responses. Use for
+ `Content-Disposition: attachment` responses
+- `remix/route-pattern` — low-level URL matching and generation. Use when
+ working with raw patterns outside the router (custom matchers, scripts)
+- `remix/fetch-proxy` — Fetch-based HTTP proxying. Use to forward a request to
+ another origin; pass `xForwardedHeaders` when the upstream needs forwarded
+ proto, host, and port
+
+### Data, Validation, and Persistence
+
+- `remix/data-schema` — schema builders for runtime validation. Use for `parse`
+ and `parseSafe` to validate any input that crosses a trust boundary, and
+ `.transform(...)` when validated output should map to a different value or
+ type
+- `remix/data-schema/checks` — common check helpers (`email`, `minLength`,
+ `maxLength`, etc.). Use to compose into a schema
+- `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans,
+ dates, and ids. Use when input arrives as a string but should be a typed value
+- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing
+ `FormData` directly. Use in actions that read browser forms
+- `remix/data-table` — typed tables and a `Database` interface. Use for `table`,
+ `column`, `createDatabase` when modeling persisted data
+- `remix/data-table-sqlite`, `remix/data-table-postgres`,
+ `remix/data-table-mysql` — adapters. Use to back `createDatabase` with a real
+ engine. SQLite accepts Node, Bun, and compatible synchronous clients with the
+ shared `prepare`/`exec` surface
+- `remix/data-table/migrations` — migration authoring and runners. Use for
+ `createMigration`, `createMigrationRunner`
+- `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in
+ startup scripts that apply migrations
+- `remix/data-table/operators` — query operators such as `inList(...)`. Use when
+ `where` clauses need set or comparison logic
+
+### Auth, Sessions, and Cookies
+
+- `remix/session` — the `Session` object: `get`, `set`, `flash`, `unset`,
+ `regenerateId`. Use for any per-browser state where tampering would be a bug
+ (login, "I submitted this form already", cart, flash messages)
+- `remix/session-middleware` — `session(cookie, storage)`. Use to wire a session
+ cookie and storage backend into the root middleware stack
+- `remix/session/fs-storage`, `remix/session/memory-storage`,
+ `remix/session/cookie-storage` — storage backends. Use `fs-storage` for
+ single-process apps, `memory-storage` for tests, `cookie-storage` for
+ stateless deployments where data fits in a cookie
+- `remix/session-storage-redis` — Redis-backed storage. Use for multi-process or
+ multi-host deployments
+- `remix/session-storage-memcache` — Memcache-backed storage. Same multi-host
+ use case as Redis
+- `remix/cookie` — `createCookie` for plain signed/unsigned cookies. Use for
+ non-sensitive preferences where the client is allowed to control the value
+ (theme, locale, dismissed banner). For state where tampering matters, prefer
+ `remix/session`
+- `remix/auth` — credentials, OAuth, OIDC, and Atmosphere providers. Use to
+ define how identity is verified, start/finish external login, and refresh
+ stored OAuth/OIDC token bundles with `refreshExternalAuth(...)`
+- `remix/auth-middleware` — `auth({ schemes })`, `requireAuth`, the `Auth`
+ context key. Use to resolve identity into the request context and to gate
+ routes
+
+### UI, Hydration, and Browser Behavior
+
+- `remix/ui` — the component runtime: components, core mixins, `clientEntry`,
+ `run`, ``, navigation helpers, and `createRoot`. Use for app UI
+ behavior
+- `remix/ui/server` — server rendering: `renderToStream`, `renderToString`. Use
+ in the `render(...)` helper that returns HTML responses
+- `remix/ui/animation` — animation APIs: `animateEntrance`, `animateExit`,
+ `animateLayout`, `spring`, `tween`, and `easings`
+- `remix/ui/` — UI primitives, mixins, glyphs, and theme helpers.
+ Import from `remix/ui/accordion`, `remix/ui/button`, `remix/ui/select`, etc.
+- `remix/ui/test` — component test rendering helpers such as `render`
+- `remix/ui/jsx-runtime` — JSX transform target. Configured in `tsconfig.json`,
+ rarely imported directly
+- `remix/html-template` — escaped HTML template literals. Use when generating
+ HTML outside the component system (RSS feeds, email bodies, error pages)
+- `remix/file-storage` — backend-agnostic `File` storage interface. Use as the
+ type bound for upload destinations
+- `remix/file-storage/fs`, `remix/file-storage/memory`, `remix/file-storage-s3`
+ — storage backends. Use to implement an upload destination
+
+### Middleware
+
+- `remix/static-middleware` — `staticFiles(dir)`. Use to serve files from
+ `public/` exactly as they exist on disk
+- `remix/form-data-middleware` — `formData()`. Use to parse `FormData` once and
+ expose it via `get(FormData)` instead of calling `await request.formData()` in
+ each action
+- `remix/form-data-parser` — lower-level `parseFormData`, `FileUpload`. Use when
+ implementing custom upload handlers. Upload handler errors propagate directly
+- `remix/multipart-parser` and `remix/multipart-parser/node` — low-level
+ multipart stream parsing. `MultipartPart.headers` is a plain object keyed by
+ lower-case header name; read values with bracket notation such as
+ `part.headers['content-type']`
+- `remix/compression-middleware` — `compression()`. Use globally for text-like
+ responses
+- `remix/logger-middleware` — `logger()`. Use in development for request logs;
+ pass `colors` to force terminal color output on or off
+- `remix/method-override-middleware` — `methodOverride()`. Use when HTML forms
+ need `PUT`, `PATCH`, or `DELETE`
+- `remix/async-context-middleware` — `asyncContext()`, `getContext()`. Use when
+ helpers outside actions need request context without threading it through
+ every call
+- `remix/cors-middleware` — `cors(opts?)`. Use for endpoints called cross-origin
+- `remix/csrf-middleware` — `csrf(opts?)`. Use when session-backed forms mutate
+ state and need synchronizer-token CSRF protection
+- `remix/cop-middleware` — cross-origin protection. Use to reject unsafe
+ cross-origin browser requests
+
+### Test
+
+- `remix/test` — `describe`, `it`, and lifecycle hooks. Use as the test
+ framework
+- `remix/test/cli` — programmatic test runner APIs such as `runRemixTest`
+- `remix/cli` — programmatic Remix CLI API. Use the `remix` executable for
+ project commands such as `remix test`, `remix routes`, and `remix doctor`
+- `remix/assert` — assertion helpers. Use in place of `node:assert` so messages
+ render cleanly in the runner
+- `remix/terminal` — ANSI styles, color detection, style factories, and testable
+ terminal streams. Use for CLIs and terminal output instead of hand-rolled
+ escape sequences
+
+## Canonical Patterns
+
+### Define routes first
+
+```typescript
+import { form, get, post, resources, route } from 'remix/fetch-router/routes'
+
+export const routes = route({
+ home: '/',
+ contact: form('contact'),
+ books: {
+ index: '/books',
+ show: '/books/:slug',
+ },
+ auth: route('auth', {
+ login: form('login'),
+ logout: post('logout'),
+ }),
+ admin: route('admin', {
+ index: get('/'),
+ books: resources('books', { param: 'bookId' }),
+ }),
+})
+```
+
+### Type controllers against the route contract
+
+```typescript
+import type { Controller } from 'remix/fetch-router'
+
+import type { AppContext } from '../router.ts'
+import { routes } from '../routes.ts'
+
+export default {
+ actions: {
+ async index({ get }) {
+ let db = get(Database)
+ let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
+ return render()
+ },
+ async show({ get, params }) {
+ let db = get(Database)
+ let book = await db.findOne(books, { where: { slug: params.slug } })
+ if (!book) return new Response('Not Found', { status: 404 })
+ return render()
+ },
+ },
+} satisfies Controller
+```
+
+### Compose middleware deliberately
+
+```typescript
+import {
+ createRouter,
+ type AnyParams,
+ type MiddlewareContext,
+ type WithParams,
+} from 'remix/fetch-router'
+
+export type RootMiddleware = [
+ ReturnType,
+ ReturnType,
+ ReturnType,
+ ReturnType,
+]
+
+export type AppContext = WithParams<
+ MiddlewareContext,
+ params
+>
+
+let middleware = []
+
+if (process.env.NODE_ENV === 'development') {
+ middleware.push(logger())
+}
+
+middleware.push(compression())
+middleware.push(staticFiles('./public'))
+middleware.push(formData())
+middleware.push(methodOverride())
+middleware.push(session(cookie, storage))
+middleware.push(asyncContext())
+middleware.push(loadDatabase())
+middleware.push(loadAuth())
+
+let router = createRouter({ middleware })
+```
+
+### Mutate, validate, and respond
+
+```typescript
+import { redirect } from 'remix/response/redirect'
+import * as s from 'remix/data-schema'
+import * as f from 'remix/data-schema/form-data'
+import { Session } from 'remix/session'
+import { Database } from 'remix/data-table'
+
+let bookSchema = f.object({
+ slug: f.field(s.string()),
+ title: f.field(s.string()),
+})
+
+export default {
+ actions: {
+ async create({ get }) {
+ let parsed = s.parseSafe(bookSchema, get(FormData))
+ if (!parsed.success) {
+ return render(, { status: 400 })
+ }
+
+ let db = get(Database)
+ let book = await db.create(books, parsed.value)
+
+ let session = get(Session)
+ session.flash('message', `Added ${book.title}.`)
+
+ return redirect(routes.books.show.href({ slug: book.slug }))
+ },
+ },
+} satisfies Controller
+```
+
+This shape works without JavaScript, returns a `Response` for every outcome, and
+is ready for `clientEntry(...)` interactivity when the UI needs it.
+
+### Build UI from handle props plus render
+
+```tsx
+import { on, type Handle } from 'remix/ui'
+
+function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
+ let count = handle.props.initialCount ?? 0
+
+ return () => (
+
+ )
+}
+```
+
+Only add `clientEntry(...)` and `run(...)` when the component needs browser
+interactivity or browser-only APIs.
diff --git a/.agents/skills/remix/references/animate-elements.md b/.agents/skills/remix/references/animate-elements.md
new file mode 100644
index 00000000..344d6f64
--- /dev/null
+++ b/.agents/skills/remix/references/animate-elements.md
@@ -0,0 +1,222 @@
+# Animating Elements
+
+## What This Covers
+
+How to animate insertion, removal, and layout changes of elements. Read this
+when the task involves:
+
+- Adding entrance, exit, or shared-layout transitions to UI
+- Choosing between spring physics (`spring(...)`) and time-based easing
+ (`tween`)
+- Coordinating CSS transitions with the same easing as JS animations
+- Imperative animation loops via `requestAnimationFrame`
+
+Import animation APIs from `remix/ui/animation`. For the smaller set of
+animation helpers that show up alongside other mixins, see
+`mixins-styling-events.md`.
+
+## Animation Mixins
+
+### `animateEntrance(config)`
+
+Animates an element when inserted. Config specifies the **starting** style the
+element animates **from**:
+
+```tsx
+
+```
+
+### `animateExit(config)`
+
+Animates an element when removed. Config specifies the **ending** style the
+element animates **to**. The element stays in the DOM until the animation
+completes:
+
+```tsx
+{
+ isVisible && (
+
+ )
+}
+```
+
+### `animateLayout(config?)`
+
+Animates layout changes (position/size) using FLIP-style transforms:
+
+```tsx
+{
+ items.map((item) => (
+
+ ))
+}
+```
+
+Options: `duration` (default 200ms), `easing` (default spring snappy), `size`
+(default true — include scale projection for size changes).
+
+### Combining mixins
+
+```tsx
+
+```
+
+### Shared-layout swap
+
+```tsx
+
+```
+
+## Spring API
+
+Physics-based spring animation. Returns a `SpringIterator` with `duration`,
+`easing`, and `toString()` for CSS.
+
+### Presets
+
+| Preset | Bounce | Duration | Character |
+| -------- | ------ | -------- | --------------------------- |
+| `smooth` | -0.3 | 400ms | Overdamped, no overshoot |
+| `snappy` | 0 | 200ms | Critically damped, quick |
+| `bouncy` | 0.3 | 400ms | Underdamped, visible bounce |
+
+```tsx
+spring('bouncy')
+spring('snappy')
+spring('smooth')
+spring('bouncy', { duration: 300 }) // override duration
+```
+
+### Custom spring
+
+```tsx
+spring({ duration: 500, bounce: 0.3 })
+spring({ duration: 500, bounce: 0.3, velocity: 2 }) // continue momentum from gesture
+```
+
+### Spread into animation mixins
+
+Spreading a spring gives both `duration` and `easing`:
+
+```tsx
+animateEntrance({ opacity: 0, ...spring('bouncy') })
+```
+
+### CSS transitions
+
+The iterator stringifies to `"550ms linear(...)"`:
+
+```tsx
+css({ transition: `width ${spring('bouncy')}` })
+```
+
+Or use the `spring.transition()` helper for multiple properties:
+
+```tsx
+css({ transition: spring.transition('width', 'bouncy') })
+css({ transition: spring.transition(['left', 'top'], 'snappy') })
+```
+
+### Web Animations API
+
+```tsx
+element.animate(keyframes, { ...spring('bouncy') })
+```
+
+### JS iteration
+
+The iterator yields position values from 0 to 1, one per frame:
+
+```tsx
+for (let t of spring('bouncy')) {
+ let x = from + (to - from) * t
+ updateSomething(x)
+ await nextFrame()
+}
+```
+
+## Tween API
+
+Generator-based tween for animating values over time with cubic bezier easing.
+Prefer animation mixins or CSS transitions with `spring` for most UI work. Use
+`tween` for imperative `requestAnimationFrame` loops, canvas/WebGL, or non-CSS
+properties.
+
+```tsx
+import { tween, easings } from 'remix/ui/animation'
+
+let animation = tween({
+ from: 0,
+ to: 100,
+ duration: 300,
+ curve: easings.easeOut,
+})
+
+animation.next() // initialize
+function tick(timestamp: number) {
+ if (handle.signal.aborted) return
+ let { value, done } = animation.next(timestamp)
+ element.style.transform = `translateX(${value}px)`
+ if (!done) requestAnimationFrame(tick)
+}
+requestAnimationFrame(tick)
+```
+
+Built-in easings: `easings.linear`, `easings.ease`, `easings.easeIn`,
+`easings.easeOut`, `easings.easeInOut`.
+
+## Practical Guidance
+
+- Always key conditional or switching elements you expect to animate.
+- Use `animateLayout` only on the element whose position or size changes.
+- Prefer one clear transition intent per mixin: entrance starts from a style,
+ exit ends at a style.
+- Default to `...spring()` for duration and easing in most cases.
+- Keep DOM work in `handle.queueTask(...)` or `ref(...)`, not in render.
diff --git a/.agents/skills/remix/references/assets-and-browser-modules.md b/.agents/skills/remix/references/assets-and-browser-modules.md
new file mode 100644
index 00000000..45e7387d
--- /dev/null
+++ b/.agents/skills/remix/references/assets-and-browser-modules.md
@@ -0,0 +1,133 @@
+# Assets and Browser Modules
+
+## What This Covers
+
+How to serve browser scripts and styles from source. Read this when the task
+involves:
+
+- Configuring `createAssetServer` (`fileMap`, `allow`, `deny`, fingerprinting,
+ compiler options)
+- Choosing between `staticFiles()` for already-built files and
+ `createAssetServer()` for source assets that need import rewriting, preloads,
+ or fingerprinted URLs
+- Generating script URLs or `` tags for a client entry
+- Keeping server-only files out of the browser via `deny` rules
+
+For routing the URL namespace itself, see `routing-and-controllers.md`. For
+client entry hydration, see `hydration-frames-navigation.md`.
+
+## When To Reach For It
+
+Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS
+from source files. This is the right tool for client entrypoints, browser-only
+helpers, styles under `app/assets/`, and monorepo code that should be compiled
+and served under a public URL namespace.
+
+Use `staticFiles()` for files that already exist on disk exactly as they should
+be served. Use `createAssetServer()` for source scripts or styles that need
+rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
+
+## Default Pattern
+
+```typescript
+import * as path from 'node:path'
+
+import { createAssetServer } from 'remix/assets'
+import { createRouter } from 'remix/fetch-router'
+
+let assetServer = createAssetServer({
+ rootDir: path.resolve(import.meta.dirname, '..'),
+ fileMap: {
+ '/assets/app/*path': 'app/*path',
+ '/assets/packages/*path': '../packages/*path',
+ },
+ allow: ['app/assets/**', '../packages/**'],
+ deny: ['app/**/*.server.*'],
+ target: { es: '2020', chrome: '109', safari: '16.4' },
+ sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
+ minify: process.env.NODE_ENV === 'production',
+ scripts: {
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify(
+ process.env.NODE_ENV ?? 'development',
+ ),
+ },
+ },
+})
+
+let router = createRouter()
+
+router.get('/assets/*path', ({ request }) => {
+ return assetServer.fetch(request)
+})
+```
+
+## Rules
+
+- Treat `allow` and `deny` as the security boundary for browser-reachable source
+ files.
+- Add a `deny` list for server-only modules such as `*.server.*`, private
+ config, or other files that should never be exposed.
+- Set `rootDir` explicitly in monorepos so relative paths resolve from the
+ intended project root.
+- `fileMap` keys are public URL patterns and values are root-relative file path
+ patterns. They use `route-pattern` syntax on both sides.
+- Keep the same wildcard params on both sides of a `fileMap` entry so import
+ rewriting can map source files back to public URLs.
+- CSS files are compiled and served alongside scripts. Local CSS `@import` rules
+ are rewritten and fingerprinted with the same asset server routing rules.
+
+## Rendering HTML
+
+Use `getHref()` when you need the public URL for one module, and `getPreloads()`
+when you want `` tags or `Link` headers for one or
+more entrypoints and their dependencies.
+
+```typescript
+let entryHref = await assetServer.getHref('app/assets/entry.ts')
+let preloads = await assetServer.getPreloads(['app/assets/entry.ts'])
+```
+
+Use this when rendering documents or layouts that boot browser behavior with a
+known client entry.
+
+When resolving hydrated client entries during server rendering, pass the source
+entry ID from `clientEntry(import.meta.url, ...)` to `getHref()` inside
+`resolveClientEntry`. Keep export-name resolution in that render helper, and
+avoid hard-coding public asset URLs in source-owned component modules.
+
+## Development vs Deployment
+
+In development:
+
+- Keep `watch` enabled so source changes are picked up without restarting the
+ server
+- Prefer stable URLs with normal revalidation
+- Enable source maps when debugging browser code
+
+In deployment:
+
+- Set `watch: false`
+- Use `fingerprint: { buildId }` for long-lived immutable caching
+- Make sure `buildId` changes for each deploy
+
+Fingerprinting assumes files on disk are stable and requires `watch: false`.
+
+## Useful Compiler Options
+
+- `minify` for production minification of scripts and styles
+- `sourceMaps` for `'external'` or `'inline'` source maps for scripts and styles
+- `sourceMapSourcePaths` for `'url'` or `'absolute'` source map paths
+- `target` as an object for shared browser targets and script-only ECMAScript
+ output, such as `{ es: '2020', chrome: '109', safari: '16.4' }`
+- `scripts.define` to replace globals such as `process.env.NODE_ENV`
+- `scripts.external` to leave specific script imports untouched
+
+Do not nest shared compiler options under `scripts`. Use top-level `minify`,
+`sourceMaps`, `sourceMapSourcePaths`, and `target` so they apply to styles as
+well as scripts.
+
+## Lifecycle
+
+If the asset server is long-lived and watching the file system, call
+`await assetServer.close()` when shutting down dev servers or disposing tests.
diff --git a/.agents/skills/remix/references/auth-and-sessions.md b/.agents/skills/remix/references/auth-and-sessions.md
new file mode 100644
index 00000000..58982ab8
--- /dev/null
+++ b/.agents/skills/remix/references/auth-and-sessions.md
@@ -0,0 +1,436 @@
+# Authentication and Sessions
+
+## What This Covers
+
+How to remember things about a browser between requests and how to identify a
+user. Read this when the task involves:
+
+- Storing per-browser state across requests (login, cart, "I have submitted this
+ form")
+- Adding a credentials login flow or an OAuth provider
+- Protecting routes with `requireAuth()` or stacking authorization checks
+- Reading or writing `Session`, `Auth`, or other identity-related context values
+- Logging in, logging out, or rotating session IDs
+
+For raw cookies that are not session-backed (theme, locale, dismissed-banner),
+see `createCookie` in this file plus the broader `Package Map` in `SKILL.md`.
+
+## Sessions vs Plain Cookies
+
+Reach for `remix/session` when state is sensitive, must be tamper-resistant, or
+represents the identity of a request: who is logged in, which form a browser
+already submitted, what items are in a cart. Sessions sign or encrypt their
+backing cookie with a server-held secret and give you a typed `Session` object
+you can `get`, `set`, `flash`, `unset`, and `regenerateId`.
+
+Reach for `remix/cookie` directly when the browser is allowed to carry the value
+and the server does not need session semantics. This often means preferences
+(theme, locale, dismissed banner), but a signed cookie can also be fine for
+small low-risk values where you truly only need one cookie-shaped fact and do
+not need `Session` helpers.
+
+If a malicious user editing the value would be a bug, or if the value needs
+server-managed lifecycle, reach for a session.
+
+### Quick chooser
+
+| Need | Best fit | Why |
+| ------------------------------------------------------------------- | --------------- | -------------------------------------------------- |
+| Theme, locale, dismissed banner | `remix/cookie` | Browser-controlled preference |
+| Small signed hint with minimal lifecycle | `remix/cookie` | One value, no `Session` helpers needed |
+| "This browser already submitted", cart, flash messages, login state | `remix/session` | Tamper-sensitive, server-managed per-browser state |
+| "One real person only", ownership, durable identity | account/auth | Cookies or sessions alone do not prove personhood |
+
+## Session Setup
+
+### Create a session cookie
+
+```typescript
+import { createCookie } from 'remix/cookie'
+
+let sessionSecret = process.env.SESSION_SECRET
+if (!sessionSecret && process.env.NODE_ENV !== 'test') {
+ throw new Error('SESSION_SECRET is required')
+}
+
+export let sessionCookie = createCookie('session', {
+ secrets: [sessionSecret ?? 'test-only-secret'],
+ httpOnly: true,
+ sameSite: 'Lax',
+ secure: process.env.NODE_ENV === 'production',
+ maxAge: 2592000, // 30 days
+ path: '/',
+})
+```
+
+The cookie should always be `httpOnly`, default to `sameSite: 'Lax'`, and be
+`secure` in production. Demo defaults like `'s3cr3t'` are fine in tests but
+should never reach production — fail fast when the secret is missing.
+
+### Create session storage
+
+```typescript
+// Filesystem storage
+import { createFsSessionStorage } from 'remix/session/fs-storage'
+export let sessionStorage = createFsSessionStorage('./tmp/sessions')
+
+// Memory storage (for tests)
+import { createMemorySessionStorage } from 'remix/session/memory-storage'
+export let sessionStorage = createMemorySessionStorage()
+```
+
+### Add session middleware
+
+```typescript
+import { session } from 'remix/session-middleware'
+
+let router = createRouter({
+ middleware: [
+ session(sessionCookie, sessionStorage),
+ // ... other middleware
+ ],
+})
+```
+
+### Using sessions in handlers
+
+```typescript
+import { Session } from 'remix/session'
+
+async function handler({ get }) {
+ let session = get(Session)
+
+ // Read
+ let userId = session.get('userId')
+
+ // Write
+ session.set('userId', 42)
+
+ // Flash (read once, then cleared)
+ session.flash('message', 'Settings saved!')
+ let message = session.get('message') // returns and clears
+
+ // Remove a key
+ session.unset('userId')
+
+ // Regenerate session ID (after login/logout)
+ session.regenerateId(true)
+}
+```
+
+### Sessions for non-auth state
+
+Sessions are not just for login. They are the right place to store any
+tamper-sensitive per-browser fact: which form a browser already submitted, how
+many free actions are left in a trial, which feature flags a tester opted into,
+what items are in a cart.
+
+```typescript
+async function submit({ get }) {
+ let session = get(Session)
+ if (session.get('hasSubmitted')) {
+ return render(, { status: 409 })
+ }
+
+ let parsed = s.parseSafe(submitSchema, get(FormData))
+ if (!parsed.success) {
+ return render(, { status: 400 })
+ }
+
+ await saveSubmission(parsed.value)
+ session.set('hasSubmitted', true)
+ session.flash('message', 'Thanks for submitting!')
+
+ return redirect(routes.thanks.href())
+}
+```
+
+Notice that there is no manual `Set-Cookie` plumbing in the action — the session
+middleware handles that, and the handler returns an ordinary `Response`.
+Per-browser state enforced this way is still bypassable by clearing cookies; if
+the guarantee needs to survive that, you also need an account (see auth
+providers below).
+
+## Auth Middleware
+
+### Basic setup
+
+```typescript
+import { auth, createSessionAuthScheme } from 'remix/auth-middleware'
+import { Session } from 'remix/session'
+import { Database } from 'remix/data-table'
+
+export function loadAuth() {
+ return auth({
+ schemes: [
+ createSessionAuthScheme({
+ read(session) {
+ let data = session.get('auth')
+ return data ?? null
+ },
+ async verify(value, context) {
+ let db = context.get(Database)
+ return (await db.find(users, value.userId)) ?? null
+ },
+ invalidate(session) {
+ session.unset('auth')
+ },
+ }),
+ ],
+ })
+}
+```
+
+### Reading auth state
+
+```typescript
+import { Auth } from 'remix/auth-middleware'
+
+function handler({ get }) {
+ let auth = get(Auth)
+
+ if (auth.ok) {
+ // User is authenticated
+ let user = auth.identity
+ }
+}
+```
+
+## Credentials Auth
+
+### Define a credentials provider
+
+```typescript
+import {
+ createCredentialsAuthProvider,
+ verifyCredentials,
+ completeAuth,
+} from 'remix/auth'
+import * as s from 'remix/data-schema'
+import * as f from 'remix/data-schema/form-data'
+
+let loginSchema = f.object({
+ email: f.field(s.defaulted(s.string(), '')),
+ password: f.field(s.defaulted(s.string(), '')),
+})
+
+export let passwordProvider = createCredentialsAuthProvider({
+ parse(context) {
+ let formData = context.get(FormData)
+ return s.parse(loginSchema, formData)
+ },
+ async verify({ email, password }, context) {
+ let db = context.get(Database)
+ let user = await db.findOne(users, { where: { email } })
+ if (!user || !(await verifyPassword(password, user.password_hash))) {
+ return null
+ }
+ return user
+ },
+})
+```
+
+### Login action
+
+```typescript
+import { verifyCredentials, completeAuth } from 'remix/auth'
+import { redirect } from 'remix/response/redirect'
+
+async action(context) {
+ let user = await verifyCredentials(passwordProvider, context)
+
+ if (user == null) {
+ let session = context.get(Session)
+ session.flash('error', 'Invalid email or password.')
+ return redirect(routes.auth.login.href())
+ }
+
+ let session = completeAuth(context)
+ session.set('auth', { userId: user.id })
+
+ return redirect(routes.home.href())
+},
+```
+
+### Logout action
+
+```typescript
+import { Session } from 'remix/session'
+import { redirect } from 'remix/response/redirect'
+
+function logout(context) {
+ let session = context.get(Session)
+ session.unset('auth')
+ session.regenerateId(true)
+ return redirect(routes.home.href())
+}
+```
+
+## OAuth / External Auth
+
+### Create providers
+
+```typescript
+import {
+ createAtmosphereAuthProvider,
+ createGoogleAuthProvider,
+ createGitHubAuthProvider,
+ startExternalAuth,
+ finishExternalAuth,
+ completeAuth,
+ refreshExternalAuth,
+} from 'remix/auth'
+
+let googleProvider = createGoogleAuthProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ redirectUri: new URL(routes.auth.google.callback.href(), origin),
+})
+
+let githubProvider = createGitHubAuthProvider({
+ clientId: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ redirectUri: new URL(routes.auth.github.callback.href(), origin),
+})
+
+let atmosphereSessionSecret = process.env.ATMOSPHERE_SESSION_SECRET
+if (!atmosphereSessionSecret && process.env.NODE_ENV !== 'test') {
+ throw new Error('ATMOSPHERE_SESSION_SECRET is required')
+}
+
+let atmosphereProvider = createAtmosphereAuthProvider({
+ clientId: 'https://app.example.com/oauth/client-metadata.json',
+ redirectUri: new URL(routes.auth.atmosphere.callback.href(), origin),
+ sessionSecret: atmosphereSessionSecret ?? 'test-only-secret',
+})
+```
+
+For Atmosphere-compatible atproto OAuth, create the provider once, call
+`atmosphereProvider.prepare(handleOrDid)` before `startExternalAuth(...)`, then
+pass the same module-scope provider to `finishExternalAuth(...)` and
+`refreshExternalAuth(...)`.
+
+### OAuth controller
+
+```typescript
+export default {
+ actions: {
+ // GET /auth/google — redirect to Google
+ async index(context) {
+ return await startExternalAuth(googleProvider, context, {
+ returnTo: context.url.searchParams.get('returnTo'),
+ })
+ },
+
+ // GET /auth/google/callback — handle redirect back
+ async callback(context) {
+ let { result, returnTo } = await finishExternalAuth(
+ googleProvider,
+ context,
+ )
+
+ let db = context.get(Database)
+ let { user, authAccount } = await resolveExternalAuth(db, result)
+
+ let session = completeAuth(context)
+ session.set('auth', {
+ userId: user.id,
+ loginMethod: result.provider,
+ authAccountId: authAccount.id,
+ })
+
+ return redirect(returnTo ?? routes.account.href())
+ },
+ },
+} satisfies Controller
+```
+
+### Refresh stored provider tokens
+
+Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC
+tokens and needs a fresh access token from a refresh token. Built-in OIDC
+providers, X, and Atmosphere support refresh-token exchange. If the provider
+does not rotate the refresh token, the refreshed bundle preserves the current
+one.
+
+```typescript
+async function refreshGoogleTokens({ get }) {
+ let db = get(Database)
+ let account = await db.findOne(authAccounts, {
+ where: { provider: 'google' },
+ })
+ if (!account) return null
+
+ let refreshed = await refreshExternalAuth(googleProvider, account.tokens)
+ await db.update(authAccounts, account.id, { tokens: refreshed.tokens })
+
+ return refreshed.tokens
+}
+```
+
+## Protecting Routes
+
+### Controller-level protection
+
+Apply `requireAuth()` to an entire controller subtree:
+
+```typescript
+import { requireAuth } from 'remix/auth-middleware'
+
+export default {
+ middleware: [requireAuth()],
+ actions: {
+ index() {
+ /* guaranteed authenticated */
+ },
+ settings: settingsController,
+ },
+} satisfies Controller
+```
+
+### Stacking middleware
+
+Combine auth checks with role checks:
+
+```typescript
+export default {
+ middleware: [requireAuth(), requireAdmin()],
+ actions: {
+ index() {
+ /* requires auth + admin */
+ },
+ },
+} satisfies Controller
+```
+
+### Action-level protection
+
+Apply middleware to a single route:
+
+```typescript
+import { Auth, requireAuth } from 'remix/auth-middleware'
+
+router.get(routes.account, {
+ middleware: [requireAuth()],
+ handler(context) {
+ let auth = context.get(Auth)
+ return render()
+ },
+})
+```
+
+### Redirect on auth failure
+
+```typescript
+import { requireAuth } from 'remix/auth-middleware'
+import { redirect } from 'remix/response/redirect'
+
+export function requireAuthRedirect() {
+ return requireAuth({
+ onFailure(context) {
+ let returnTo = encodeURIComponent(context.url.pathname)
+ return redirect(routes.auth.login.href() + `?returnTo=${returnTo}`, 303)
+ },
+ })
+}
+```
diff --git a/.agents/skills/remix/references/component-model.md b/.agents/skills/remix/references/component-model.md
new file mode 100644
index 00000000..873b83e0
--- /dev/null
+++ b/.agents/skills/remix/references/component-model.md
@@ -0,0 +1,298 @@
+# Component Model
+
+## What This Covers
+
+How a Remix Component is shaped and how its state, lifecycle, and updates
+behave. Read this when the task involves:
+
+- Writing a component (`handle` plus render function)
+- Managing component-local state, derived values, or post-render DOM work
+- Using `handle.props`, `handle.update()`, `handle.queueTask()`,
+ `handle.signal`, `handle.id`, or `handle.context`
+- Listening to global events with cleanup tied to the component lifecycle
+
+For host-element behavior (event handlers, styles, refs, animations), see
+`mixins-styling-events.md`. For browser hydration, frames, and navigation, see
+`hydration-frames-navigation.md`.
+
+## Phases
+
+A component has two phases:
+
+1. **Setup phase** — runs once when the component is created
+2. **Render phase** — returned function runs on initial render and every update
+
+```tsx
+import { on, type Handle } from 'remix/ui'
+
+function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
+ let count = handle.props.initialCount ?? 0
+
+ return () => (
+
+ )
+}
+```
+
+## Props
+
+Components receive all JSX props through `handle.props`. The object identity is
+stable for the component lifetime, and its values are updated before each
+render. Put initialization inputs on normal JSX props and read them from
+`handle.props`:
+
+```tsx
+function Timer(handle: Handle<{ initialSeconds: number; paused?: boolean }>) {
+ let seconds = handle.props.initialSeconds
+
+ return () =>
Time remaining: {seconds}s
+}
+
+// Usage:
+```
+
+Because `handle.props` is stable, destructuring `let { props } = handle` is safe
+when helpers need to read current values later. Destructuring individual prop
+values is only a snapshot; prefer `handle.props.name` inside callbacks and
+render output when values can change.
+
+## State Rules
+
+- Keep state in setup scope as plain JavaScript variables.
+- Store only what affects rendering. Derive computed values in render.
+- Do not mirror input state unless you truly need controlled behavior.
+- Do work in event handlers, not in render. Use the handler scope for transient
+ state.
+
+```tsx
+// Derive computed values in render
+function TodoList(handle: Handle) {
+ let todos: Array<{ text: string; completed: boolean }> = []
+
+ return () => {
+ let completedCount = todos.filter((t) => t.completed).length
+ return
Completed: {completedCount}
+ }
+}
+```
+
+## Handle API
+
+### `handle.update()`
+
+Schedules a rerender. Returns a promise that resolves with an `AbortSignal`
+after the update completes. Await it when you need the updated DOM before
+follow-up work:
+
+```tsx
+on('click', async () => {
+ isPlaying = true
+ let signal = await handle.update()
+ // DOM is now updated, safe to focus or measure
+ stopButton.focus()
+})
+```
+
+### `handle.queueTask(task)`
+
+Schedules a task to run after the next update. The task receives an
+`AbortSignal` that aborts when the component re-renders or is removed. Use for
+post-render DOM work, reactive data loading, or hydration-sensitive setup:
+
+```tsx
+let data = null
+let requestedUrl: string | null = null
+
+// Post-render DOM work in an event handler
+on('click', () => {
+ showDetails = true
+ handle.update()
+ handle.queueTask(() => {
+ detailsSection.scrollIntoView({ behavior: 'smooth' })
+ })
+})
+
+// Reactive data loading keyed by props.url
+return () => {
+ if (requestedUrl !== handle.props.url) {
+ let nextUrl = handle.props.url
+ requestedUrl = nextUrl
+ data = null
+
+ handle.queueTask(async (signal) => {
+ let response = await fetch(nextUrl, { signal })
+ let json = await response.json()
+ if (signal.aborted || requestedUrl !== nextUrl) return
+ data = json
+ handle.update()
+ })
+ }
+
+ return
{data ?? 'Loading...'}
+}
+```
+
+Avoid creating intermediate state just to trigger `queueTask`. Do the work
+directly in the handler or the queued task.
+
+### `handle.signal`
+
+An `AbortSignal` aborted when the component disconnects. Use for cleanup:
+
+```tsx
+function Clock(handle: Handle) {
+ let interval = setInterval(handle.update, 1000)
+ handle.signal.addEventListener('abort', () => clearInterval(interval))
+
+ return () => {new Date().toString()}
+}
+```
+
+### `handle.id`
+
+Stable identifier per component instance. Useful for `htmlFor`, `aria-owns`,
+etc.:
+
+```tsx
+function LabeledInput(handle: Handle) {
+ return () => (
+
+
+
+
+ )
+}
+```
+
+### `handle.frame` and `handle.frames`
+
+Frame-aware behavior for client entries rendered inside frames:
+
+- `handle.frame.reload()` — reload the containing frame
+- `handle.frame.src` — the URL of the containing frame
+- `handle.frames.top` — the root frame (the whole page)
+- `handle.frames.top.reload()` — reload the entire page/frame tree
+- `handle.frames.get(name)` — look up a named frame; returns
+ `FrameHandle | undefined`
+
+```tsx
+function RefreshButton(handle: Handle) {
+ return () => (
+
+ )
+}
+```
+
+### `handle.context`
+
+Context for ancestor/descendant communication. See the context section below.
+
+## Context
+
+Use `handle.context.set()` to provide values and `handle.context.get(Provider)`
+to consume them. `set()` does **not** trigger updates — call `handle.update()`
+if the tree needs to rerender.
+
+```tsx
+function ThemeProvider(
+ handle: Handle<{ children?: RemixNode }, { theme: 'light' | 'dark' }>,
+) {
+ let theme: 'light' | 'dark' = 'light'
+ handle.context.set({ theme })
+
+ return () => (
+
+}
+```
+
+## Global Events
+
+Use `addEventListeners(target, handle.signal, listeners)` to listen to global
+targets with automatic cleanup when the component disconnects:
+
+```tsx
+import { addEventListeners, type Handle } from 'remix/ui'
+
+function ResizeTracker(handle: Handle) {
+ let width = window.innerWidth
+
+ addEventListeners(window, handle.signal, {
+ resize() {
+ width = window.innerWidth
+ handle.update()
+ },
+ })
+
+ return () =>
{width}
+}
+```
diff --git a/.agents/skills/remix/references/create-mixins.md b/.agents/skills/remix/references/create-mixins.md
new file mode 100644
index 00000000..e0b7d6f4
--- /dev/null
+++ b/.agents/skills/remix/references/create-mixins.md
@@ -0,0 +1,161 @@
+# Creating Mixins
+
+## What This Covers
+
+How to author your own reusable host-element behavior with `createMixin`. Read
+this when the task involves:
+
+- Combining multiple low-level events or DOM hooks into one semantic mixin
+- Dispatching custom DOM events from a host node
+- Encapsulating imperative DOM setup that several components share
+- Typing custom events on `HTMLElementEventMap` for use with `on(...)`
+
+For the built-in mixins most code should use, see `mixins-styling-events.md`.
+
+Use `createMixin` from `remix/ui` to author reusable host-element behavior.
+
+Most app code should use built-in core mixins (`on`, `css`, `ref`, `link`,
+`attrs`) and animation mixins from `remix/ui/animation`. Create custom mixins
+when combining multiple low-level events into one semantic event, or when the
+pattern is reused across components.
+
+## Core Semantics
+
+1. A mixin handle is tied to one mounted host node lifecycle.
+2. `insert` is the host-node availability point for imperative setup.
+3. `remove` is teardown for that same lifecycle.
+4. `queueTask` runs post-commit and receives `(node, signal)` for mixins.
+5. Mixin render functions should stay pure; side effects belong in `insert`,
+ `remove`, or queued work.
+
+```tsx
+import { createMixin } from 'remix/ui'
+
+let myMixin = createMixin((handle) => {
+ handle.addEventListener('insert', (event) => {
+ // event.node is the mounted host node
+ })
+
+ handle.addEventListener('remove', () => {
+ // Clean up listeners, timers, observers
+ })
+
+ return (props) => {
+ handle.queueTask((node) => {
+ // Post-commit work that needs the concrete host node
+ })
+ return
+ }
+})
+```
+
+## Patterns
+
+### Pure prop transform
+
+```tsx
+let withTitle = createMixin(
+ (handle) => (title: string, props: { title?: string }) => (
+
+ ),
+)
+```
+
+### Lifecycle-managed imperative setup
+
+```tsx
+let withFocus = createMixin((handle) => {
+ handle.addEventListener('insert', (event) => {
+ event.node.focus()
+ })
+ return (props) =>
+})
+```
+
+## Custom Event Mixins
+
+Create event mixins when you combine multiple low-level events into one semantic
+custom event that is reused across components.
+
+1. Namespace custom event names (`myapp:*`) to avoid collisions.
+2. Extend `Event` with the data consumers need.
+3. Declare the event on `HTMLElementEventMap` for type safety with `on(...)`.
+4. Dispatch from the host node inside the mixin.
+
+```tsx
+import { createMixin, on } from 'remix/ui'
+
+export let dragReleaseType = 'myapp:drag-release' as const
+
+declare global {
+ interface HTMLElementEventMap {
+ [dragReleaseType]: DragReleaseEvent
+ }
+}
+
+export class DragReleaseEvent extends Event {
+ velocityX: number
+ velocityY: number
+ constructor(init: { velocityX: number; velocityY: number }) {
+ super(dragReleaseType, { bubbles: true, cancelable: true })
+ this.velocityX = init.velocityX
+ this.velocityY = init.velocityY
+ }
+}
+
+export let dragRelease = createMixin((handle) => {
+ let node: HTMLElement | undefined
+ let tracking = false
+ let velocityX = 0
+ let velocityY = 0
+ let lastX = 0
+ let lastY = 0
+ let lastT = 0
+
+ handle.addEventListener('insert', (event) => {
+ node = event.node
+ })
+
+ return () => (
+ {
+ if (!event.isPrimary) return
+ tracking = true
+ lastX = event.clientX
+ lastY = event.clientY
+ lastT = event.timeStamp
+ node?.setPointerCapture(event.pointerId)
+ }),
+ on('pointermove', (event) => {
+ if (!tracking) return
+ let dt = Math.max(1, event.timeStamp - lastT)
+ velocityX = (event.clientX - lastX) / dt
+ velocityY = (event.clientY - lastY) / dt
+ lastX = event.clientX
+ lastY = event.clientY
+ lastT = event.timeStamp
+ }),
+ on('pointerup', () => {
+ if (!tracking) return
+ tracking = false
+ node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))
+ }),
+ ]}
+ />
+ )
+})
+```
+
+Consume it:
+
+```tsx
+
{
+ console.log('velocity:', event.velocityX, event.velocityY)
+ }),
+ ]}
+/>
+```
diff --git a/.agents/skills/remix/references/data-and-validation.md b/.agents/skills/remix/references/data-and-validation.md
new file mode 100644
index 00000000..2cdb11a5
--- /dev/null
+++ b/.agents/skills/remix/references/data-and-validation.md
@@ -0,0 +1,393 @@
+# Data Access and Validation
+
+## What This Covers
+
+How input becomes a value the app trusts, and how that value reaches storage.
+Read this when the task involves:
+
+- Defining database tables, columns, relations, and migrations
+- Querying or mutating persisted data with `Database`
+- Parsing and validating user input from forms, query strings, or external
+ payloads
+- Choosing between schema-level checks, table validation hooks, and
+ migration-level constraints
+
+For where validation runs in the request lifecycle, see
+`routing-and-controllers.md`. For session or identity-bound writes, see
+`auth-and-sessions.md`.
+
+## Table Definitions (`remix/data-table`)
+
+Define tables with typed columns, relations, and optional validation hooks:
+
+```typescript
+import { belongsTo, column as c, hasMany, table } from 'remix/data-table'
+import type { TableRow, TableRowWith } from 'remix/data-table'
+
+export const books = table({
+ name: 'books',
+ columns: {
+ id: c.integer().primaryKey().autoIncrement(),
+ slug: c.text().notNull().unique(),
+ title: c.text().notNull(),
+ author: c.text().notNull(),
+ price: c.decimal(10, 2).notNull(),
+ genre: c.text().notNull(),
+ in_stock: c.boolean(),
+ },
+})
+
+export const orders = table({
+ name: 'orders',
+ columns: {
+ id: c.integer().primaryKey().autoIncrement(),
+ user_id: c.integer().notNull().references('users', 'id'),
+ total: c.decimal(10, 2).notNull(),
+ created_at: c.integer().notNull(),
+ },
+ relations: {
+ user: belongsTo('users', 'user_id'),
+ items: hasMany('order_items', 'order_id'),
+ },
+})
+
+export type Book = TableRow
+export type Order = TableRow
+export type OrderWithItems = TableRowWith
+```
+
+### Column types
+
+| Method | SQL type |
+| ----------------------------- | ------------------ |
+| `c.integer()` | INTEGER |
+| `c.text()` | TEXT |
+| `c.boolean()` | BOOLEAN |
+| `c.decimal(precision, scale)` | DECIMAL |
+| `c.enum([...])` | TEXT (string enum) |
+| `c.uuid()` | UUID / TEXT |
+| `c.varchar(length)` | VARCHAR |
+
+Column modifiers: `.primaryKey()`, `.autoIncrement()`, `.notNull()`,
+`.unique()`, `.references(table, column, fkName?)`, `.onDelete(action)`,
+`.default(value)`.
+
+Composite primary keys go on the table option, not the column:
+`primaryKey: ['order_id', 'book_id']`.
+
+### Schema vs migrations
+
+Column modifiers describe SQL constraints — the source of truth for them is your
+**migration** files, where they generate the actual DDL. Runtime `table(...)`
+definitions in `app/data/schema.ts` can use the same modifiers, or they can stay
+minimal (`c.integer()`, `c.text()`, ...) since the runtime only needs the column
+shape and validation hooks. Two valid patterns:
+
+- **Modifiers in both** — schema and migrations stay in sync visually; useful
+ when you want schema-level docs.
+- **Bare columns in schema, full modifiers in migrations** — schema describes
+ what the app reads and writes; migrations own the DDL and constraints.
+
+Pick one and apply it consistently across the app.
+
+### Table validation hooks
+
+Tables can define `validate`, `beforeWrite`, and `afterRead` hooks:
+
+```typescript
+export const books = table({
+ name: 'books',
+ columns: {
+ /* ... */
+ },
+ validate({ operation, value }) {
+ let issues = []
+ if (operation === 'create' && !value.slug) {
+ issues.push({ message: 'Slug is required.', path: ['slug'] })
+ }
+ return issues.length > 0 ? { issues } : { value }
+ },
+})
+```
+
+## Database Setup
+
+Create a database with an adapter and expose it via middleware:
+
+```typescript
+import BetterSqlite3 from 'better-sqlite3'
+import { createDatabase, Database } from 'remix/data-table'
+import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'
+
+let sqlite = new BetterSqlite3('./db/app.db')
+sqlite.pragma('foreign_keys = ON')
+let adapter = createSqliteDatabaseAdapter(sqlite)
+export let db = createDatabase(adapter)
+```
+
+`createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared
+`prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`,
+and compatible clients. Use whichever client fits the runtime instead of
+assuming `better-sqlite3` is required.
+
+### Database middleware
+
+```typescript
+import type { Middleware } from 'remix/fetch-router'
+import { Database } from 'remix/data-table'
+
+export function loadDatabase(): Middleware {
+ return async (context, next) => {
+ context.set(Database, db)
+ return next()
+ }
+}
+```
+
+### Querying
+
+```typescript
+let db = get(Database)
+
+// Find by primary key
+let book = await db.find(books, id)
+
+// Find one by condition
+let user = await db.findOne(users, { where: { email } })
+
+// Find many with ordering
+let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
+
+// Count
+let total = await db.count(orders, { where: { user_id: userId } })
+
+// Query builder
+let genres = await db
+ .query(books)
+ .select('genre')
+ .distinct()
+ .orderBy('genre', 'asc')
+ .all()
+
+// Create
+let newBook = await db.create(books, {
+ slug: 'new-book',
+ title: 'New Book' /* ... */,
+})
+
+// Update
+await db.update(books, bookId, { title: 'Updated Title' })
+
+// Delete
+await db.delete(books, bookId)
+```
+
+### Operators
+
+```typescript
+import { inList } from 'remix/data-table/operators'
+
+let featured = await db.findMany(books, {
+ where: inList('slug', ['book-a', 'book-b', 'book-c']),
+})
+```
+
+## Migrations
+
+### Writing migrations
+
+```typescript
+import { column as c, createMigration } from 'remix/data-table/migrations'
+import { table } from 'remix/data-table'
+
+export default createMigration({
+ async up({ schema }) {
+ let users = table({
+ name: 'users',
+ columns: {
+ id: c.integer().primaryKey().autoIncrement(),
+ email: c.text().notNull().unique(),
+ name: c.text().notNull(),
+ },
+ })
+ await schema.createTable(users)
+ await schema.createIndex(users, 'email', {
+ name: 'users_email_idx',
+ unique: true,
+ })
+ },
+
+ async down({ schema }) {
+ await schema.dropTable('users')
+ },
+})
+```
+
+Migrations can also import table definitions from the app schema to avoid
+duplication:
+
+```typescript
+import { createMigration } from 'remix/data-table/migrations'
+import { users, authAccounts } from '../../app/data/schema.ts'
+
+export default createMigration({
+ async up({ schema }) {
+ await schema.createTable(users)
+ await schema.createTable(authAccounts)
+ },
+})
+```
+
+### Running migrations
+
+```typescript
+import { createMigrationRunner } from 'remix/data-table/migrations'
+import { loadMigrations } from 'remix/data-table/migrations/node'
+
+let migrations = await loadMigrations('./db/migrations')
+let runner = createMigrationRunner(adapter, migrations)
+await runner.up()
+```
+
+### Migration file naming
+
+Name migration files with a timestamp prefix: `20260228090000_create_users.ts`.
+Place them in `db/migrations/`.
+
+## Input Validation (`remix/data-schema`)
+
+Use `data-schema` to validate user input (forms, query params, API payloads).
+This is separate from table-level `validate` hooks which run at persistence.
+
+### Schema builders
+
+```typescript
+import * as s from 'remix/data-schema'
+import { email, minLength, maxLength } from 'remix/data-schema/checks'
+
+let userSchema = s.object({
+ name: s.string().pipe(minLength(1)),
+ email: s.string().pipe(email()),
+ age: s.optional(s.number()),
+})
+
+let result = s.parse(userSchema, data)
+```
+
+### FormData validation
+
+Use `remix/data-schema/form-data` to validate `FormData` directly:
+
+```typescript
+import * as s from 'remix/data-schema'
+import * as f from 'remix/data-schema/form-data'
+import { email, minLength } from 'remix/data-schema/checks'
+
+let signupSchema = f.object({
+ name: f.field(s.string().pipe(minLength(1))),
+ email: f.field(s.string().pipe(email())),
+ password: f.field(s.string().pipe(minLength(8))),
+})
+
+// In a controller action:
+let formData = get(FormData)
+let { name, email, password } = s.parse(signupSchema, formData)
+```
+
+### Reading FormData: middleware vs `request.formData()`
+
+There are two ways to get a `FormData` value inside an action.
+
+The recommended way: register `formData()` middleware in the root stack and read
+with `get(FormData)`. The body is parsed once per request, and the typed
+`FormData` value flows through the context system. This also lets
+`methodOverride()` and CSRF middleware work uniformly.
+
+```typescript
+import { formData } from 'remix/form-data-middleware'
+
+let router = createRouter({
+ middleware: [, /* ... */ formData() /* ... */],
+})
+
+// In an action:
+let parsed = s.parseSafe(signupSchema, get(FormData))
+```
+
+The fallback: `await request.formData()` directly. This works without middleware
+and is fine for small one-off cases, but it bypasses the context system, runs
+once per call site, and doesn't compose with middleware that depends on parsed
+form fields.
+
+### Safe parsing
+
+`s.parse` throws on invalid input. `s.parseSafe` returns a tagged result and is
+usually what an action wants, since validation failure is an expected outcome
+(re-render the form with errors) rather than an exception:
+
+```typescript
+let result = s.parseSafe(signupSchema, get(FormData))
+if (!result.success) {
+ return render(, { status: 400 })
+}
+let { name, email, password } = result.value
+```
+
+Returning a `Response` for validation failures keeps the route contract honest:
+the same action returns 200 on success, 400 with errors on bad input, no
+out-of-band exception flow.
+
+### Transforming validated output
+
+Use `.transform(...)` when a schema should validate one shape but return another
+value or output type. Transforms run after validation and compose with
+`.pipe(...)` and `.refine(...)`:
+
+```typescript
+import * as coerce from 'remix/data-schema/coerce'
+
+let slugSchema = s
+ .string()
+ .pipe(minLength(1))
+ .transform((value) => value.trim().toLowerCase().replace(/\s+/g, '-'))
+
+let pageSchema = f.object({
+ page: f.field(s.defaulted(coerce.coerceNumber(), 1).refine(Number.isInteger)),
+ q: f.field(s.defaulted(s.string(), '').transform((value) => value.trim())),
+})
+
+let { page, q } = s.parse(pageSchema, formData)
+```
+
+### Anti-patterns
+
+Avoid these shapes when reading and validating input:
+
+- **Raw `formData.get('name')` plus an `if (typeof name !== 'string')` guard**,
+ then a thrown custom error. This reinvents what `data-schema` already does,
+ loses the typed result, and pushes error translation into a `try/catch`
+ instead of a return value.
+- **Letting route-local domain errors leak out of the action.** Translate
+ expected outcomes (bad input, missing record, duplicate entry) into the
+ `Response` the route means to return instead of throwing a custom `Error`
+ subclass with a `status` field and catching it later.
+- **Trusting `params`, query strings, or external payloads without a schema.**
+ Anything that crosses a trust boundary should be parsed before it reaches
+ business logic.
+
+### Common patterns
+
+```typescript
+// Optional with default
+let limitSchema = f.field(s.defaulted(s.string(), '10'))
+
+// Union types
+let methodSchema = s.union([
+ s.literal('credentials'),
+ s.literal('google'),
+ s.literal('github'),
+])
+
+// Refinements
+let idSchema = s.number().refine(Number.isInteger, 'Expected an integer')
+```
diff --git a/.agents/skills/remix/references/hydration-frames-navigation.md b/.agents/skills/remix/references/hydration-frames-navigation.md
new file mode 100644
index 00000000..a2ffcb37
--- /dev/null
+++ b/.agents/skills/remix/references/hydration-frames-navigation.md
@@ -0,0 +1,311 @@
+# Hydration, Frames, and Navigation
+
+## What This Covers
+
+How server-rendered UI becomes interactive in the browser, and how the page
+updates without a full navigation. Read this when the task involves:
+
+- Marking a component for client-side hydration with `clientEntry`
+- Booting the client runtime with `run`
+- Streaming server content into a region of the page with `` and
+ reloading those regions
+- Triggering Navigation API transitions with `navigate(...)` or `link(...)`
+- Server rendering with `renderToStream` or `renderToString`
+- Managing the document ``
+
+For component-local state and updates, see `component-model.md`. For
+host-element behavior and events, see `mixins-styling-events.md`.
+
+## Server First, Then Hydrate
+
+Make the server route correct before adding `clientEntry(...)`. A POST should
+already do the right thing on its own — return HTML, a redirect, or an error
+response — and a GET should already render the page the user expects.
+`clientEntry` exists to layer interactivity on top of UI that already works
+without it.
+
+When server state changes after a mutation, prefer reloading a `` when
+the UI region already maps cleanly to a server-rendered route. Frames re-fetch
+the same route, so the rendering logic stays in one place and the client does
+not need a parallel "state" API.
+
+```tsx
+on('submit', async (event, signal) => {
+ event.preventDefault()
+ await fetch(routes.cart.add.href(), {
+ method: 'POST',
+ body: new FormData(event.currentTarget),
+ signal,
+ })
+ if (signal.aborted) return
+ await handle.frames.get('cart-summary')?.reload()
+})
+```
+
+Use polling or a small JSON state endpoint when the data changes outside this
+page, or when a tiny shared widget would be heavier to model as a frame. Pick
+the lightest sync mechanism that preserves clear ownership of rendering logic.
+
+## Client Entries
+
+Use `clientEntry` to mark a component for client-side hydration. In
+source-served apps, prefer the source module's `import.meta.url` as the entry ID
+and let server rendering map it to the public asset URL:
+
+```tsx
+import { clientEntry, on, type Handle } from 'remix/ui'
+
+export const Counter = clientEntry(
+ import.meta.url,
+ function Counter(handle: Handle<{ initialCount: number; label: string }>) {
+ let count = handle.props.initialCount
+
+ return () => (
+
+
+ {handle.props.label}: {count}
+
+
+
+ )
+ },
+)
+```
+
+On the server, provide `resolveClientEntry` to `renderToStream(...)` so source
+file URLs become browser-loadable asset URLs. Keep this resolution in the render
+helper so component modules do not hard-code deployment-specific asset paths:
+
+```tsx
+let stream = renderToStream(, {
+ async resolveClientEntry(entryId, component) {
+ let exportName = entryId.split('#')[1] || component.name
+ if (!exportName) {
+ throw new Error(`Unable to resolve client entry export for ${entryId}`)
+ }
+
+ return {
+ href: await assetServer.getHref(entryId),
+ exportName,
+ }
+ },
+})
+```
+
+If the module export name differs from the component function name, include
+`#ExportName` in the entry ID or return the exact export name from
+`resolveClientEntry`. A render helper that only supports source-owned entries
+can also fail fast when `entryId` is not a `file://` URL.
+
+On the server, `clientEntry` components render like any other component. The
+server wraps their output in comment markers and serializes props into a
+`'
-let response = createHtmlResponse(html`
${unsafe}
`, { status: 400 })
-```
-
-The `html.raw` template tag can be used to interpolate values without escaping
-them. This has the same semantics as `String.raw` but for HTML snippets that
-have already been escaped or are from trusted sources:
-
-```ts
-// Use html.raw as a template tag to skip escaping interpolations
-let safeHtml = 'Bold'
-let content = html.raw`
${safeHtml}
`
-let response = createHtmlResponse(content)
-
-// This is particularly useful when building HTML from multiple safe fragments
-let header = 'Title'
-let body = 'Content'
-let footer = ''
-let page = html.raw`
-
-
-
- ${header}
- ${body}
- ${footer}
-
-
-`
-
-// You can nest html.raw inside html to preserve SafeHtml fragments
-let icon = html.raw``
-let button = html`` // icon is not escaped
-```
-
-**Warning**: Only use `html.raw` with trusted content. Unlike the regular `html`
-template tag, `html.raw` does not escape its interpolations, which can lead to
-XSS vulnerabilities if used with untrusted user input.
-
-See the
-[`@remix-run/html-template` documentation](https://github.com/remix-run/remix/tree/main/packages/html-template#readme)
-for more details.
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Middleware and request context](./middleware.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/index.md b/docs/agents/remix/fetch-router/index.md
deleted file mode 100644
index 0b6f0e79..00000000
--- a/docs/agents/remix/fetch-router/index.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# fetch-router
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Overview
-
-A minimal, composable router built on the
-[web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and
-[`route-pattern`](../route-pattern). Ideal for building APIs, web services, and
-server-rendered applications across any JavaScript runtime.
-
-## Features
-
-- **Fetch API**: Built on standard web APIs that work everywhere - Node.js, Bun,
- Deno, Cloudflare Workers, and browsers
-- **Type-Safe Routing**: Leverage TypeScript for compile-time route validation
- and parameter inference
-- **Composable Architecture**: Nest routers, combine middleware, and organize
- routes hierarchically
-- **Declarative Route Maps**: Define your entire route structure upfront with
- type-safe route names and request methods
-- **Flexible Middleware**: Apply middleware globally, per-route, or to entire
- route hierarchies
-- **Easy Testing**: Use standard `fetch()` to test your routes - no special test
- harness required
-
-## Goals
-
-- **Simplicity**: A router should be simple to understand and use. The entire
- API surface fits in your head.
-- **Composability**: Small routers combine to build large applications.
- Middleware and nested routers make organization natural.
-- **Standards-Based**: Built on web standards that work across runtimes. No
- proprietary APIs or Node.js-specific code.
-
-## Installation
-
-```sh
-npm i remix
-```
-
-Import route definition helpers from `remix/fetch-router/routes`, and runtime
-APIs from `remix/fetch-router`.
-
-## Navigation
-
-- [Basic usage and route maps](./usage.md)
-- [Routing based on request method](./routing-methods.md)
-- [Resource-based routes](./routing-resources.md)
-- [Middleware and request context](./middleware.md)
-- [Additional topics and HTML helpers](./advanced-topics.md)
-- [Testing and related work](./testing-and-related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/middleware.md b/docs/agents/remix/fetch-router/middleware.md
deleted file mode 100644
index 14b0110d..00000000
--- a/docs/agents/remix/fetch-router/middleware.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# Middleware and request context
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Controllers and middleware
-
-Middleware functions run code before and/or after actions. They are a powerful
-way to add functionality to your app.
-
-A basic logging middleware might look like this:
-
-```ts
-import type { Middleware } from 'remix/fetch-router'
-
-// You can use the `Middleware` type to type middleware functions.
-function logger(): Middleware {
- return async (context, next) => {
- let start = new Date()
-
- // Call next() to invoke the next middleware or action in the chain.
- let response = await next()
-
- let end = new Date()
- let duration = end.getTime() - start.getTime()
-
- console.log(
- `${context.request.method} ${context.request.url} ${response.status} ${duration}ms`,
- )
-
- return response
- }
-}
-
-// Use it like this:
-let router = createRouter({
- middleware: [logger()],
-})
-```
-
-Middleware is typically built as a function that returns a middleware function.
-This allows you to pass options to the middleware function if needed. For
-example, the `auth()` middleware below allows you to pass a `token` option that
-is used to authenticate the request.
-
-```tsx
-interface AuthOptions {
- token: string
-}
-
-function auth(options?: AuthOptions): Middleware {
- let token = options?.token ?? 'secret'
-
- return (context, next) => {
- if (context.headers.get('Authorization') !== `Bearer ${token}`) {
- return new Response('Unauthorized', { status: 401 })
- }
- return next()
- }
-}
-```
-
-Middleware may be used in two different contexts: globally (at the router level)
-or inline (at the route level).
-
-Global middleware is added to the router when it is created using the
-`createRouter({ middleware })` option. This middleware runs before any routes
-are matched and is useful for doing things like logging, serving static files,
-profiling, and a variety of other things. Global middleware runs on every
-request, so it's important to keep them lightweight and fast.
-
-Inline (or "route") middleware is added to the router when actions are
-registered using either `router.map()` or one of the method-specific helpers
-like `router.get()`, `router.post()`, `router.put()`, `router.delete()`, etc.
-Route middleware runs after global middleware but before the route action, and
-is useful for doing things like authentication, authorization, and data
-validation.
-
-```tsx
-let routes = route({
- home: '/',
- admin: {
- dashboard: '/admin/dashboard',
- },
-})
-
-let router = createRouter({
- // This middleware runs on all requests.
- middleware: [staticFiles('./public')],
-})
-
-router.map(routes.home, () => new Response('Home'))
-
-router.map(routes.admin.dashboard, {
- // This middleware runs only on the `/admin/dashboard` route.
- middleware: [auth({ token: 'secret' })],
- action() {
- return new Response('Dashboard')
- },
-})
-```
-
-## Request context
-
-Every action and middleware receives a `context` object with useful properties:
-
-```ts
-router.get('/posts/:id', ({ request, url, params, storage }) => {
- // request: The original Request object
- console.log(request.method) // "GET"
- console.log(request.headers.get('Accept'))
-
- // url: Parsed URL object
- console.log(url.pathname) // "/posts/123"
- console.log(url.searchParams.get('sort'))
-
- // params: Route parameters (fully typed!)
- console.log(params.id) // "123"
-
- // storage: AppStorage for type-safe access to request-scoped data
- storage.set('user', currentUser)
-
- return new Response(`Post ${params.id}`)
-})
-```
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Routing based on request method](./routing-methods.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/routing-methods.md b/docs/agents/remix/fetch-router/routing-methods.md
deleted file mode 100644
index 62e183d3..00000000
--- a/docs/agents/remix/fetch-router/routing-methods.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Routing based on request method
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Routing based on request method
-
-In the example above, both the `home` and `contact` routes are able to be
-registered for any incoming
-[`request.method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method).
-If you inspect their types, you'll see:
-
-```tsx
-type HomeRoute = typeof routes.home // Route<'ANY', '/'>
-type ContactRoute = typeof routes.contact // Route<'ANY', '/contact'>
-```
-
-We used `router.get()` and `router.post()` to register actions on each route
-specifically for the `GET` and `POST` request methods.
-
-However, we can also encode the request method into the route definition itself
-using the `method` property on the route. When you include the `method` in the
-route definition, `router.map()` will register the action only for that specific
-request method. This can be more convenient than using `router.get()` and
-`router.post()` to register actions one at a time.
-
-```ts
-import * as assert from 'node:assert/strict'
-import { createRouter } from 'remix/fetch-router'
-import { route } from 'remix/fetch-router/routes'
-
-let routes = route({
- home: { method: 'GET', pattern: '/' },
- contact: {
- index: { method: 'GET', pattern: '/contact' },
- action: { method: 'POST', pattern: '/contact' },
- },
-})
-
-type Routes = typeof routes
-// Each route is now typed with a specific request method.
-// {
-// home: Route<'GET', '/'>,
-// contact: {
-// index: Route<'GET', '/contact'>,
-// action: Route<'POST', '/contact'>,
-// },
-// }
-
-let router = createRouter()
-
-router.map(routes, {
- home({ method }) {
- assert.equal(method, 'GET')
- return new Response('Home')
- },
- contact: {
- index({ method }) {
- assert.equal(method, 'GET')
- return new Response('Contact')
- },
- action({ method }) {
- assert.equal(method, 'POST')
- return new Response('Contact Action')
- },
- },
-})
-```
-
-## Declaring routes
-
-In additon to the `{ method, pattern }` syntax shown above, the router provides
-a few shorthand methods that help to eliminate some of the boilerplate when
-building complex route maps:
-
-- [`form`](#declaring-form-routes) - creates a route map with an `index` (`GET`)
- and `action` (`POST`) route. This is well-suited to showing a standard HTML
- `
-
-
- `)
- },
- // POST /contact - handles the form submission
- action({ formData }) {
- let message = formData.get('message') as string
- let body = html`
-
-
-
-
-
- `
-
- return createHtmlResponse(body)
- },
- },
-})
-```
-
-## Navigation
-
-- [Resource-based routes](./routing-resources.md)
-- [Basic usage and route maps](./usage.md)
-- [fetch-router overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/routing-resources.md b/docs/agents/remix/fetch-router/routing-resources.md
deleted file mode 100644
index 150b7e52..00000000
--- a/docs/agents/remix/fetch-router/routing-resources.md
+++ /dev/null
@@ -1,186 +0,0 @@
-# Resource-based routes
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Resource-based routes
-
-The router provides a `resources()` helper that creates a route map with a set
-of resource-based routes, useful when defining RESTful API routes or modeling
-resources in a web application (similar to Rails' `resources` helper).
-
-```ts
-import { createRouter } from 'remix/fetch-router'
-import { resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- brands: {
- ...resources('brands', { only: ['index', 'show'] }),
- products: resources('brands/:brandId/products', {
- only: ['index', 'show'],
- }),
- },
-})
-
-type Routes = typeof routes
-// {
-// brands: {
-// index: Route<'GET', '/brands'>
-// show: Route<'GET', '/brands/:id'>
-// products: {
-// index: Route<'GET', '/brands/:brandId/products'>
-// show: Route<'GET', '/brands/:brandId/products/:id'>
-// },
-// },
-// }
-
-let router = createRouter()
-
-router.map(routes.brands, {
- // GET /brands
- index() {
- return new Response('Brands Index')
- },
- // GET /brands/:id
- show({ params }) {
- return new Response(`Brand ${params.id}`)
- },
- products: {
- // GET /brands/:brandId/products
- index() {
- return new Response('Products Index')
- },
- // GET /brands/:brandId/products/:id
- show({ params }) {
- return new Response(`Brand ${params.brandId}, Product ${params.id}`)
- },
- },
-})
-```
-
-The `resource()` helper creates a route map for a single resource (not something
-that is part of a collection). This is useful when defining operations on a
-singleton resource, like a user profile.
-
-```tsx
-import { createRouter } from 'remix/fetch-router'
-import { resource, resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- user: {
- ...resources('users', { only: ['index', 'show'] }),
- profile: resource('users/:userId/profile', {
- only: ['show', 'edit', 'update'],
- }),
- },
-})
-
-type Routes = typeof routes
-// {
-// user: {
-// index: Route<'GET', '/users'>
-// show: Route<'GET', '/users/:id'>
-// profile: {
-// show: Route<'GET', '/users/:userId/profile'>
-// edit: Route<'GET', '/users/:userId/profile/edit'>
-// update: Route<'PUT', '/users/:userId/profile'>
-// },
-// },
-// }
-```
-
-Without the `only` option, a `resources('users')` route map contains 7 routes:
-`index`, `new`, `show`, `create`, `edit`, `update`, and `destroy`.
-
-```tsx
-let routes = resources('users')
-type Routes = typeof routes
-// {
-// index: Route<'GET', '/users'> - Lists all users
-// new: Route<'GET', '/users/new'> - Shows a form to create a new user
-// show: Route<'GET', '/users/:id'> - Shows a single user
-// create: Route<'POST', '/users'> - Creates a new user
-// edit: Route<'GET', '/users/:id/edit'> - Shows a form to edit a user
-// update: Route<'PUT', '/users/:id'> - Updates a user
-// destroy: Route<'DELETE', '/users/:id'> - Deletes a user
-// }
-```
-
-Similarly, a `resource('profile')` route map contains 6 routes: `new`, `show`,
-`create`, `edit`, `update`, and `destroy`. There is no `index` route because a
-`resource()` represents a singleton resource, not a collection, so there is no
-collection view.
-
-```tsx
-let routes = resource('profile')
-type Routes = typeof routes
-// {
-// new: Route<'GET', '/profile/new'> - Shows a form to create the profile
-// show: Route<'GET', '/profile'> - Shows the profile
-// create: Route<'POST', '/profile'> - Creates the profile
-// edit: Route<'GET', '/profile/edit'> - Shows a form to edit the profile
-// update: Route<'PUT', '/profile'> - Updates the profile
-// destroy: Route<'DELETE', '/profile'> - Deletes the profile
-// }
-```
-
-Resource route names may be customized using the `names` option when you'd
-prefer not to use the default
-`index`/`new`/`show`/`create`/`edit`/`update`/`destroy` route names.
-
-```tsx
-import { createRouter } from 'remix/fetch-router'
-import { resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- users: resources('users', {
- only: ['index', 'show'],
- names: { index: 'list', show: 'view' },
- }),
-})
-type Routes = typeof routes.users
-// {
-// list: Route<'GET', '/users'> - Lists all users
-// view: Route<'GET', '/users/:id'> - Shows a single user
-// }
-```
-
-If you want to use a param name other than `id`, you can use the `param` option.
-
-```tsx
-import { createRouter } from 'remix/fetch-router'
-import { resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- users: resources('users', {
- only: ['index', 'show', 'edit', 'update'],
- param: 'userId',
- }),
-})
-type Routes = typeof routes.users
-// {
-// index: Route<'GET', '/users'> - Lists all users
-// show: Route<'GET', '/users/:userId'> - Shows a single user
-// edit: Route<'GET', '/users/:userId/edit'> - Shows a form to edit a user
-// update: Route<'PUT', '/users/:userId'> - Updates a user
-// }
-```
-
-You can use the `exclude` option to exclude routes from being generated.
-
-```tsx
-let routes = resources('users', { exclude: ['edit', 'update', 'destroy'] })
-type Routes = typeof routes
-// {
-// index: Route<'GET', '/users'> - Lists all users
-// new: Route<'GET', '/users/new'> - Shows a form to create a new user
-// show: Route<'GET', '/users/:userId'> - Shows a single user
-// create: Route<'POST', '/users'> - Creates a new user
-// }
-```
-
-## Navigation
-
-- [Routing based on request method](./routing-methods.md)
-- [Basic usage and route maps](./usage.md)
-- [fetch-router overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/testing-and-related.md b/docs/agents/remix/fetch-router/testing-and-related.md
deleted file mode 100644
index 16607c5a..00000000
--- a/docs/agents/remix/fetch-router/testing-and-related.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Testing and related work
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Testing
-
-Testing is straightforward because `fetch-router` uses the standard `fetch()`
-API:
-
-```ts
-import * as assert from 'node:assert/strict'
-import { describe, it } from 'node:test'
-
-describe('blog routes', () => {
- it('creates a new post', async () => {
- let response = await router.fetch('https://api.remix.run/posts', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ title: 'Hello', content: 'World' }),
- })
-
- assert.equal(response.status, 201)
- let post = await response.json()
- assert.equal(post.title, 'Hello')
- })
-
- it('returns 404 for missing posts', async () => {
- let response = await router.fetch('https://api.remix.run/posts/not-found')
- assert.equal(response.status, 404)
- })
-})
-```
-
-No special test harness or mocking required! Just use `fetch()` like you would
-in production.
-
-## Related work
-
-- [@remix-run/response](../response/index.md) - Response helpers for HTML, JSON,
- files, and redirects
-- [@remix-run/headers](../headers/index.md) - A library for working with HTTP
- headers
-- [@remix-run/form-data-parser](../form-data-parser) - A library for parsing
- multipart/form-data requests
-- [@remix-run/route-pattern](../route-pattern) - The pattern matching library
- that powers `fetch-router`
-- [Express](https://expressjs.com/) - The classic Node.js web framework
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/usage.md b/docs/agents/remix/fetch-router/usage.md
deleted file mode 100644
index 14fc7b8e..00000000
--- a/docs/agents/remix/fetch-router/usage.md
+++ /dev/null
@@ -1,170 +0,0 @@
-# Basic usage and route maps
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-The main purpose of the router is to map incoming requests to request handlers
-and middleware. The router uses the `fetch()` API to accept a
-[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return
-a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
-
-The example below is a small site with a home page, an "about" page, and a blog.
-
-```ts
-import { createRouter } from 'remix/fetch-router'
-import { route } from 'remix/fetch-router/routes'
-import { logger } from 'remix/logger-middleware'
-
-// `route()` creates a "route map" that organizes routes by name. The keys
-// of the map may be any name, and may be nested to group related routes.
-let routes = route({
- home: '/',
- about: '/about',
- blog: {
- index: '/blog',
- show: '/blog/:slug',
- },
-})
-
-let router = createRouter({
- // Middleware may be used to run code before and/or after actions run.
- // In this case, the `logger()` middleware logs the request to the console.
- middleware: [logger()],
-})
-
-// Map the routes to a "controller" that defines actions for each route. The
-// structure of the controller mirrors the structure of the route map.
-router.map(routes, {
- home() {
- return new Response('Home')
- },
- about() {
- return new Response('About')
- },
- blog: {
- index() {
- return new Response('Blog')
- },
- show({ params }) {
- // params is a type-safe object with the parameters from the route pattern
- return new Response(`Post ${params.slug}`)
- },
- },
-})
-
-let response = await router.fetch('https://remix.run/blog/hello-remix')
-console.log(await response.text()) // "Post hello-remix"
-```
-
-The route map is an object of the same shape as the object passed into
-`route()`, including nested objects. The leaves of the map are `Route` objects,
-which you can see if you inspect the type of the `routes` variable in your IDE.
-
-```ts
-type Routes = typeof routes
-// {
-// home: Route<'ANY', '/'>
-// about: Route<'ANY', '/about'>
-// blog: {
-// index: Route<'ANY', '/blog'>
-// show: Route<'ANY', '/blog/:slug'>
-// },
-// }
-```
-
-The `routes.home` route is a `Route<'ANY', '/'>`, which means it serves any
-request method (`GET`, `POST`, `PUT`, `DELETE`, etc.) when the URL path is `/`.
-We'll discuss routing based on request method in the routing guide.
-
-## Links and form actions
-
-In addition to describing the structure of your routes, route maps also make it
-easy to generate type-safe links and form actions using the `href()` function on
-a route. The example below is a small site with a home page and a "Contact Us"
-page.
-
-Note: We're using the
-[`createHtmlResponse` helper from `remix/response`](https://github.com/remix-run/remix/tree/main/packages/response/README.md#html-responses)
-below to create `Response`s with `Content-Type: text/html`. We're also using the
-`html` template tag to create safe HTML strings to use in the response body.
-
-```ts
-import { createRouter } from 'remix/fetch-router'
-import { route } from 'remix/fetch-router/routes'
-import { html } from 'remix/html-template'
-import { createHtmlResponse } from 'remix/response/html'
-
-let routes = route({
- home: '/',
- contact: '/contact',
-})
-
-let router = createRouter()
-
-// Register an action for `GET /`
-router.get(routes.home, () => {
- return createHtmlResponse(`
-
-
-
-
-
-
-
- `)
-})
-
-// Register an action for `POST /contact`
-router.post(routes.contact, ({ formData }) => {
- // POST actions receive a `context` object with a `formData` property that
- // contains the `FormData` from the form submission. It is automatically
- // parsed from the request body and available in all POST actions.
- let message = formData.get('message') as string
- let body = html`
-
-
-
Thanks!
-
-
You said: ${message}
-
-
-
-
- `
-
- return createHtmlResponse(body)
-})
-```
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Routing based on request method](./routing-methods.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/file-storage-s3.md b/docs/agents/remix/file-storage-s3.md
deleted file mode 100644
index ed64e787..00000000
--- a/docs/agents/remix/file-storage-s3.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# file-storage-s3
-
-Source: https://github.com/remix-run/remix/tree/main/packages/file-storage-s3
-
-## README
-
-S3 backend for `remix/file-storage`.
-
-Use this package when you want the `FileStorage` API backed by AWS S3 or an
-S3-compatible provider (MinIO, LocalStack, etc.).
-
-## Installation
-
-```sh
-npm i remix
-```
-
-## Usage
-
-```ts
-import { createS3FileStorage } from 'remix/file-storage-s3'
-
-let storage = createS3FileStorage({
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
- bucket: 'my-app-uploads',
- region: 'us-east-1',
-})
-```
-
-Use `endpoint` and `forcePathStyle: true` for non-AWS S3-compatible providers.
-
-## Related packages
-
-- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage)
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser)
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/file-storage.md b/docs/agents/remix/file-storage.md
deleted file mode 100644
index 4cf3af13..00000000
--- a/docs/agents/remix/file-storage.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# file-storage
-
-Source: https://github.com/remix-run/remix/tree/main/packages/file-storage
-
-## README
-
-`file-storage` is a key/value interface for storing
-[`File` objects](https://developer.mozilla.org/en-US/docs/Web/API/File) in
-JavaScript.
-
-Handling file uploads and storage is a common requirement in web applications,
-but each storage backend (local disk, AWS S3, Cloudflare R2, etc.) has its own
-API and conventions. This fragmentation makes it difficult to write portable
-code that can easily switch between storage providers or support multiple
-backends simultaneously.
-
-Similar to how `localStorage` allows you to store key/value pairs of strings in
-the browser, `file-storage` allows you to store key/value pairs of files on the
-server with a consistent interface regardless of the underlying storage
-mechanism.
-
-## Features
-
-- **Simple API** - Intuitive key/value API (like
- [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API),
- but for `File`s instead of strings)
-- **Generic Interface** - `FileStorage` interface that works for various large
- object storage backends (can be adapted to AWS S3, Cloudflare R2, etc.)
-- **Streaming Support** - Stream file content to and from storage
-- **Metadata Preservation** - Preserves all `File` metadata including
- `file.name`, `file.type`, and `file.lastModified`
-
-## Installation
-
-Install from [npm](https://www.npmjs.com/):
-
-```sh
-npm i remix
-```
-
-## Usage
-
-### File System
-
-```ts
-import { createFsFileStorage } from 'remix/file-storage/fs'
-
-let storage = createFsFileStorage('./user/files')
-
-let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
-let key = 'hello-key'
-
-// Put the file in storage.
-await storage.set(key, file)
-
-// Then, sometime later...
-let fileFromStorage = await storage.get(key)
-// All of the original file's metadata is intact
-fileFromStorage.name // 'hello.txt'
-fileFromStorage.type // 'text/plain'
-
-// To remove from storage
-await storage.remove(key)
-```
-
-## Related Packages
-
-- [`file-storage-s3`](https://github.com/remix-run/remix/tree/main/packages/file-storage-s3) -
- S3 backend for `file-storage`
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
- Pairs well with this library for storing `FileUpload` objects received in
- `multipart/form-data` requests
-- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) -
- The streaming `File` implementation used internally to stream files from
- storage
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/form-data-middleware.md b/docs/agents/remix/form-data-middleware.md
deleted file mode 100644
index df789f40..00000000
--- a/docs/agents/remix/form-data-middleware.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# form-data-middleware
-
-Source:
-https://github.com/remix-run/remix/tree/main/packages/form-data-middleware
-
-## README
-
-Middleware for parsing
-[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) from
-incoming request bodies for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-## Installation
-
-```sh
-bun add @remix-run/form-data-middleware
-```
-
-## Usage
-
-Use the `formData()` middleware at the router level to parse `FormData` from the
-request body and make it available on the request context as `context.formData`.
-
-`context.files` will also be available as a map of `File` objects keyed by the
-name of the form field.
-
-```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { formData } from '@remix-run/form-data-middleware'
-
-let router = createRouter({
- middleware: [formData()],
-})
-
-router.post('/users', async (context) => {
- let name = context.formData.get('name')
- let email = context.formData.get('email')
-
- // Handle file uploads
- let avatar = context.files?.get('avatar')
-
- return Response.json({ name, email, hasAvatar: !!avatar })
-})
-```
-
-### Custom File Upload Handler
-
-You can use a custom upload handler to customize how file uploads are handled.
-The return value of the upload handler will be used as the value of the form
-field in the `FormData` object.
-
-```ts
-import { formData } from '@remix-run/form-data-middleware'
-import { writeFile } from 'node:fs/promises'
-
-let router = createRouter({
- middleware: [
- formData({
- async uploadHandler(upload) {
- // Save to disk and return path
- let path = `./uploads/${upload.name}`
- await writeFile(path, Buffer.from(await upload.arrayBuffer()))
- return path
- },
- }),
- ],
-})
-```
-
-### Suppress Parse Errors
-
-Some requests may contain invalid form data that cannot be parsed. You can
-suppress parse errors by setting `suppressErrors` to `true`. In these cases,
-`context.formData` will be an empty `FormData` object.
-
-```ts
-let router = createRouter({
- middleware: [
- formData({
- suppressErrors: true, // Invalid form data won't throw
- }),
- ],
-})
-```
-
-## Related Packages
-
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router for the web Fetch API
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
- The underlying form data parser
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/form-data-parser.md b/docs/agents/remix/form-data-parser.md
deleted file mode 100644
index f80095df..00000000
--- a/docs/agents/remix/form-data-parser.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# form-data-parser
-
-Source: https://github.com/remix-run/remix/tree/main/packages/form-data-parser
-
-## README
-
-A streaming `multipart/form-data` parser that solves memory issues with file
-uploads in server environments. Built as an enhanced replacement for the native
-`request.formData()` API, it enables efficient handling of large file uploads by
-streaming directly to disk or cloud storage services like
-[AWS S3](https://aws.amazon.com/s3/) or
-[Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/), preventing
-server crashes from memory exhaustion.
-
-## Features
-
-- **Drop-in replacement** for `request.formData()` with streaming file upload
- support
-- **Minimal buffering** - processes file upload streams with minimal memory
- footprint
-- **Standards-based** - built on the
- [web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
- and [File API](https://developer.mozilla.org/en-US/docs/Web/API/File)
-- **Smart fallback** - automatically uses native `request.formData()` for
- non-`multipart/form-data` requests
-- **Storage agnostic** - works with any storage backend (local disk, S3, R2,
- etc.)
-
-## Why You Need This
-
-The native
-[`request.formData()` method](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData)
-has a few major flaws in server environments:
-
-- It buffers all file uploads in memory
-- It does not provide fine-grained control over file upload handling
-- It does not prevent DoS attacks from malicious requests
-
-In normal usage, this makes it difficult to process requests with large file
-uploads because they can exhaust your server's RAM and crash the application.
-
-For attackers, this creates an attack vector where malicious actors can
-overwhelm your server's memory by sending large payloads with many files.
-
-`form-data-parser` solves this by handling file uploads as they arrive in the
-request body stream, allowing you to safely store files and use either a) the
-`File` directly or b) a unique identifier for that file in the returned
-`FormData` object.
-
-## Installation
-
-Install from [npm](https://www.npmjs.com/):
-
-```sh
-bun add @remix-run/form-data-parser
-```
-
-## Usage
-
-The `parseFormData` interface allows you to define an "upload handler" function
-for fine-grained control of handling file uploads.
-
-```ts
-import * as fsp from 'node:fs/promises'
-import type { FileUpload } from '@remix-run/form-data-parser'
-import { parseFormData } from '@remix-run/form-data-parser'
-
-// Define how to handle incoming file uploads
-async function uploadHandler(fileUpload: FileUpload) {
- // Is this file upload from the field?
- if (fileUpload.fieldName === 'user-avatar') {
- let filename = `/uploads/user-${user.id}-avatar.bin`
-
- // Store the file safely on disk
- await fsp.writeFile(filename, fileUpload.bytes)
-
- // Return the file name to use in the FormData object so we don't
- // keep the file contents around in memory.
- return filename
- }
-
- // Ignore unrecognized fields
-}
-
-// Handle form submissions with file uploads
-async function requestHandler(request: Request) {
- // Parse the form data from the request.body stream, passing any files
- // through your upload handler as they are parsed from the stream
- let formData = await parseFormData(request, uploadHandler)
-
- let avatarFilename = formData.get('user-avatar')
-
- if (avatarFilename != null) {
- console.log(`User avatar uploaded to ${avatarFilename}`)
- } else {
- console.log(`No user avatar file was uploaded`)
- }
-}
-```
-
-To limit the maximum size of files that are uploaded, or the maximum number of
-files that may be uploaded in a single request, use the `maxFileSize` and
-`maxFiles` options.
-
-```ts
-import {
- MaxFilesExceededError,
- MaxFileSizeExceededError,
-} from '@remix-run/form-data-parser'
-
-const oneKb = 1024
-const oneMb = 1024 * oneKb
-
-try {
- let formData = await parseFormData(request, {
- maxFiles: 5,
- maxFileSize: 10 * oneMb,
- })
-} catch (error) {
- if (error instanceof MaxFilesExceededError) {
- console.error(`Request may not contain more than 5 files`)
- } else if (error instanceof MaxFileSizeExceededError) {
- console.error(`Files may not be larger than 10 MiB`)
- } else {
- console.error(`An unknown error occurred:`, error)
- }
-}
-```
-
-If you're looking for a more flexible storage solution for `File` objects that
-are uploaded, this library pairs really well with
-[the `file-storage` library](https://github.com/remix-run/remix/tree/main/packages/file-storage)
-for keeping files in various storage backends.
-
-```ts
-import { LocalFileStorage } from '@remix-run/file-storage/local'
-import type { FileUpload } from '@remix-run/form-data-parser'
-import { parseFormData } from '@remix-run/form-data-parser'
-
-// Set up storage for uploaded files
-const fileStorage = new LocalFileStorage('/uploads/user-avatars')
-
-// Define how to handle incoming file uploads
-async function uploadHandler(fileUpload: FileUpload) {
- // Is this file upload from the field?
- if (fileUpload.fieldName === 'user-avatar') {
- let storageKey = `user-${user.id}-avatar`
-
- // Put the file in storage
- await fileStorage.set(storageKey, fileUpload)
-
- // Return a lazy File object that can access the stored file when needed
- return fileStorage.get(storageKey)
- }
-
- // Ignore unrecognized fields
-}
-```
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos)
-contains working demos:
-
-- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos/node) -
- using form-data-parser with file-storage in Node.js
-
-## Related Packages
-
-- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) -
- A simple key/value interface for storing `FileUpload` objects you get from the
- parser
-- [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) -
- The parser used internally for parsing `multipart/form-data` HTTP messages
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/fs.md b/docs/agents/remix/fs.md
deleted file mode 100644
index 8e5c38b9..00000000
--- a/docs/agents/remix/fs.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# fs
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fs
-
-## README
-
-Lazy, streaming filesystem utilities for JavaScript.
-
-This package provides utilities for working with files on the local filesystem
-using the
-[`LazyFile`](https://github.com/remix-run/remix/tree/main/packages/lazy-file)/
-native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API.
-
-## Features
-
-- **Web Standards** - Uses
- [`LazyFile`](https://github.com/remix-run/remix/tree/main/packages/lazy-file)
- which matches the native
- [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API and
- provides `.stream()`, `.toFile()`, and `.toBlob()` for converting to native
- types.
-- **Seamless Node.js Compat** - Works seamlessly with Node.js file descriptors
- and handles
-
-## Installation
-
-Install from [npm](https://www.npmjs.com/):
-
-```sh
-bun add @remix-run/fs
-```
-
-## Usage
-
-### Opening Lazy Files
-
-```ts
-import { openLazyFile } from '@remix-run/fs'
-
-// Open a file from the filesystem
-let lazyFile = openLazyFile('./path/to/file.json')
-
-// The file is lazy - no data is read until you call lazyFile.text(), lazyFile.bytes(), etc.
-let json = JSON.parse(await lazyFile.text())
-
-// You can override file metadata
-let customLazyFile = openLazyFile('./image.jpg', {
- name: 'custom-name.jpg',
- type: 'image/jpeg',
- lastModified: Date.now(),
-})
-```
-
-### Writing Files
-
-```ts
-import { openLazyFile, writeFile } from '@remix-run/fs'
-
-// Read a file and write it elsewhere
-let lazyFile = openLazyFile('./source.txt')
-await writeFile('./destination.txt', lazyFile)
-
-// Write to an open file handle
-import * as fsp from 'node:fs/promises'
-let handle = await fsp.open('./destination.txt', 'w')
-await writeFile(handle, lazyFile)
-await handle.close()
-```
-
-## Related Packages
-
-- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) -
- Lazy, streaming `Blob`/`File` implementation
-- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) -
- Storage abstraction for files
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/headers/accept-headers.md b/docs/agents/remix/headers/accept-headers.md
deleted file mode 100644
index bf2b9a05..00000000
--- a/docs/agents/remix/headers/accept-headers.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# Accept headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-Each supported header has a class that represents the header value. Use the
-static `from()` method to parse header values. Each class has a `toString()`
-method that returns the header value as a string.
-
-## Accept
-
-Parse, manipulate and stringify
-[`Accept` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept).
-
-Implements `Map`.
-
-```ts
-import { Accept } from '@remix-run/headers'
-
-// Parse from headers
-let accept = Accept.from(request.headers.get('accept'))
-
-accept.mediaTypes // ['text/html', 'text/*']
-accept.weights // [1, 0.9]
-accept.accepts('text/html') // true
-accept.accepts('text/plain') // true (matches text/*)
-accept.accepts('image/jpeg') // false
-accept.getWeight('text/plain') // 1 (matches text/*)
-accept.getPreferred(['text/html', 'text/plain']) // 'text/html'
-
-// Iterate
-for (let [mediaType, quality] of accept) {
- // ...
-}
-
-// Modify and set header
-accept.set('application/json', 0.8)
-accept.delete('text/*')
-headers.set('Accept', accept)
-
-// Construct directly
-new Accept('text/html, text/*;q=0.9')
-new Accept({ 'text/html': 1, 'text/*': 0.9 })
-new Accept(['text/html', ['text/*', 0.9]])
-
-// Use class for type safety when setting Headers values
-// via Accept's `.toString()` method
-let headers = new Headers({
- Accept: new Accept({ 'text/html': 1, 'application/json': 0.8 }),
-})
-headers.set('Accept', new Accept({ 'text/html': 1, 'application/json': 0.8 }))
-```
-
-## Accept-Encoding
-
-Parse, manipulate and stringify
-[`Accept-Encoding` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).
-
-Implements `Map`.
-
-```ts
-import { AcceptEncoding } from '@remix-run/headers'
-
-// Parse from headers
-let acceptEncoding = AcceptEncoding.from(request.headers.get('accept-encoding'))
-
-acceptEncoding.encodings // ['gzip', 'deflate']
-acceptEncoding.weights // [1, 0.8]
-acceptEncoding.accepts('gzip') // true
-acceptEncoding.accepts('br') // false
-acceptEncoding.getWeight('gzip') // 1
-acceptEncoding.getPreferred(['gzip', 'deflate', 'br']) // 'gzip'
-
-// Modify and set header
-acceptEncoding.set('br', 1)
-acceptEncoding.delete('deflate')
-headers.set('Accept-Encoding', acceptEncoding)
-
-// Construct directly
-new AcceptEncoding('gzip, deflate;q=0.8')
-new AcceptEncoding({ gzip: 1, deflate: 0.8 })
-
-// Use class for type safety when setting Headers values
-// via AcceptEncoding's `.toString()` method
-let headers = new Headers({
- 'Accept-Encoding': new AcceptEncoding({ gzip: 1, br: 0.9 }),
-})
-headers.set('Accept-Encoding', new AcceptEncoding({ gzip: 1, br: 0.9 }))
-```
-
-## Accept-Language
-
-Parse, manipulate and stringify
-[`Accept-Language` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language).
-
-Implements `Map`.
-
-```ts
-import { AcceptLanguage } from '@remix-run/headers'
-
-// Parse from headers
-let acceptLanguage = AcceptLanguage.from(request.headers.get('accept-language'))
-
-acceptLanguage.languages // ['en-us', 'en']
-acceptLanguage.weights // [1, 0.9]
-acceptLanguage.accepts('en-US') // true
-acceptLanguage.accepts('en-GB') // true (matches en)
-acceptLanguage.getWeight('en-GB') // 1 (matches en)
-acceptLanguage.getPreferred(['en-US', 'en-GB', 'fr']) // 'en-US'
-
-// Modify and set header
-acceptLanguage.set('fr', 0.5)
-acceptLanguage.delete('en')
-headers.set('Accept-Language', acceptLanguage)
-
-// Construct directly
-new AcceptLanguage('en-US, en;q=0.9')
-new AcceptLanguage({ 'en-US': 1, en: 0.9 })
-
-// Use class for type safety when setting Headers values
-// via AcceptLanguage's `.toString()` method
-let headers = new Headers({
- 'Accept-Language': new AcceptLanguage({ 'en-US': 1, fr: 0.5 }),
-})
-headers.set('Accept-Language', new AcceptLanguage({ 'en-US': 1, fr: 0.5 }))
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Content and cache headers](./content-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/conditional-headers.md b/docs/agents/remix/headers/conditional-headers.md
deleted file mode 100644
index 1dd4cbfa..00000000
--- a/docs/agents/remix/headers/conditional-headers.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Conditionals and ranges
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## If-Match
-
-Parse, manipulate and stringify
-[`If-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match).
-
-Implements `Set`.
-
-```ts
-import { IfMatch } from '@remix-run/headers'
-
-// Parse from headers
-let ifMatch = IfMatch.from(request.headers.get('if-match'))
-
-ifMatch.tags // ['"67ab43"', '"54ed21"']
-ifMatch.has('"67ab43"') // true
-ifMatch.matches('"67ab43"') // true (checks precondition)
-ifMatch.matches('"abc123"') // false
-
-// Note: Uses strong comparison only (weak ETags never match)
-let weak = IfMatch.from('W/"67ab43"')
-weak.matches('W/"67ab43"') // false
-
-// Modify and set header
-ifMatch.add('"newetag"')
-ifMatch.delete('"67ab43"')
-headers.set('If-Match', ifMatch)
-
-// Construct directly
-new IfMatch(['abc123', 'def456'])
-
-// Use class for type safety when setting Headers values
-// via IfMatch's `.toString()` method
-let headers = new Headers({
- 'If-Match': new IfMatch(['"abc123"', '"def456"']),
-})
-headers.set('If-Match', new IfMatch(['"abc123"', '"def456"']))
-```
-
-## If-None-Match
-
-Parse, manipulate and stringify
-[`If-None-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match).
-
-Implements `Set`.
-
-```ts
-import { IfNoneMatch } from '@remix-run/headers'
-
-// Parse from headers
-let ifNoneMatch = IfNoneMatch.from(request.headers.get('if-none-match'))
-
-ifNoneMatch.tags // ['"67ab43"', '"54ed21"']
-ifNoneMatch.has('"67ab43"') // true
-ifNoneMatch.matches('"67ab43"') // true
-
-// Supports weak comparison (unlike If-Match)
-let weak = IfNoneMatch.from('W/"67ab43"')
-weak.matches('W/"67ab43"') // true
-
-// Modify and set header
-ifNoneMatch.add('"newetag"')
-ifNoneMatch.delete('"67ab43"')
-headers.set('If-None-Match', ifNoneMatch)
-
-// Construct directly
-new IfNoneMatch(['abc123'])
-
-// Use class for type safety when setting Headers values
-// via IfNoneMatch's `.toString()` method
-let headers = new Headers({
- 'If-None-Match': new IfNoneMatch(['"abc123"']),
-})
-headers.set('If-None-Match', new IfNoneMatch(['"abc123"']))
-```
-
-## If-Range
-
-Parse, manipulate and stringify
-[`If-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range).
-
-```ts
-import { IfRange } from '@remix-run/headers'
-
-// Parse from headers
-let ifRange = IfRange.from(request.headers.get('if-range'))
-
-// With HTTP date
-ifRange.matches({ lastModified: 1609459200000 }) // true
-ifRange.matches({ lastModified: new Date('2021-01-01') }) // true
-
-// With ETag
-let etagHeader = IfRange.from('"67ab43"')
-etagHeader.matches({ etag: '"67ab43"' }) // true
-
-// Empty/null returns empty instance (range proceeds unconditionally)
-let empty = IfRange.from(null)
-empty.matches({ etag: '"any"' }) // true
-
-// Construct directly
-new IfRange('"abc123"')
-
-// Use class for type safety when setting Headers values
-// via IfRange's `.toString()` method
-let headers = new Headers({
- 'If-Range': new IfRange('"abc123"'),
-})
-headers.set('If-Range', new IfRange('"abc123"'))
-```
-
-## Range
-
-Parse, manipulate and stringify
-[`Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range).
-
-```ts
-import { Range } from '@remix-run/headers'
-
-// Parse from headers
-let range = Range.from(request.headers.get('range'))
-
-range.unit // "bytes"
-range.ranges // [{ start: 200, end: 1000 }]
-range.canSatisfy(2000) // true
-range.canSatisfy(500) // false
-range.normalize(2000) // [{ start: 200, end: 1000 }]
-
-// Multiple ranges
-let multi = Range.from('bytes=0-499, 1000-1499')
-multi.ranges.length // 2
-
-// Suffix range (last N bytes)
-let suffix = Range.from('bytes=-500')
-suffix.normalize(2000) // [{ start: 1500, end: 1999 }]
-
-// Construct directly
-new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })
-
-// Use class for type safety when setting Headers values
-// via Range's `.toString()` method
-let headers = new Headers({
- Range: new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),
-})
-headers.set(
- 'Range',
- new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),
-)
-```
-
-## Vary
-
-Parse, manipulate and stringify
-[`Vary` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary).
-
-Implements `Set`.
-
-```ts
-import { Vary } from '@remix-run/headers'
-
-// Parse from headers
-let vary = Vary.from(response.headers.get('vary'))
-
-vary.headerNames // ['accept-encoding', 'accept-language']
-vary.has('Accept-Encoding') // true (case-insensitive)
-vary.size // 2
-
-// Modify and set header
-vary.add('User-Agent')
-vary.delete('Accept-Language')
-headers.set('Vary', vary)
-
-// Construct directly
-new Vary('Accept-Encoding, Accept-Language')
-new Vary(['Accept-Encoding', 'Accept-Language'])
-new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })
-
-// Use class for type safety when setting Headers values
-// via Vary's `.toString()` method
-let headers = new Headers({
- Vary: new Vary(['Accept-Encoding', 'Accept-Language']),
-})
-headers.set('Vary', new Vary(['Accept-Encoding', 'Accept-Language']))
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Cookie headers](./cookie-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/content-headers.md b/docs/agents/remix/headers/content-headers.md
deleted file mode 100644
index f007953a..00000000
--- a/docs/agents/remix/headers/content-headers.md
+++ /dev/null
@@ -1,161 +0,0 @@
-# Content and cache headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Cache-Control
-
-Parse, manipulate and stringify
-[`Cache-Control` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
-
-```ts
-import { CacheControl } from '@remix-run/headers'
-
-// Parse from headers
-let cacheControl = CacheControl.from(response.headers.get('cache-control'))
-
-cacheControl.public // true
-cacheControl.maxAge // 3600
-cacheControl.sMaxage // 7200
-cacheControl.noCache // undefined
-cacheControl.noStore // undefined
-cacheControl.noTransform // undefined
-cacheControl.mustRevalidate // undefined
-cacheControl.immutable // undefined
-
-// Modify and set header
-cacheControl.maxAge = 7200
-cacheControl.immutable = true
-headers.set('Cache-Control', cacheControl)
-
-// Construct directly
-new CacheControl('public, max-age=3600')
-new CacheControl({ public: true, maxAge: 3600 })
-
-// Use class for type safety when setting Headers values
-// via CacheControl's `.toString()` method
-let headers = new Headers({
- 'Cache-Control': new CacheControl({ public: true, maxAge: 3600 }),
-})
-headers.set('Cache-Control', new CacheControl({ public: true, maxAge: 3600 }))
-```
-
-## Content-Disposition
-
-Parse, manipulate and stringify
-[`Content-Disposition` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition).
-
-```ts
-import { ContentDisposition } from '@remix-run/headers'
-
-// Parse from headers
-let contentDisposition = ContentDisposition.from(
- response.headers.get('content-disposition'),
-)
-
-contentDisposition.type // 'attachment'
-contentDisposition.filename // 'example.pdf'
-contentDisposition.filenameSplat // "UTF-8''example.pdf"
-contentDisposition.preferredFilename // 'example.pdf' (decoded from filename*)
-
-// Modify and set header
-contentDisposition.filename = 'download.pdf'
-headers.set('Content-Disposition', contentDisposition)
-
-// Construct directly
-new ContentDisposition('attachment; filename="example.pdf"')
-new ContentDisposition({ type: 'attachment', filename: 'example.pdf' })
-
-// Use class for type safety when setting Headers values
-// via ContentDisposition's `.toString()` method
-let headers = new Headers({
- 'Content-Disposition': new ContentDisposition({
- type: 'attachment',
- filename: 'example.pdf',
- }),
-})
-headers.set(
- 'Content-Disposition',
- new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),
-)
-```
-
-## Content-Range
-
-Parse, manipulate and stringify
-[`Content-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).
-
-```ts
-import { ContentRange } from '@remix-run/headers'
-
-// Parse from headers
-let contentRange = ContentRange.from(response.headers.get('content-range'))
-
-contentRange.unit // "bytes"
-contentRange.start // 200
-contentRange.end // 1000
-contentRange.size // 67589
-
-// Unsatisfied range
-let unsatisfied = ContentRange.from('bytes */67589')
-unsatisfied.start // null
-unsatisfied.end // null
-unsatisfied.size // 67589
-
-// Construct directly
-new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 })
-
-// Use class for type safety when setting Headers values
-// via ContentRange's `.toString()` method
-let headers = new Headers({
- 'Content-Range': new ContentRange({
- unit: 'bytes',
- start: 0,
- end: 499,
- size: 1000,
- }),
-})
-headers.set(
- 'Content-Range',
- new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }),
-)
-```
-
-## Content-Type
-
-Parse, manipulate and stringify
-[`Content-Type` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).
-
-```ts
-import { ContentType } from '@remix-run/headers'
-
-// Parse from headers
-let contentType = ContentType.from(request.headers.get('content-type'))
-
-contentType.mediaType // "text/html"
-contentType.charset // "utf-8"
-contentType.boundary // undefined (or boundary string for multipart)
-
-// Modify and set header
-contentType.charset = 'iso-8859-1'
-headers.set('Content-Type', contentType)
-
-// Construct directly
-new ContentType('text/html; charset=utf-8')
-new ContentType({ mediaType: 'text/html', charset: 'utf-8' })
-
-// Use class for type safety when setting Headers values
-// via ContentType's `.toString()` method
-let headers = new Headers({
- 'Content-Type': new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
-})
-headers.set(
- 'Content-Type',
- new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
-)
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Accept headers](./accept-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/cookie-headers.md b/docs/agents/remix/headers/cookie-headers.md
deleted file mode 100644
index 65e4fa38..00000000
--- a/docs/agents/remix/headers/cookie-headers.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Cookie headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Cookie
-
-Parse, manipulate and stringify
-[`Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie).
-
-Implements `Map`.
-
-```ts
-import { Cookie } from '@remix-run/headers'
-
-// Parse from headers
-let cookie = Cookie.from(request.headers.get('cookie'))
-
-cookie.get('session_id') // 'abc123'
-cookie.get('theme') // 'dark'
-cookie.has('session_id') // true
-cookie.size // 2
-
-// Iterate
-for (let [name, value] of cookie) {
- // ...
-}
-
-// Modify and set header
-cookie.set('theme', 'light')
-cookie.delete('session_id')
-headers.set('Cookie', cookie)
-
-// Construct directly
-new Cookie('session_id=abc123; theme=dark')
-new Cookie({ session_id: 'abc123', theme: 'dark' })
-new Cookie([
- ['session_id', 'abc123'],
- ['theme', 'dark'],
-])
-
-// Use class for type safety when setting Headers values
-// via Cookie's `.toString()` method
-let headers = new Headers({
- Cookie: new Cookie({ session_id: 'abc123', theme: 'dark' }),
-})
-headers.set('Cookie', new Cookie({ session_id: 'abc123', theme: 'dark' }))
-```
-
-## Set-Cookie
-
-Parse, manipulate and stringify
-[`Set-Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
-
-```ts
-import { SetCookie } from '@remix-run/headers'
-
-// Parse from headers
-let setCookie = SetCookie.from(response.headers.get('set-cookie'))
-
-setCookie.name // "session_id"
-setCookie.value // "abc"
-setCookie.path // "/"
-setCookie.httpOnly // true
-setCookie.secure // true
-setCookie.domain // undefined
-setCookie.maxAge // undefined
-setCookie.expires // undefined
-setCookie.sameSite // undefined
-
-// Modify and set header
-setCookie.maxAge = 3600
-setCookie.sameSite = 'Strict'
-headers.set('Set-Cookie', setCookie)
-
-// Construct directly
-new SetCookie('session_id=abc; Path=/; HttpOnly; Secure')
-new SetCookie({
- name: 'session_id',
- value: 'abc',
- path: '/',
- httpOnly: true,
- secure: true,
-})
-
-// Use class for type safety when setting Headers values
-// via SetCookie's `.toString()` method
-let headers = new Headers({
- 'Set-Cookie': new SetCookie({
- name: 'session_id',
- value: 'abc',
- httpOnly: true,
- }),
-})
-headers.set(
- 'Set-Cookie',
- new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }),
-)
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Conditionals and ranges](./conditional-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/index.md b/docs/agents/remix/headers/index.md
deleted file mode 100644
index ad5586d8..00000000
--- a/docs/agents/remix/headers/index.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Overview
-
-Utilities for parsing, manipulating and stringifying HTTP header values.
-
-HTTP headers contain critical information - from content negotiation and caching
-directives to authentication tokens and file metadata. While the native
-`Headers` API provides a basic string-based interface, it leaves the
-complexities of parsing specific header formats entirely up to you.
-
-## Installation
-
-```sh
-bun add @remix-run/headers
-```
-
-## Header utilities
-
-- Accept headers: [accept-headers](./accept-headers.md)
-- Content and cache headers: [content-headers](./content-headers.md)
-- Cookies: [cookie-headers](./cookie-headers.md)
-- Conditionals and ranges: [conditional-headers](./conditional-headers.md)
-- Raw header parsing: [raw-headers](./raw-headers.md)
-
-## Navigation
-
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/raw-headers.md b/docs/agents/remix/headers/raw-headers.md
deleted file mode 100644
index eeea3698..00000000
--- a/docs/agents/remix/headers/raw-headers.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Raw header parsing
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Raw headers
-
-Parse and stringify raw HTTP header strings.
-
-```ts
-import { parse, stringify } from '@remix-run/headers'
-
-let headers = parse('Content-Type: text/html\\r\\nCache-Control: no-cache')
-headers.get('content-type') // 'text/html'
-headers.get('cache-control') // 'no-cache'
-
-stringify(headers)
-// 'Content-Type: text/html\\r\\nCache-Control: no-cache'
-```
-
-## Related packages
-
-- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) -
- Build HTTP proxy servers using the web fetch API
-- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) -
- Build HTTP servers on Node.js using the web fetch API
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/html-template.md b/docs/agents/remix/html-template.md
deleted file mode 100644
index b9cd9f77..00000000
--- a/docs/agents/remix/html-template.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# html-template
-
-Source: https://github.com/remix-run/remix/tree/main/packages/html-template
-
-## README
-
-Safe HTML template tag with auto-escaping for JavaScript.
-
-Building HTML strings with user input is dangerous. Without proper escaping, you
-risk XSS (cross-site scripting) vulnerabilities where malicious code can be
-injected into your pages. Manual escaping is error-prone and easy to forget,
-especially when composing HTML from multiple sources.
-
-`html-template` provides a tagged template literal for safely constructing HTML
-strings with automatic escaping of interpolated values to prevent XSS
-vulnerabilities.
-
-## Features
-
-- **Automatic HTML escaping** - All interpolated values are escaped by default
-- **Explicit raw HTML** - Use `html.raw` when you need unescaped HTML from
- trusted sources
-- **Composable** - SafeHtml values can be nested without double-escaping
-- **Type-safe** - Full TypeScript support with branded types
-- **Zero dependencies** - Lightweight and self-contained
-- **Runtime agnostic** - Works in Node.js, Bun, Deno, browsers, and edge
- runtimes
-
-## Installation
-
-Install from [npm](https://www.npmjs.com/):
-
-```sh
-bun add @remix-run/html-template
-```
-
-## Usage
-
-```ts
-import { html } from '@remix-run/html-template'
-
-let userInput = ''
-let greeting = html`
Hello ${userInput}!
`
-
-console.log(String(greeting))
-// Output:
Hello <script>alert("XSS")</script>!
-```
-
-By default, all interpolated values are automatically escaped to prevent XSS
-attacks.
-
-If you have trusted HTML that should not be escaped, use `html.raw`:
-
-```ts
-import { html } from '@remix-run/html-template'
-
-let trustedIcon = ''
-let button = html.raw``
-
-console.log(String(button))
-// =>
-```
-
-**Warning**: Only use `html.raw` with content you trust. Never use it with user
-input.
-
-### Composing HTML Fragments
-
-SafeHtml values can be nested without double-escaping:
-
-```ts
-import { html } from '@remix-run/html-template'
-
-let title = html`
My Title
`
-let content = html`
Some content with ${userInput}
`
-
-let page = html`
-
-
-
- ${title} ${content}
-
-
-`
-```
-
-### Working with Arrays
-
-You can interpolate arrays of values, which will be flattened and joined:
-
-```ts
-import { html } from '@remix-run/html-template'
-
-let items = ['Apple', 'Banana', 'Cherry']
-let list = html`
-
- ${items.map((item) => html`
${item}
`)}
-
-`
-```
-
-### Conditional Rendering
-
-Use `null` or `undefined` to render nothing:
-
-```ts
-import { html } from '@remix-run/html-template'
-
-let showError = false
-let errorMessage = 'Something went wrong'
-let page = html`
- ${showError ? html`
${errorMessage}
` : null}
-
`
-```
-
-## Related Packages
-
-- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- HTTP router that works great with html-template
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/index.md b/docs/agents/remix/index.md
deleted file mode 100644
index 4e2588d1..00000000
--- a/docs/agents/remix/index.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# Remix packages
-
-Docs for every package in https://github.com/remix-run/remix/tree/main/packages.
-
-## Table of contents
-
-- [Start here](#start-here)
-- [UI and components](#ui-and-components)
-- [Routing and requests](#routing-and-requests)
-- [Data and SQL](#data-and-sql)
-- [Sessions and cookies](#sessions-and-cookies)
-- [Responses and headers](#responses-and-headers)
-- [Uploads and parsing](#uploads-and-parsing)
-- [Files and storage](#files-and-storage)
-- [Middleware and utilities](#middleware-and-utilities)
-- [Package map](#package-map)
-- [Update instructions](#update-instructions)
-
-## Start here
-
-- Building UI with Remix Component: [component](./component/index.md)
-- Routing and request handling: [fetch-router](./fetch-router/index.md) +
- [route-pattern](./route-pattern.md)
-- Sessions and cookies: [session](./session/index.md) +
- [session-middleware](./session-middleware.md) + [cookie](./cookie.md)
-- Responses, headers, and HTML safety: [response](./response/index.md) +
- [headers](./headers/index.md) + [html-template](./html-template.md)
-- Data validation and SQL tables: [data-schema](./data-schema.md) +
- [data-table](./data-table.md)
-- File upload pipelines: [form-data-middleware](./form-data-middleware.md) +
- [form-data-parser](./form-data-parser.md) +
- [multipart-parser](./multipart-parser/index.md)
-- File storage and streaming: [file-storage](./file-storage.md) +
- [file-storage-s3](./file-storage-s3.md) + [lazy-file](./lazy-file.md) +
- [fs](./fs.md)
-- Static assets and compression: [static-middleware](./static-middleware.md) +
- [compression-middleware](./compression-middleware/index.md)
-
-## epicflare adoption snapshot
-
-- Primary runtime packages in active use:
- - `remix/component`
- - `remix/fetch-router`
- - `remix/data-schema`
- - `remix/data-table`
-- D1 integration uses `remix/data-table` with a repository adapter
- (`worker/d1-data-table-adapter.ts`) instead of `remix/data-table-sqlite`.
-- Package coverage audit against installed `remix@3.0.0-alpha.3` top-level
- exports: no missing Remix package docs in this index.
-
-## UI and components
-
-- [component](./component/index.md)
- - [Getting started](./component/getting-started.md)
- - [Components](./component/components.md)
- - [Styling basics](./component/styling-basics.md)
- - [Animate basics](./component/animate-basics.md)
- - [Testing](./component/testing.md)
-- [interaction](./interaction/index.md)
- - [Event listeners and interactions](./interaction/listeners.md)
- - [Containers and disposal](./interaction/containers-and-disposal.md)
- - [Custom interactions and typed targets](./interaction/custom-interactions.md)
-
-## Routing and requests
-
-- [fetch-router](./fetch-router/index.md)
- - [Basic usage and route maps](./fetch-router/usage.md)
- - [Routing based on request method](./fetch-router/routing-methods.md)
- - [Resource-based routes](./fetch-router/routing-resources.md)
- - [Middleware and request context](./fetch-router/middleware.md)
-- [route-pattern](./route-pattern.md)
-- [node-fetch-server](./node-fetch-server/index.md)
- - [Quick start](./node-fetch-server/quick-start.md)
- - [Advanced usage](./node-fetch-server/advanced-usage.md)
- - [Migration from Express](./node-fetch-server/migration.md)
- - [Demos and benchmark](./node-fetch-server/demos-and-benchmark.md)
-- [fetch-proxy](./fetch-proxy.md)
-
-## Data and SQL
-
-- [data-schema](./data-schema.md)
-- [data-table](./data-table.md)
-- [data-table-postgres](./data-table-postgres.md)
-- [data-table-mysql](./data-table-mysql.md)
-- [data-table-sqlite](./data-table-sqlite.md)
-
-## Sessions and cookies
-
-- [session](./session/index.md)
- - [Flash data and security](./session/flash-and-security.md)
- - [Storage strategies](./session/storage-strategies.md)
- - [Related packages](./session/related.md)
-- [session-middleware](./session-middleware.md)
-- [session-storage-memcache](./session-storage-memcache.md)
-- [session-storage-redis](./session-storage-redis.md)
-- [cookie](./cookie.md)
-
-## Responses and headers
-
-- [response](./response/index.md)
- - [File responses](./response/file-responses.md)
- - [HTML responses](./response/html-responses.md)
- - [Redirect responses](./response/redirect-responses.md)
- - [Compressed responses](./response/compress-responses.md)
- - [Related packages](./response/related.md)
-- [headers](./headers/index.md)
- - [Accept headers](./headers/accept-headers.md)
- - [Content and cache headers](./headers/content-headers.md)
- - [Cookie headers](./headers/cookie-headers.md)
- - [Conditionals and ranges](./headers/conditional-headers.md)
- - [Raw header parsing](./headers/raw-headers.md)
-- [html-template](./html-template.md)
-
-## Uploads and parsing
-
-- [form-data-middleware](./form-data-middleware.md)
-- [form-data-parser](./form-data-parser.md)
-- [multipart-parser](./multipart-parser/index.md)
- - [Limits and Node bindings](./multipart-parser/limits-and-node.md)
- - [Low-level APIs](./multipart-parser/low-level.md)
- - [Benchmarks and related packages](./multipart-parser/benchmarks.md)
-
-## Files and storage
-
-- [file-storage](./file-storage.md)
-- [file-storage-s3](./file-storage-s3.md)
-- [lazy-file](./lazy-file.md)
-- [fs](./fs.md)
-- [tar-parser](./tar-parser.md)
-
-## Middleware and utilities
-
-- [compression-middleware](./compression-middleware/index.md)
- - [Options and configuration](./compression-middleware/options.md)
-- [static-middleware](./static-middleware.md)
-- [logger-middleware](./logger-middleware.md)
-- [method-override-middleware](./method-override-middleware.md)
-- [async-context-middleware](./async-context-middleware.md)
-- [mime](./mime.md)
-- [remix](./remix.md)
-
-## Package map
-
-| Package | Focus | Docs |
-| -------------------------- | ------------------------------------------ | ------------------------------------------------------------- |
-| async-context-middleware | AsyncLocalStorage context for fetch-router | [async-context-middleware](./async-context-middleware.md) |
-| component | Remix Component UI system | [component](./component/index.md) |
-| compression-middleware | Response compression for fetch-router | [compression-middleware](./compression-middleware/index.md) |
-| cookie | Cookie parsing, signing, and serialization | [cookie](./cookie.md) |
-| data-schema | Runtime validation and schema parsing | [data-schema](./data-schema.md) |
-| data-table | Typed SQL query toolkit | [data-table](./data-table.md) |
-| data-table-mysql | MySQL adapter for data-table | [data-table-mysql](./data-table-mysql.md) |
-| data-table-postgres | Postgres adapter for data-table | [data-table-postgres](./data-table-postgres.md) |
-| data-table-sqlite | SQLite adapter for data-table | [data-table-sqlite](./data-table-sqlite.md) |
-| fetch-proxy | Fetch-based HTTP proxy | [fetch-proxy](./fetch-proxy.md) |
-| fetch-router | Fetch-based router and middleware | [fetch-router](./fetch-router/index.md) |
-| file-storage | Storage abstraction for files | [file-storage](./file-storage.md) |
-| file-storage-s3 | S3 backend for file-storage | [file-storage-s3](./file-storage-s3.md) |
-| form-data-middleware | Request FormData middleware | [form-data-middleware](./form-data-middleware.md) |
-| form-data-parser | Streaming multipart/form-data parser | [form-data-parser](./form-data-parser.md) |
-| fs | Lazy file system utilities | [fs](./fs.md) |
-| headers | Header parsing and helpers | [headers](./headers/index.md) |
-| html-template | Safe HTML template tag | [html-template](./html-template.md) |
-| interaction | Event helpers and interactions | [interaction](./interaction/index.md) |
-| lazy-file | Streaming File/Blob implementation | [lazy-file](./lazy-file.md) |
-| logger-middleware | Request/response logging | [logger-middleware](./logger-middleware.md) |
-| method-override-middleware | HTML form method override | [method-override-middleware](./method-override-middleware.md) |
-| mime | MIME type utilities | [mime](./mime.md) |
-| multipart-parser | Streaming multipart parser | [multipart-parser](./multipart-parser/index.md) |
-| node-fetch-server | Fetch-based Node server | [node-fetch-server](./node-fetch-server/index.md) |
-| remix | Remix framework package | [remix](./remix.md) |
-| response | Response helpers | [response](./response/index.md) |
-| route-pattern | URL matching and href generation | [route-pattern](./route-pattern.md) |
-| session | Session management and storage | [session](./session/index.md) |
-| session-middleware | Session middleware for fetch-router | [session-middleware](./session-middleware.md) |
-| session-storage-memcache | Memcache storage adapter for sessions | [session-storage-memcache](./session-storage-memcache.md) |
-| session-storage-redis | Redis storage adapter for sessions | [session-storage-redis](./session-storage-redis.md) |
-| static-middleware | Static file middleware | [static-middleware](./static-middleware.md) |
-| tar-parser | Streaming tar parser | [tar-parser](./tar-parser.md) |
-
-## Update instructions
-
-See [update](./update.md) for how to sync this documentation from upstream.
diff --git a/docs/agents/remix/interaction/containers-and-disposal.md b/docs/agents/remix/interaction/containers-and-disposal.md
deleted file mode 100644
index 10c11164..00000000
--- a/docs/agents/remix/interaction/containers-and-disposal.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Containers and disposal
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Updating listeners efficiently
-
-Use `createContainer` when you need to update listeners in place (e.g., in a
-component system). The container diffs and updates existing bindings without
-unnecessary `removeEventListener`/`addEventListener` churn.
-
-```ts
-import { createContainer } from '@remix-run/interaction'
-
-let container = createContainer(form)
-
-let formData = new FormData()
-
-container.set({
- change(event) {
- formData = new FormData(event.currentTarget)
- },
- async submit(event, signal) {
- event.preventDefault()
- await fetch('/save', { method: 'POST', body: formData, signal })
- },
-})
-
-// later - only the minimal necessary changes are rebound
-container.set({
- change(event) {
- console.log('different listener')
- },
- submit(event, signal) {
- console.log('different listener')
- },
-})
-```
-
-## Disposing listeners
-
-`on` returns a dispose function. Containers expose `dispose()`. You can also
-pass an external `AbortSignal`.
-
-```ts
-import { on, createContainer } from '@remix-run/interaction'
-
-// Using the function returned from on()
-let dispose = on(button, { click: () => {} })
-dispose()
-
-// Containers
-let container = createContainer(window)
-container.set({ resize: () => {} })
-container.dispose()
-
-// Use a signal
-let eventsController = new AbortController()
-let container = createContainer(window, {
- signal: eventsController.signal,
-})
-container.set({ resize: () => {} })
-eventsController.abort()
-```
-
-## Stop propagation semantics
-
-All DOM semantics are preserved.
-
-```ts
-on(button, {
- click: [
- (event) => {
- event.stopImmediatePropagation()
- },
- () => {
- // not called
- },
- ],
-})
-```
-
-## Navigation
-
-- [Event listeners and interactions](./listeners.md)
-- [interaction overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/interaction/custom-interactions.md b/docs/agents/remix/interaction/custom-interactions.md
deleted file mode 100644
index c47f8546..00000000
--- a/docs/agents/remix/interaction/custom-interactions.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# Custom interactions and typed targets
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Custom interactions
-
-Define semantic interactions that can dispatch custom events and be reused
-declaratively.
-
-```ts
-import { defineInteraction, on, type Interaction } from '@remix-run/interaction'
-
-// Provide type safety for consumers
-declare global {
- interface HTMLElementEventMap {
- [keydownEnter]: KeyboardEvent
- }
-}
-
-function KeydownEnter(handle: Interaction) {
- if (!(handle.target instanceof HTMLElement)) return
-
- handle.on(handle.target, {
- keydown(event) {
- if (event.key === 'Enter') {
- handle.target.dispatchEvent(
- new KeyboardEvent(keydownEnter, { key: 'Enter' }),
- )
- }
- },
- })
-}
-
-// define the interaction type and setup function
-const keydownEnter = defineInteraction('keydown:enter', KeydownEnter)
-
-// usage
-let button = document.createElement('button')
-on(button, {
- [keydownEnter](event) {
- console.log('Enter key pressed')
- },
-})
-```
-
-Notes:
-
-- An interaction is initialized at most once per target, even if multiple
- listeners bind the same interaction type.
-
-## Typed event targets
-
-Use `TypedEventTarget` to get type-safe `addEventListener` and
-integrate with this library's `on` helpers.
-
-```ts
-import { TypedEventTarget, on } from '@remix-run/interaction'
-
-interface DrummerEventMap {
- kick: DrummerEvent
- snare: DrummerEvent
- hat: DrummerEvent
-}
-
-class DrummerEvent extends Event {
- constructor(type: keyof DrummerEventMap) {
- super(type)
- }
-}
-
-class Drummer extends TypedEventTarget {
- kick() {
- // ...
- this.dispatchEvent(new DrummerEvent('kick'))
- }
-}
-
-let drummer = new Drummer()
-
-// native API is NOT typed
-drummer.addEventListener('kick', (event) => {
- // event is DrummerEvent
-})
-
-// type safe with on()
-on(drummer, {
- kick: (event) => {
- // event is Dispatched
- },
-})
-```
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/interaction/demos)
-contains working demos:
-
-- [`demos/async`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/async) -
- Async listeners with abort signal
-- [`demos/basic`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/basic) -
- Basic event handling
-- [`demos/form`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/form) -
- Form event handling
-- [`demos/keys`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/keys) -
- Keyboard interactions
-- [`demos/popover`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/popover) -
- Popover interactions
-- [`demos/press`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/press) -
- Press and long press interactions
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [interaction overview](./index.md)
-- [Event listeners and interactions](./listeners.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/interaction/index.md b/docs/agents/remix/interaction/index.md
deleted file mode 100644
index 1d5d08ba..00000000
--- a/docs/agents/remix/interaction/index.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# interaction
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Overview
-
-Enhanced events and custom interactions for any
-[EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget).
-
-## Features
-
-- **Declarative Bindings** - Event bindings with plain objects
-- **Semantic Interactions** - Reusable "interactions" like `longPress` and
- `arrowDown`
-- **Async Support** - Listeners with reentry protection via
- [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
-- **Type Safety** - Type-safe listeners and custom `EventTarget` subclasses with
- `TypedEventTarget`
-
-## Installation
-
-```sh
-bun add @remix-run/interaction
-```
-
-## Quick start
-
-```ts
-import { on } from '@remix-run/interaction'
-
-let inputElement = document.createElement('input')
-
-on(inputElement, {
- input: (event, signal) => {
- console.log('current value', event.currentTarget.value)
- },
-})
-```
-
-## Navigation
-
-- [Event listeners and interactions](./listeners.md)
-- [Containers and disposal](./containers-and-disposal.md)
-- [Custom interactions and typed targets](./custom-interactions.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/interaction/listeners.md b/docs/agents/remix/interaction/listeners.md
deleted file mode 100644
index 970cdfb1..00000000
--- a/docs/agents/remix/interaction/listeners.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Event listeners and interactions
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Adding event listeners
-
-Use `on(target, listeners)` to add one or more listeners. Each listener receives
-`(event, signal)` where `signal` is aborted on reentry.
-
-```ts
-import { on } from '@remix-run/interaction'
-
-let inputElement = document.createElement('input')
-
-on(inputElement, {
- input: (event, signal) => {
- console.log('current value', event.currentTarget.value)
- },
-})
-```
-
-Listeners can be arrays. They run in order and preserve normal DOM semantics
-(including `stopImmediatePropagation`).
-
-```ts
-import { on } from '@remix-run/interaction'
-
-on(inputElement, {
- input: [
- (event) => {
- console.log('first')
- },
- {
- capture: true,
- listener(event) {
- // capture phase
- },
- },
- {
- once: true,
- listener(event) {
- console.log('only once')
- },
- },
- ],
-})
-```
-
-## Built-in interactions
-
-Builtin interactions are higher-level, semantic event types (e.g., `press`,
-`longPress`, arrow keys) exported as string constants. Consume them just like
-native events by using computed keys in your listener map. When you bind one,
-the necessary underlying host events are set up automatically.
-
-```tsx
-import { on } from '@remix-run/interaction'
-import { press, longPress } from '@remix-run/interaction/press'
-
-on(listItem, {
- [press](event) {
- navigateTo(listItem.href)
- },
-
- [longPress](event) {
- event.preventDefault() // prevents `press`
- showActions()
- },
-})
-```
-
-Import builtins from their modules (for example, `@remix-run/interaction/press`,
-`@remix-run/interaction/keys`). Some interactions may coordinate with others
-(for example, calling `event.preventDefault()` in one listener can prevent a
-related interaction from firing).
-
-## Async listeners and reentry protection
-
-The `signal` is aborted when the same listener is re-entered (for example, a
-user types quickly and triggers `input` repeatedly). Pass it to async APIs or
-check it manually to avoid stale work.
-
-```ts
-on(inputElement, {
- async input(event, signal) {
- showSearchSpinner()
-
- // Abortable fetch
- let res = await fetch(`/search?q=${event.currentTarget.value}`, { signal })
- let results = await res.json()
- updateResults(results)
- },
-})
-```
-
-For APIs that don't accept a signal:
-
-```ts
-on(inputElement, {
- async input(event, signal) {
- showSearchSpinner()
- let results = await someSearch(event.currentTarget.value)
- if (signal.aborted) return
- updateResults(results)
- },
-})
-```
-
-## Event listener options
-
-All DOM
-[`AddEventListenerOptions`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options)
-are supported via descriptors:
-
-```ts
-import { on } from '@remix-run/interaction'
-
-on(button, {
- click: {
- capture: true,
- listener(event) {
- console.log('capture phase')
- },
- },
- focus: {
- once: true,
- listener(event) {
- console.log('focused once')
- },
- },
-})
-```
-
-## Navigation
-
-- [Containers and disposal](./containers-and-disposal.md)
-- [interaction overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/lazy-file.md b/docs/agents/remix/lazy-file.md
deleted file mode 100644
index d4162c32..00000000
--- a/docs/agents/remix/lazy-file.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# lazy-file
-
-Source: https://github.com/remix-run/remix/tree/main/packages/lazy-file
-
-## README
-
-`lazy-file` is a lazy, streaming `Blob`/`File` implementation for JavaScript.
-
-It allows you to easily create
-[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)-like and
-[File](https://developer.mozilla.org/en-US/docs/Web/API/File)-like objects that
-defer reading their contents until needed, which is ideal for situations where a
-file's contents do not fit in memory all at once. When file contents are read,
-they are streamed to avoid buffering.
-
-## Features
-
-- **Deferred Loading** - Blob/file contents loaded on demand to minimize memory
- usage
-- **Familiar Interface** - `LazyBlob` and `LazyFile` implement the same
- interface as native `Blob` and `File`
-- **Easy Conversion** - Convert to native `ReadableStream` with `.stream()`, or
- to native `Blob`/`File` with `.toBlob()` and `.toFile()`
-- **Standard Constructors** - Accepts all the same content types as the original
- [`Blob()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob) and
- [`File()`](https://developer.mozilla.org/en-US/docs/Web/API/File/File)
- constructors
-- **Slice Support** - Supports
- [`Blob.slice()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice),
- even on streaming content
-
-## The Problem
-
-JavaScript's [File API](https://developer.mozilla.org/en-US/docs/Web/API/File)
-is useful, but it's not a great fit for streaming server environments where you
-don't want to buffer file contents. In particular,
-[`the File() constructor`](https://developer.mozilla.org/en-US/docs/Web/API/File/File)
-requires the contents of a file to be supplied up front when the object is first
-created, like this:
-
-```ts
-let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
-```
-
-A `LazyFile` improves this model by accepting an additional content type in its
-constructor: `LazyContent`.
-
-```ts
-let lazyContent: LazyContent = {
- /* See below for usage */
-}
-let lazyFile = new LazyFile(lazyContent, 'hello.txt', { type: 'text/plain' })
-```
-
-All other `File` functionality works as you'd expect.
-
-## Installation
-
-Install from [npm](https://www.npmjs.com/):
-
-```sh
-bun add @remix-run/lazy-file
-```
-
-## Usage
-
-The low-level API can be used to create a `LazyFile` that streams content from
-anywhere:
-
-```ts
-import { type LazyContent, LazyFile } from '@remix-run/lazy-file'
-
-let content: LazyContent = {
- // The total length of this file in bytes.
- byteLength: 100000,
- // A function that provides a stream of data for the file contents,
- // beginning at the `start` index and ending at `end`.
- stream(start, end) {
- // ... read the file contents from somewhere and return a ReadableStream
- return new ReadableStream({
- start(controller) {
- controller.enqueue('X'.repeat(100000).slice(start, end))
- controller.close()
- },
- })
- },
-}
-
-let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })
-await lazyFile.arrayBuffer() // ArrayBuffer of the file's content
-lazyFile.name // "example.txt"
-lazyFile.type // "text/plain"
-```
-
-All file contents are read on-demand and nothing is ever buffered unless you
-explicitly call `.toFile()` or `.toBlob()`.
-
-### Streaming Content
-
-Use `.stream()` to get a `ReadableStream` for `Response` and other streaming
-APIs:
-
-```ts
-import { openLazyFile } from '@remix-run/fs'
-
-let lazyFile = openLazyFile('./large-video.mp4')
-
-let response = new Response(lazyFile.stream(), {
- headers: {
- 'Content-Type': lazyFile.type,
- 'Content-Length': String(lazyFile.size),
- },
-})
-```
-
-### Converting to Native File/Blob
-
-For non-streaming APIs that require a complete `File` or `Blob` (e.g.
-`FormData`), use `.toFile()` or `.toBlob()`.
-
-```ts
-let lazyFile = openLazyFile('./document.pdf')
-let realFile = await lazyFile.toFile()
-
-let formData = new FormData()
-formData.append('document', realFile)
-```
-
-> **Note:** `.toFile()` and `.toBlob()` read the entire file into memory. Only
-> use these for non-streaming APIs that require a complete `File` or `Blob`
-> (e.g. `FormData`). Always prefer `.stream()` if possible.
-
-## Related Packages
-
-- [`fs`](https://github.com/remix-run/remix/tree/main/packages/fs) - Filesystem
- utilities for reading and writing files using the Web `File` API
-- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) -
- Storage abstraction for files on disk or in memory
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/logger-middleware.md b/docs/agents/remix/logger-middleware.md
deleted file mode 100644
index 04555554..00000000
--- a/docs/agents/remix/logger-middleware.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# logger-middleware
-
-Source: https://github.com/remix-run/remix/tree/main/packages/logger-middleware
-
-## README
-
-Middleware for logging HTTP requests and responses for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-Logs information about HTTP requests and responses with customizable formatting.
-
-## Installation
-
-```sh
-bun add @remix-run/logger-middleware
-```
-
-## Usage
-
-```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { logger } from '@remix-run/logger-middleware'
-
-let router = createRouter({
- middleware: [logger()],
-})
-
-// Logs: [19/Nov/2025:14:32:10 -0800] GET /users/123 200 1234
-```
-
-### Custom Format
-
-You can use the `format` option to customize the log format. The following
-tokens are available:
-
-- `%date` - Date and time in Apache/nginx format (dd/Mon/yyyy:HH:mm:ss +/-zzzz)
-- `%dateISO` - Date and time in ISO format
-- `%duration` - Request duration in milliseconds
-- `%contentLength` - Response Content-Length header
-- `%contentType` - Response Content-Type header
-- `%host` - Request URL host
-- `%hostname` - Request URL hostname
-- `%method` - Request method
-- `%path` - Request pathname + search
-- `%pathname` - Request pathname
-- `%port` - Request port
-- `%query` - Request query string (search)
-- `%referer` - Request Referer header
-- `%search` - Request search string
-- `%status` - Response status code
-- `%statusText` - Response status text
-- `%url` - Full request URL
-- `%userAgent` - Request User-Agent header
-
-```ts
-let router = createRouter({
- middleware: [
- logger({
- format: '%method %path - %status (%duration ms)',
- }),
- ],
-})
-// Logs: GET /users/123 - 200 (42 ms)
-```
-
-For Apache-style combined log format, you can use the following format:
-
-```ts
-let router = createRouter({
- middleware: [
- logger({
- format:
- '%host - - [%date] "%method %path" %status %contentLength "%referer" "%userAgent"',
- }),
- ],
-})
-```
-
-### Custom Logger
-
-You can use a custom logger to write logs to a file or other stream.
-
-```ts
-import { createWriteStream } from 'node:fs'
-
-let logStream = createWriteStream('access.log', { flags: 'a' })
-
-let router = createRouter({
- middleware: [
- logger({
- log(message) {
- logStream.write(message + '\n')
- },
- }),
- ],
-})
-```
-
-## Related Packages
-
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router for the web Fetch API
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/method-override-middleware.md b/docs/agents/remix/method-override-middleware.md
deleted file mode 100644
index 86fed5fd..00000000
--- a/docs/agents/remix/method-override-middleware.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# method-override-middleware
-
-Source:
-https://github.com/remix-run/remix/tree/main/packages/method-override-middleware
-
-## README
-
-Middleware for overriding HTTP request methods from form data for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-Allows HTML forms (which only support GET and POST) to submit with other HTTP
-methods like PUT, PATCH, or DELETE by including a special form field.
-
-## Installation
-
-```sh
-bun add @remix-run/method-override-middleware
-```
-
-## Usage
-
-This middleware runs after
-[the `formData` middleware](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware)
-and updates the request context's `context.method` with the value of the method
-override field. This is useful for simulating RESTful API request methods like
-PUT and DELETE using HTML forms.
-
-```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { formData } from '@remix-run/form-data-middleware'
-import { methodOverride } from '@remix-run/method-override-middleware'
-
-let router = createRouter({
- // methodOverride must come AFTER formData middleware
- middleware: [formData(), methodOverride()],
-})
-
-router.delete('/users/:id', async (context) => {
- let userId = context.params.id
- // Delete user logic...
- return new Response('User deleted')
-})
-```
-
-In your HTML form:
-
-```html
-
-```
-
-### Custom Field Name
-
-You can customize the name of the method override field by passing a `fieldName`
-option to the `methodOverride()` middleware.
-
-```ts
-let router = createRouter({
- middleware: [formData(), methodOverride({ fieldName: '__method__' })],
-})
-```
-
-```html
-
-```
-
-## Related Packages
-
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router for the web Fetch API
-- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) -
- Required for parsing form data
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/mime.md b/docs/agents/remix/mime.md
deleted file mode 100644
index 81ce6fd6..00000000
--- a/docs/agents/remix/mime.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# @remix-run/mime
-
-Source: https://github.com/remix-run/remix/tree/main/packages/mime
-
-## README
-
-Utilities for working with MIME types.
-
-Data used for these utilities is generated at build time from
-[mime-db](https://github.com/jshttp/mime-db).
-
-## Installation
-
-```bash
-bun add @remix-run/mime
-```
-
-## Usage
-
-### `detectMimeType(extension)`
-
-Detects the MIME type for a given file extension or filename.
-
-```ts
-import { detectMimeType } from '@remix-run/mime'
-
-detectMimeType('txt') // 'text/plain'
-detectMimeType('.txt') // 'text/plain'
-detectMimeType('file.txt') // 'text/plain'
-detectMimeType('path/to/file.txt') // 'text/plain'
-detectMimeType('unknown') // undefined
-```
-
-### `detectContentType(extension)`
-
-Detects the Content-Type header value for a given file extension or filename,
-including `charset` for text-based types. See
-[`mimeTypeToContentType`](#mimetypetocontenttypemimetype) for charset logic.
-
-```ts
-import { detectContentType } from '@remix-run/mime'
-
-detectContentType('css') // 'text/css; charset=utf-8'
-detectContentType('.json') // 'application/json; charset=utf-8'
-detectContentType('image.png') // 'image/png'
-detectContentType('path/to/file.unknown') // undefined
-```
-
-### `isCompressibleMimeType(mimeType)`
-
-Checks if a MIME type is known to be compressible.
-
-```ts
-import { isCompressibleMimeType } from '@remix-run/mime'
-
-isCompressibleMimeType('text/html') // true
-isCompressibleMimeType('application/json') // true
-isCompressibleMimeType('image/png') // false
-isCompressibleMimeType('video/mp4') // false
-```
-
-For convenience, the function also accepts a full Content-Type header value:
-
-```ts
-import { isCompressibleMimeType } from '@remix-run/mime'
-
-isCompressibleMimeType('text/html; charset=utf-8') // true
-isCompressibleMimeType('application/json; charset=utf-8') // true
-isCompressibleMimeType('image/png; charset=utf-8') // false
-isCompressibleMimeType('video/mp4; charset=utf-8') // false
-```
-
-### `mimeTypeToContentType(mimeType)`
-
-Converts a MIME type to a Content-Type header value, adding `; charset=utf-8` to
-text-based MIME types: `text/*` (except `text/xml` which has built-in encoding
-declarations), `application/json`, `application/javascript`, and all `+json`
-suffixed types. All other types are returned unchanged.
-
-```ts
-import { mimeTypeToContentType } from '@remix-run/mime'
-
-mimeTypeToContentType('text/css') // 'text/css; charset=utf-8'
-mimeTypeToContentType('application/json') // 'application/json; charset=utf-8'
-mimeTypeToContentType('application/ld+json') // 'application/ld+json; charset=utf-8'
-mimeTypeToContentType('image/png') // 'image/png'
-```
-
-### `defineMimeType(definition)`
-
-Registers or overrides a MIME type for one or more file extensions.
-
-```ts
-import { defineMimeType } from '@remix-run/mime'
-
-defineMimeType({
- extensions: ['myformat'],
- mimeType: 'application/x-myformat',
-})
-```
-
-You can also optionally configure the charset and whether the MIME type is
-compressible:
-
-```ts
-defineMimeType({
- extensions: ['myformat'],
- mimeType: 'application/x-myformat',
- compressible: true,
- charset: 'utf-8',
-})
-```
-
-## License
-
-MIT
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/multipart-parser/benchmarks.md b/docs/agents/remix/multipart-parser/benchmarks.md
deleted file mode 100644
index 2603635b..00000000
--- a/docs/agents/remix/multipart-parser/benchmarks.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Benchmarks and related packages
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
-contains working demos:
-
-- [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) -
- using multipart-parser in Bun
-- [`demos/cf-workers`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/cf-workers) -
- using multipart-parser in a Cloudflare Worker and storing file uploads in R2
-- [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) -
- using multipart-parser in Deno
-- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) -
- using multipart-parser in Node.js
-
-## Benchmark
-
-`multipart-parser` is designed to be as efficient as possible, operating on
-streams of data and rarely buffering in common usage. This design yields
-exceptional performance when handling multipart payloads of any size. In
-benchmarks, `multipart-parser` is as fast or faster than `busboy`.
-
-The results of running the benchmarks on my laptop:
-
-```
-> @remix-run/multipart-parser@0.10.1 bench:node /Users/michael/Projects/remix-the-web/packages/multipart-parser
-> node --disable-warning=ExperimentalWarning ./bench/runner.ts
-
-Platform: Darwin (24.5.0)
-CPU: Apple M1 Pro
-Date: 6/13/2025, 12:27:09 PM
-Node.js v24.0.2
-(index) | 1 small file | 1 large file | 100 small files | 5 large files
-multipart-parser | '0.01 ms +/- 0.03' | '1.08 ms +/- 0.08' | '0.04 ms +/- 0.01' | '10.50 ms +/- 0.38'
-multipasta | '0.02 ms +/- 0.06' | '1.07 ms +/- 0.02' | '0.15 ms +/- 0.02' | '10.46 ms +/- 0.11'
-busboy | '0.06 ms +/- 0.17' | '3.07 ms +/- 0.24' | '0.24 ms +/- 0.05' | '29.85 ms +/- 0.18'
-@fastify/busboy | '0.05 ms +/- 0.13' | '1.23 ms +/- 0.09' | '0.45 ms +/- 0.22' | '11.81 ms +/- 0.11'
-
-> @remix-run/multipart-parser@0.10.1 bench:bun /Users/michael/Projects/remix-the-web/packages/multipart-parser
-> bun run ./bench/runner.ts
-
-Platform: Darwin (24.5.0)
-CPU: Apple M1 Pro
-Date: 6/13/2025, 12:27:31 PM
-Bun 1.2.13
-(index) | 1 small file | 1 large file | 100 small files | 5 large files
-multipart-parser | 0.01 ms +/- 0.04 | 0.86 ms +/- 0.09 | 0.04 ms +/- 0.01 | 8.32 ms +/- 0.26
-multipasta | 0.02 ms +/- 0.07 | 0.87 ms +/- 0.03 | 0.25 ms +/- 0.21 | 8.27 ms +/- 0.09
-busboy | 0.05 ms +/- 0.17 | 3.54 ms +/- 0.10 | 0.30 ms +/- 0.03 | 34.79 ms +/- 0.38
-@fastify/busboy | 0.06 ms +/- 0.18 | 4.04 ms +/- 0.08 | 0.48 ms +/- 0.06 | 39.91 ms +/- 0.37
-
-> @remix-run/multipart-parser@0.10.1 bench:deno /Users/michael/Projects/remix-the-web/packages/multipart-parser
-> deno run --allow-sys ./bench/runner.ts
-
-Platform: Darwin (24.5.0)
-CPU: Apple M1 Pro
-Date: 6/13/2025, 12:28:12 PM
-Deno 2.3.6
-(idx) | 1 small file | 1 large file | 100 small files | 5 large files
-multipart-parser | "0.01 ms +/- 0.03" | "1.03 ms +/- 0.04" | "0.05 ms +/- 0.01" | "10.05 ms +/- 0.20"
-multipasta | "0.02 ms +/- 0.07" | "1.04 ms +/- 0.03" | "0.16 ms +/- 0.02" | "10.10 ms +/- 0.08"
-busboy | "0.05 ms +/- 0.19" | "3.06 ms +/- 0.15" | "0.32 ms +/- 0.05" | "29.92 ms +/- 0.24"
-@fastify/busboy | "0.06 ms +/- 0.14" | "14.72 ms +/- 11.42" | "0.81 ms +/- 0.20" | "127.63 ms +/- 35.77"
-```
-
-## Related packages
-
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
- Uses `multipart-parser` internally to parse multipart requests and generate
- `FileUpload`s for storage
-- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) -
- Used internally to parse HTTP headers and get metadata (filename, content
- type) for each `MultipartPart`
-
-## Credits
-
-Thanks to Jacob Ebey who gave me several code reviews on this project prior to
-publishing.
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [multipart-parser overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/multipart-parser/index.md b/docs/agents/remix/multipart-parser/index.md
deleted file mode 100644
index b71de1a7..00000000
--- a/docs/agents/remix/multipart-parser/index.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# multipart-parser
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Overview
-
-`multipart-parser` is a fast, streaming multipart parser that works in any
-JavaScript environment. Whether you're handling file uploads, parsing email
-attachments, or working with multipart API responses, `multipart-parser` has you
-covered.
-
-## Why multipart-parser?
-
-- **Universal JavaScript** - One library that works everywhere: Node.js, Bun,
- Deno, Cloudflare Workers, and browsers
-- **Blazing Fast** - Outperforms popular alternatives like busboy in benchmarks
-- **Zero Dependencies** - Lightweight and secure with no external dependencies
-- **Memory Efficient** - Streaming architecture that yields files as they are
- found in the stream
-- **Type Safe** - Written in TypeScript with comprehensive type definitions
-- **Standards Based** - Built on the web Streams API for maximum compatibility
-- **Production Ready** - Battle-tested error handling with specific error types
-
-## Installation
-
-```sh
-bun add @remix-run/multipart-parser
-```
-
-## Usage
-
-The most common use case is handling file uploads when you're building a web
-server. The `parseMultipartRequest` function validates the request, extracts the
-multipart boundary from the `Content-Type` header, parses all fields and files
-in the `request.body` stream, and gives each one to you as a `MultipartPart`
-object.
-
-```ts
-import {
- MultipartParseError,
- parseMultipartRequest,
-} from '@remix-run/multipart-parser'
-
-async function handleRequest(request: Request): void {
- try {
- for await (let part of parseMultipartRequest(request)) {
- if (part.isFile) {
- // Access file data in multiple formats
- let buffer = part.arrayBuffer // ArrayBuffer
- console.log(
- `File received: ${part.filename} (${buffer.byteLength} bytes)`,
- )
- console.log(`Content type: ${part.mediaType}`)
- console.log(`Field name: ${part.name}`)
-
- // Save to disk, upload to cloud storage, etc.
- await saveFile(part.filename, part.bytes)
- } else {
- let text = part.text // string
- console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
- }
- }
- } catch (error) {
- if (error instanceof MultipartParseError) {
- console.error('Failed to parse multipart request:', error.message)
- } else {
- console.error('An unexpected error occurred:', error)
- }
- }
-}
-```
-
-## Navigation
-
-- [Limits and Node bindings](./limits-and-node.md)
-- [Low-level APIs](./low-level.md)
-- [Benchmarks and related packages](./benchmarks.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/multipart-parser/limits-and-node.md b/docs/agents/remix/multipart-parser/limits-and-node.md
deleted file mode 100644
index 7d2dcc8b..00000000
--- a/docs/agents/remix/multipart-parser/limits-and-node.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Limits and Node bindings
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Limiting file upload size
-
-You can set a file upload size limit using the `maxFileSize` option, and return
-a 413 "Payload Too Large" response when you receive a request that exceeds the
-limit.
-
-```ts
-import {
- MultipartParseError,
- MaxFileSizeExceededError,
- parseMultipartRequest,
-} from '@remix-run/multipart-parser/node'
-
-const oneMb = Math.pow(2, 20)
-const maxFileSize = 10 * oneMb
-
-async function handleRequest(request: Request): Promise {
- try {
- for await (let part of parseMultipartRequest(request, { maxFileSize })) {
- // ...
- }
- } catch (error) {
- if (error instanceof MaxFileSizeExceededError) {
- return new Response('File size limit exceeded', { status: 413 })
- } else if (error instanceof MultipartParseError) {
- return new Response('Failed to parse multipart request', { status: 400 })
- } else {
- console.error(error)
- return new Response('Internal Server Error', { status: 500 })
- }
- }
-}
-```
-
-## Node.js bindings
-
-The main module (`import from "@remix-run/multipart-parser"`) assumes you're
-working with the Fetch API (`Request`, `ReadableStream`, etc). Support for these
-interfaces was added to Node.js by the undici project in version 16.5.0.
-
-If you're building a server for Node.js that relies on node-specific APIs like
-`http.IncomingMessage`, `stream.Readable`, and `buffer.Buffer`,
-`multipart-parser` ships with an additional module that works directly with
-these APIs.
-
-```ts
-import * as http from 'node:http'
-import {
- MultipartParseError,
- parseMultipartRequest,
-} from '@remix-run/multipart-parser/node'
-
-let server = http.createServer(async (req, res) => {
- try {
- for await (let part of parseMultipartRequest(req)) {
- // ...
- }
- } catch (error) {
- if (error instanceof MultipartParseError) {
- console.error('Failed to parse multipart request:', error.message)
- } else {
- console.error('An unexpected error occurred:', error)
- }
- }
-})
-
-server.listen(8080)
-```
-
-## Navigation
-
-- [multipart-parser overview](./index.md)
-- [Low-level APIs](./low-level.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/multipart-parser/low-level.md b/docs/agents/remix/multipart-parser/low-level.md
deleted file mode 100644
index 5df57730..00000000
--- a/docs/agents/remix/multipart-parser/low-level.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Low-level APIs
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Low-level API
-
-If you're working directly with multipart boundaries and buffers/streams of
-multipart data that are not necessarily part of a request, `multipart-parser`
-provides a low-level `parseMultipart()` API that you can use directly:
-
-```ts
-import { parseMultipart } from '@remix-run/multipart-parser'
-
-let message = new Uint8Array(/* ... */)
-let boundary = '----WebKitFormBoundary56eac3x'
-
-for (let part of parseMultipart(message, { boundary })) {
- // ...
-}
-```
-
-In addition, the `parseMultipartStream` function provides an async generator
-interface for multipart data in a `ReadableStream`:
-
-```ts
-import { parseMultipartStream } from '@remix-run/multipart-parser'
-
-let message = new ReadableStream(/* ... */)
-let boundary = '----WebKitFormBoundary56eac3x'
-
-for await (let part of parseMultipartStream(message, { boundary })) {
- // ...
-}
-```
-
-## Navigation
-
-- [multipart-parser overview](./index.md)
-- [Benchmarks and related packages](./benchmarks.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/advanced-usage.md b/docs/agents/remix/node-fetch-server/advanced-usage.md
deleted file mode 100644
index b4f1007a..00000000
--- a/docs/agents/remix/node-fetch-server/advanced-usage.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Advanced usage
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Low-level API
-
-For more control over request/response handling, use the low-level API:
-
-```ts
-import * as http from 'node:http'
-import { createRequest, sendResponse } from '@remix-run/node-fetch-server'
-
-let server = http.createServer(async (req, res) => {
- // Convert Node.js request to Fetch API Request
- let request = createRequest(req, res, { host: process.env.HOST })
-
- try {
- // Add custom headers or middleware logic
- let startTime = Date.now()
-
- // Process the request with your handler
- let response = await handler(request)
-
- // Add response timing header
- let duration = Date.now() - startTime
- response.headers.set('X-Response-Time', `${duration}ms`)
-
- // Send the response
- await sendResponse(res, response)
- } catch (error) {
- console.error('Server error:', error)
- res.writeHead(500, { 'Content-Type': 'text/plain' })
- res.end('Internal Server Error')
- }
-})
-
-server.listen(3000)
-```
-
-The low-level API provides:
-
-- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web
- Request
-- `sendResponse(res, response)` - Sends web Response using Node.js
- ServerResponse
-
-This is useful for:
-
-- Building custom middleware systems
-- Integrating with existing Node.js code
-- Implementing custom error handling
-- Performance-critical applications
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Migration from Express](./migration.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/demos-and-benchmark.md b/docs/agents/remix/node-fetch-server/demos-and-benchmark.md
deleted file mode 100644
index aab88a96..00000000
--- a/docs/agents/remix/node-fetch-server/demos-and-benchmark.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Demos and benchmark
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos)
-contains working demos:
-
-- [`demos/http2`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos/http2) -
- HTTP/2 server with TLS certificates
-
-## Benchmark
-
-To run benchmarks comparing `node-fetch-server` performance with comparable
-libraries:
-
-```sh
-pnpm run bench
-```
-
-## Related packages
-
-- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) -
- Build HTTP proxy servers using the web fetch API
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/index.md b/docs/agents/remix/node-fetch-server/index.md
deleted file mode 100644
index c0690aea..00000000
--- a/docs/agents/remix/node-fetch-server/index.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# node-fetch-server
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Overview
-
-Build portable Node.js servers using web-standard Fetch API primitives.
-
-`node-fetch-server` brings the simplicity and familiarity of the
-[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to
-Node.js server development. Instead of dealing with Node's traditional
-`req`/`res` objects, you work with web-standard
-[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and
-[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
-objects - the same APIs you already use in the browser and modern JavaScript
-runtimes.
-
-## Features
-
-- **Web Standards** - Standard `Request` and `Response` APIs
-- **Drop-in Integration** - Works with `node:http` and `node:https`
-- **Streaming Support** - Response support with `ReadableStream`
-- **Custom Hostname** - Configuration for deployment flexibility
-- **Client Info** - Access to client connection info (IP address, port)
-- **TypeScript** - Full TypeScript support with type definitions
-
-## Installation
-
-```sh
-bun add @remix-run/node-fetch-server
-```
-
-## Navigation
-
-- [Quick start examples](./quick-start.md)
-- [Advanced usage](./advanced-usage.md)
-- [Migration from Express](./migration.md)
-- [Demos and benchmark](./demos-and-benchmark.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/migration.md b/docs/agents/remix/node-fetch-server/migration.md
deleted file mode 100644
index 63dec91f..00000000
--- a/docs/agents/remix/node-fetch-server/migration.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Migration from Express
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Basic routing
-
-```ts
-// Express
-let app = express()
-
-app.get('/users/:id', async (req, res) => {
- let user = await db.getUser(req.params.id)
- if (!user) {
- return res.status(404).json({ error: 'User not found' })
- }
- res.json(user)
-})
-
-app.listen(3000)
-
-// node-fetch-server
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-async function handler(request: Request) {
- let url = new URL(request.url)
- let match = url.pathname.match(/^\\/users\\/(\\w+)$/)
-
- if (match && request.method === 'GET') {
- let user = await db.getUser(match[1])
- if (!user) {
- return Response.json({ error: 'User not found' }, { status: 404 })
- }
- return Response.json(user)
- }
-
- return new Response('Not Found', { status: 404 })
-}
-
-http.createServer(createRequestListener(handler)).listen(3000)
-```
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Demos and benchmark](./demos-and-benchmark.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/quick-start.md b/docs/agents/remix/node-fetch-server/quick-start.md
deleted file mode 100644
index 3ed2e0b5..00000000
--- a/docs/agents/remix/node-fetch-server/quick-start.md
+++ /dev/null
@@ -1,193 +0,0 @@
-# Quick start
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Basic server
-
-```ts
-import * as http from 'node:http'
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-// Example: Simple in-memory user storage
-let users = new Map([
- ['1', { id: '1', name: 'Alice', email: 'alice@example.com' }],
- ['2', { id: '2', name: 'Bob', email: 'bob@example.com' }],
-])
-
-async function handler(request: Request) {
- let url = new URL(request.url)
-
- // GET / - Home page
- if (url.pathname === '/' && request.method === 'GET') {
- return new Response('Welcome to the User API! Try GET /api/users')
- }
-
- // GET /api/users - List all users
- if (url.pathname === '/api/users' && request.method === 'GET') {
- return Response.json(Array.from(users.values()))
- }
-
- // GET /api/users/:id - Get specific user
- let userMatch = url.pathname.match(/^\\/api\\/users\\/(\\w+)$/)
- if (userMatch && request.method === 'GET') {
- let user = users.get(userMatch[1])
- if (user) {
- return Response.json(user)
- }
- return new Response('User not found', { status: 404 })
- }
-
- return new Response('Not Found', { status: 404 })
-}
-
-// Create a standard Node.js server
-let server = http.createServer(createRequestListener(handler))
-
-server.listen(3000, () => {
- console.log('Server running at http://localhost:3000')
-})
-```
-
-## Working with request data
-
-```ts
-async function handler(request: Request) {
- let url = new URL(request.url)
-
- // Handle JSON data
- if (request.method === 'POST' && url.pathname === '/api/users') {
- try {
- let userData = await request.json()
-
- // Validate required fields
- if (!userData.name || !userData.email) {
- return Response.json(
- { error: 'Name and email are required' },
- { status: 400 },
- )
- }
-
- // Create user (your implementation)
- let newUser = {
- id: Date.now().toString(),
- ...userData,
- }
-
- return Response.json(newUser, { status: 201 })
- } catch (error) {
- return Response.json({ error: 'Invalid JSON' }, { status: 400 })
- }
- }
-
- // Handle URL search params
- if (url.pathname === '/api/search') {
- let query = url.searchParams.get('q')
- let limit = parseInt(url.searchParams.get('limit') || '10')
-
- return Response.json({
- query,
- limit,
- results: [], // Your search results here
- })
- }
-
- return new Response('Not Found', { status: 404 })
-}
-```
-
-## Streaming responses
-
-```ts
-async function handler(request: Request) {
- if (request.url.endsWith('/stream')) {
- // Create a streaming response
- let stream = new ReadableStream({
- async start(controller) {
- for (let i = 0; i < 5; i++) {
- controller.enqueue(new TextEncoder().encode(`Chunk ${i}\\n`))
- await new Promise((resolve) => setTimeout(resolve, 1000))
- }
- controller.close()
- },
- })
-
- return new Response(stream, {
- headers: { 'Content-Type': 'text/plain' },
- })
- }
-
- return new Response('Not Found', { status: 404 })
-}
-```
-
-## Custom hostname configuration
-
-```ts
-import * as http from 'node:http'
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-// Use a custom hostname (e.g., from environment variable)
-let hostname = process.env.HOST || 'api.example.com'
-
-async function handler(request: Request) {
- // request.url will now use your custom hostname
- console.log(request.url) // https://api.example.com/path
-
- return Response.json({
- message: 'Hello from custom domain!',
- url: request.url,
- })
-}
-
-let server = http.createServer(
- createRequestListener(handler, { host: hostname }),
-)
-
-server.listen(3000)
-```
-
-## Accessing client information
-
-```ts
-import { type FetchHandler } from '@remix-run/node-fetch-server'
-
-let handler: FetchHandler = async (request, client) => {
- // Log client information
- console.log(`Request from ${client.address}:${client.port}`)
-
- // Use for rate limiting, geolocation, etc.
- if (isRateLimited(client.address)) {
- return new Response('Too Many Requests', { status: 429 })
- }
-
- return Response.json({
- message: 'Hello!',
- yourIp: client.address,
- })
-}
-```
-
-## HTTPS support
-
-```ts
-import * as https from 'node:https'
-import * as fs from 'node:fs'
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-let options = {
- key: fs.readFileSync('private-key.pem'),
- cert: fs.readFileSync('certificate.pem'),
-}
-
-let server = https.createServer(options, createRequestListener(handler))
-
-server.listen(443, () => {
- console.log('HTTPS Server running on port 443')
-})
-```
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Advanced usage](./advanced-usage.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/remix.md b/docs/agents/remix/remix.md
deleted file mode 100644
index 30fc7d9b..00000000
--- a/docs/agents/remix/remix.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# remix
-
-Source: https://github.com/remix-run/remix/tree/main/packages/remix
-
-## README
-
-A modern web framework for JavaScript.
-
-See [remix.run](https://remix.run) for framework docs.
-
-## Installation
-
-```sh
-npm i remix
-```
-
-## Package usage in Remix 3 alpha
-
-The `remix` package is used through subpath imports.
-
-- ✅ `import { createRouter } from 'remix/fetch-router'`
-- ✅ `import { route } from 'remix/fetch-router/routes'`
-- ✅ `import { createRoot } from 'remix/component'`
-- ❌ `import { ... } from 'remix'` (root import removed in `3.0.0-alpha.3`)
-
-## Subpath export surface (`3.0.0-alpha.3`)
-
-Top-level package exports currently include:
-
-- `remix/async-context-middleware`
-- `remix/component`
-- `remix/compression-middleware`
-- `remix/cookie`
-- `remix/data-schema`
-- `remix/data-table`
-- `remix/fetch-proxy`
-- `remix/fetch-router`
-- `remix/file-storage`
-- `remix/file-storage-s3`
-- `remix/form-data-middleware`
-- `remix/form-data-parser`
-- `remix/fs`
-- `remix/headers`
-- `remix/html-template`
-- `remix/interaction`
-- `remix/lazy-file`
-- `remix/logger-middleware`
-- `remix/method-override-middleware`
-- `remix/mime`
-- `remix/multipart-parser`
-- `remix/node-fetch-server`
-- `remix/response`
-- `remix/route-pattern`
-- `remix/session`
-- `remix/session-middleware`
-- `remix/session-storage-memcache`
-- `remix/session-storage-redis`
-- `remix/static-middleware`
-- `remix/tar-parser`
-
-Plus adapter/data helper subpaths and utility subpaths:
-
-- `remix/data-schema/checks`, `remix/data-schema/coerce`,
- `remix/data-schema/lazy`
-- `remix/data-table-mysql`, `remix/data-table-postgres`,
- `remix/data-table-sqlite`
-- `remix/fetch-router/routes`
-- `remix/component/jsx-runtime`, `remix/component/jsx-dev-runtime`,
- `remix/component/server`
-- `remix/interaction/form`, `remix/interaction/keys`,
- `remix/interaction/popover`, `remix/interaction/press`
-- `remix/response/compress`, `remix/response/file`, `remix/response/html`,
- `remix/response/redirect`
-- `remix/route-pattern/specificity`
-- `remix/session/cookie-storage`, `remix/session/fs-storage`,
- `remix/session/memory-storage`
-- `remix/file-storage/fs`, `remix/file-storage/memory`
-- `remix/multipart-parser/node`
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/response/compress-responses.md b/docs/agents/remix/response/compress-responses.md
deleted file mode 100644
index 9573fdb2..00000000
--- a/docs/agents/remix/response/compress-responses.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Compressed responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `compressResponse` helper compresses a `Response` based on the client's
-`Accept-Encoding` header:
-
-```ts
-import { compressResponse } from '@remix-run/response/compress'
-
-let response = new Response(JSON.stringify(data), {
- headers: { 'Content-Type': 'application/json' },
-})
-let compressed = await compressResponse(response, request)
-```
-
-Compression is automatically skipped for:
-
-- Responses with no `Accept-Encoding` header
-- Responses that are already compressed (existing `Content-Encoding`)
-- Responses with `Cache-Control: no-transform`
-- Responses with `Content-Length` below threshold (default: 1024 bytes)
-- Responses with range support (`Accept-Ranges: bytes`)
-- 206 Partial Content responses
-- HEAD requests (only headers are modified)
-
-## Options
-
-```ts
-await compressResponse(response, request, {
- // Minimum size in bytes to compress (only enforced if Content-Length is present).
- // Default: 1024
- threshold: 1024,
-
- // Which encodings the server supports for negotiation.
- // Defaults to ['br', 'gzip', 'deflate']
- encodings: ['br', 'gzip', 'deflate'],
-
- // node:zlib options for gzip/deflate compression.
- // For SSE responses (text/event-stream), flush: Z_SYNC_FLUSH
- // is automatically applied unless you explicitly set a flush value.
- // See: https://nodejs.org/api/zlib.html#class-options
- zlib: {
- level: 6,
- },
-
- // node:zlib options for Brotli compression.
- // For SSE responses (text/event-stream), flush: BROTLI_OPERATION_FLUSH
- // is automatically applied unless you explicitly set a flush value.
- // See: https://nodejs.org/api/zlib.html#class-brotlioptions
- brotli: {
- params: {
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
- },
- },
-})
-```
-
-## Range requests and compression
-
-Range requests and compression are mutually exclusive. When
-`Accept-Ranges: bytes` is present in the response headers, `compressResponse`
-will not compress the response. This is why the `createFileResponse` helper
-enables ranges only for non-compressible MIME types by default - to allow
-text-based assets to be compressed while still supporting resumable downloads
-for media files.
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/file-responses.md b/docs/agents/remix/response/file-responses.md
deleted file mode 100644
index 43d7ddc5..00000000
--- a/docs/agents/remix/response/file-responses.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# File responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `createFileResponse` helper creates a response for serving files with full
-HTTP semantics. It works with both native `File` objects and `LazyFile` from
-`@remix-run/lazy-file`.
-
-```ts
-import { createFileResponse } from '@remix-run/response/file'
-import { openLazyFile } from '@remix-run/fs'
-
-let lazyFile = openLazyFile('./public/image.jpg')
-let response = await createFileResponse(lazyFile, request, {
- cacheControl: 'public, max-age=3600',
-})
-```
-
-## Features
-
-- **Content-Type** and **Content-Length** headers
-- **ETag** generation (weak or strong)
-- **Last-Modified** headers
-- **Cache-Control** headers
-- **Conditional requests** (`If-None-Match`, `If-Modified-Since`, `If-Match`,
- `If-Unmodified-Since`)
-- **Range requests** for partial content (`206 Partial Content`)
-- **HEAD** request support
-
-## Options
-
-```ts
-await createFileResponse(file, request, {
- // Cache-Control header value.
- // Defaults to `undefined` (no Cache-Control header).
- cacheControl: 'public, max-age=3600',
-
- // ETag generation strategy:
- // - 'weak': Generates weak ETags based on file size and mtime (default)
- // - 'strong': Generates strong ETags by hashing file content
- // - false: Disables ETag generation
- etag: 'weak',
-
- // Hash algorithm for strong ETags (Web Crypto API algorithm names).
- // Only used when etag: 'strong'.
- // Defaults to 'SHA-256'.
- digest: 'SHA-256',
-
- // Whether to generate Last-Modified headers.
- // Defaults to `true`.
- lastModified: true,
-
- // Whether to support HTTP Range requests for partial content.
- // Defaults to `true`.
- acceptRanges: true,
-})
-```
-
-## Strong ETags and content hashing
-
-For assets that require strong validation (e.g., to support
-[`If-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match)
-preconditions or
-[`If-Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range)
-with
-[`Range` requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)),
-configure strong ETag generation:
-
-```ts
-return createFileResponse(file, request, {
- etag: 'strong',
-})
-```
-
-By default, strong ETags are generated using the Web Crypto API with the
-`'SHA-256'` algorithm. You can customize this:
-
-```ts
-return createFileResponse(file, request, {
- etag: 'strong',
- // Specify a different hash algorithm
- digest: 'SHA-512',
-})
-```
-
-For large files or custom hashing requirements, provide a custom digest
-function:
-
-```ts
-await createFileResponse(file, request, {
- etag: 'strong',
- async digest(file) {
- // Custom streaming hash for large files
- let { createHash } = await import('node:crypto')
- let hash = createHash('sha256')
- for await (let chunk of file.stream()) {
- hash.update(chunk)
- }
- return hash.digest('hex')
- },
-})
-```
-
-## Navigation
-
-- [Response overview](./index.md)
-- [HTML responses](./html-responses.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/html-responses.md b/docs/agents/remix/response/html-responses.md
deleted file mode 100644
index 27ff0f04..00000000
--- a/docs/agents/remix/response/html-responses.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# HTML responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `createHtmlResponse` helper creates HTML responses with proper
-`Content-Type` and DOCTYPE handling:
-
-```ts
-import { createHtmlResponse } from '@remix-run/response/html'
-
-let response = createHtmlResponse('
-```
-
-The helper automatically prepends `` if not already present. It
-works with strings, `SafeHtml` from `@remix-run/html-template`, Blobs/Files,
-ArrayBuffers, and ReadableStreams.
-
-```ts
-import { html } from '@remix-run/html-template'
-import { createHtmlResponse } from '@remix-run/response/html'
-
-let name = ''
-let response = createHtmlResponse(html`
Hello, ${name}!
`)
-// Safely escaped HTML
-```
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Redirect responses](./redirect-responses.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/index.md b/docs/agents/remix/response/index.md
deleted file mode 100644
index eb5fedcb..00000000
--- a/docs/agents/remix/response/index.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# response
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-## Overview
-
-Response helpers for the web Fetch API. `response` provides a collection of
-helper functions for creating common HTTP responses with proper headers and
-semantics.
-
-## Features
-
-- **Web Standards Compliant:** Built on the standard `Response` API, works in
- any JavaScript runtime (Node.js, Bun, Deno, Cloudflare Workers)
-- **File Responses:** Full HTTP semantics including ETags, Last-Modified,
- conditional requests, and Range support
-- **HTML Responses:** Automatic DOCTYPE prepending and proper Content-Type
- headers
-- **Redirect Responses:** Simple redirect creation with customizable status
- codes
-- **Compress Responses:** Streaming compression based on Accept-Encoding header
-
-## Installation
-
-```sh
-bun add @remix-run/response
-```
-
-## Usage
-
-This package provides no default export. Instead, import the specific helper you
-need:
-
-```ts
-import { createFileResponse } from '@remix-run/response/file'
-import { createHtmlResponse } from '@remix-run/response/html'
-import { createRedirectResponse } from '@remix-run/response/redirect'
-import { compressResponse } from '@remix-run/response/compress'
-```
-
-## Navigation
-
-- [File responses](./file-responses.md)
-- [HTML responses](./html-responses.md)
-- [Redirect responses](./redirect-responses.md)
-- [Compressed responses](./compress-responses.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/redirect-responses.md b/docs/agents/remix/response/redirect-responses.md
deleted file mode 100644
index f5f58f54..00000000
--- a/docs/agents/remix/response/redirect-responses.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Redirect responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `createRedirectResponse` helper creates redirect responses. The main
-improvements over the native `Response.redirect` API are:
-
-- Accepts a relative `location` instead of a full URL.
-- Accepts a `ResponseInit` object as the second argument, allowing you to set
- additional headers and status code.
-
-```ts
-import { createRedirectResponse } from '@remix-run/response/redirect'
-
-// Default 302 redirect
-let response = createRedirectResponse('/login')
-
-// Custom status code
-let response = createRedirectResponse('/new-page', 301)
-
-// With additional headers
-let response = createRedirectResponse('/dashboard', {
- status: 303,
- headers: { 'X-Redirect-Reason': 'authentication' },
-})
-```
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Compressed responses](./compress-responses.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/related.md b/docs/agents/remix/response/related.md
deleted file mode 100644
index 9e1eabf4..00000000
--- a/docs/agents/remix/response/related.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Related packages
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-## Related packages
-
-- [`@remix-run/headers`](https://github.com/remix-run/remix/tree/main/packages/headers) -
- Type-safe HTTP header manipulation
-- [`@remix-run/html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template) -
- Safe HTML templating with automatic escaping
-- [`@remix-run/fs`](https://github.com/remix-run/remix/tree/main/packages/fs) -
- File system utilities including `openFile`
-- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Build HTTP routers using the web fetch API
-- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) -
- MIME type utilities
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/route-pattern.md b/docs/agents/remix/route-pattern.md
deleted file mode 100644
index a8fafa97..00000000
--- a/docs/agents/remix/route-pattern.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# route-pattern
-
-Source: https://github.com/remix-run/remix/tree/main/packages/route-pattern
-
-## README
-
-Fast URL matching and href generation with type safe params.
-
-```ts
-import { RoutePattern } from '@remix-run/route-pattern'
-
-let blog = new RoutePattern('blog/:slug')
-blog.match('https://remix.run/blog/v3') // { params: { slug: 'v3' } }
-blog.href({ slug: 'v3' }) // '/blog/v3'
-
-let api = new RoutePattern('api(/v:version)/*path')
-api.match('https://api.com/api/v2/users/profile') // { params: { version: '2', path: 'users/profile' } }
-api.href({ version: '2', path: 'users/profile' }) // '/api/v2/users/profile'
-api.href({ path: 'users/profile' }) // '/api/users/profile'
-
-let cdn = new RoutePattern('http(s)://:region.cdn.com/assets/*file.:ext')
-cdn.match('https://us-west.cdn.com/assets/images/logo.png') // { params: { region: 'us-west', file: 'images/logo', ext: 'png' } }
-cdn.href({ region: 'us-west', file: 'images/logo', ext: 'png' }) // 'https://us-west.cdn.com/assets/images/logo.png'
-```
-
-**Goals**
-
-- **Universal**: Runs on any JS runtime (Node, Bun, Deno, Cloudflare Workers,
- browsers, ...)
-- **Type-safe params**: Autocomplete and validation for variables, wildcards,
- and search params
-- **Full URL matching**: Protocol, hostname, port, pathname, search params
-- **Fast**: Includes matchers optimized for small and large apps
-- **Simple ranking**: Static segments beat variables, variables beat wildcards
-
-## Installation
-
-```sh
-bun add @remix-run/route-pattern
-```
-
-## Intuitive syntax
-
-**Variables** capture dynamic segments using `:name`:
-
-```ts
-new RoutePattern('users/:id') // matches /users/123
-new RoutePattern('blog/:year-:month-:day/:slug') // matches /blog/2024-01-15/hello
-```
-
-**Wildcards** match multi-segment paths using `*name`:
-
-```ts
-new RoutePattern('files/*path') // matches /files/images/logo.png
-new RoutePattern('node_modules/*package/dist/index.js') // matches /node_modules/@remix-run/router/dist/index.js
-new RoutePattern('files/*') // matches any path under /files, but doesn't capture the value for the wildcard
-```
-
-**Optionals** make parts optional using `()`:
-
-```ts
-new RoutePattern('api(/v:version)/users') // matches /api/users AND /api/v2/users
-new RoutePattern('blog/:slug(.html)') // matches /blog/hello AND /blog/hello.html
-new RoutePattern('docs(/guides/:category)') // multiple segments optional: /docs OR /docs/guides/routing
-new RoutePattern('api(/v:major(.:minor))') // nested optionals: /api, /api/v2, /api/v2.1
-```
-
-**Search params** narrow matches using `?key` or `?key=value`:
-
-```ts
-new RoutePattern('search?q') // requires ?q in URL
-new RoutePattern('search?q=') // requires ?q with any value
-new RoutePattern('search?q=routing') // requires ?q=routing exactly
-```
-
-**Flexible matching** for partial URL patterns:
-
-```ts
-new RoutePattern('blog/:slug') // omits protocol/hostname, matches any origin
-new RoutePattern('://example.com/api') // omits protocol, matches http and https
-new RoutePattern('search?q') // allows additional search params beyond ?q
-```
-
-## Matchers
-
-Match URLs against multiple patterns. Each pattern can have associated data
-(handlers, route IDs, metadata, etc.):
-
-```ts
-import { ArrayMatcher as Matcher } from '@remix-run/route-pattern'
-
-// Any data type you want!
-let matcher = new Matcher()
-
-matcher.add('/', 'home')
-matcher.add('blog/:slug', 'blog-post')
-matcher.add('api(/v:version)/*path', 'api')
-
-matcher.match('https://example.com/blog/v3')
-// { pattern: 'blog/:slug', params: { slug: 'v3' }, data: 'blog-post' }
-
-matcher.match('https://example.com/api/v2/users/profile')
-// { pattern: 'api(/v:version)/*path', params: { version: '2', path: 'users/profile' }, data: 'api' }
-```
-
-**ArrayMatcher vs TrieMatcher**
-
-- **ArrayMatcher**: Best for small apps (~80 routes or fewer)
-- **TrieMatcher**: Best for large apps (hundreds of routes)
-
-Note: Performance depends on your specific patterns - benchmark both to verify
-which is faster for your app.
-
-Both implement the `Matcher` API so you can swap them out easily:
-
-```ts
-// import { ArrayMatcher as Matcher } from "@remix-run/route-pattern"
-import { TrieMatcher as Matcher } from '@remix-run/route-pattern'
-```
-
-## Specificity
-
-When multiple patterns match a URL, the most specific pattern wins.
-
-**Pathname specificity** (left-to-right):
-
-```ts
-import { ArrayMatcher } from '@remix-run/route-pattern'
-
-let matcher = new ArrayMatcher()
-matcher.add('blog/hello', 'static')
-matcher.add('blog/:slug', 'variable')
-matcher.add('blog/*path', 'wildcard')
-matcher.add('*path', 'catch-all')
-
-matcher.match('https://example.com/blog/hello')
-// { pattern: 'blog/hello', params: {}, data: 'static' }
-// 'blog/hello' wins: static segments beat variables/wildcards at each position
-```
-
-**Search parameter specificity**:
-
-```ts
-let router = new ArrayMatcher()
-router.add('search', 'no-params')
-router.add('search?q', 'has-q')
-router.add('search?q=', 'has-q-with-value')
-router.add('search?q=hello', 'exact-match')
-
-router.match('https://example.com/search?q=hello')
-// { pattern: 'search?q=hello', params: {}, data: 'exact-match' }
-// More constrained search params = more specific
-```
-
-## Benchmark
-
-To run benchmarks comparing `route-pattern` performance with comparable
-libraries:
-
-```sh
-pnpm bench bench/comparison.bench.ts
-```
-
-## Related Work
-
-- [`path-to-regexp`](https://www.npmjs.com/package/path-to-regexp)
-- [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session-middleware.md b/docs/agents/remix/session-middleware.md
deleted file mode 100644
index 70a1d766..00000000
--- a/docs/agents/remix/session-middleware.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# session-middleware
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session-middleware
-
-## README
-
-Middleware for managing sessions with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router)
-via securely signed cookies.
-
-## Installation
-
-```sh
-bun add @remix-run/session-middleware
-```
-
-## Usage
-
-```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { createCookie } from '@remix-run/cookie'
-import { createCookieSessionStorage } from '@remix-run/session/cookie-storage'
-import { session } from '@remix-run/session-middleware'
-
-let sessionCookie = createCookie('__session', {
- secrets: ['s3cr3t'], // session cookies must be signed!
- httpOnly: true,
- secure: true,
- sameSite: 'lax',
-})
-
-let sessionStorage = createCookieSessionStorage()
-
-let router = createRouter({
- middleware: [session(sessionCookie, sessionStorage)],
-})
-
-router.get('/', (context) => {
- context.session.set('count', Number(context.session.get('count') ?? 0) + 1)
- return new Response(`Count: ${context.session.get('count')}`)
-})
-```
-
-The middleware:
-
-- Reads the session from the cookie on incoming requests
-- Makes it available as `context.session`
-- Automatically saves session changes and sets the cookie on responses
-
-Note: The session cookie must be signed for security. This prevents tampering
-with the session data on the client.
-
-### Login/Logout Flow
-
-A basic login/logout flow could look like this:
-
-```ts
-import * as res from '@remix-run/fetch-router/response-helpers'
-
-router.get('/login', ({ session }) => {
- let error = session.get('error')
- return res.html(`
-
-
-
Login
- ${typeof error === 'string' ?
${error}
: null}
-
-
-
- `)
-})
-
-router.post('/login', ({ session, formData }) => {
- let username = formData.get('username')
- let password = formData.get('password')
-
- let user = authenticateUser(username, password)
- if (!user) {
- session.flash('error', 'Invalid username or password')
- return res.redirect('/login')
- }
-
- session.regenerateId()
- session.set('userId', user.id)
-
- return res.redirect('/dashboard')
-})
-
-router.post('/logout', ({ session }) => {
- session.destroy()
- return res.redirect('/')
-})
-```
-
-## Related Packages
-
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router for the web Fetch API
-- [`session`](https://github.com/remix-run/remix/tree/main/packages/session) -
- Session management and storage
-- [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) -
- Cookie parsing and serialization
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session-storage-memcache.md b/docs/agents/remix/session-storage-memcache.md
deleted file mode 100644
index c2f8a4bf..00000000
--- a/docs/agents/remix/session-storage-memcache.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# session-storage-memcache
-
-Source:
-https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache
-
-## README
-
-Memcache session storage for `remix/session`.
-
-## Installation
-
-```sh
-npm i remix
-```
-
-## Usage
-
-```ts
-import { createMemcacheSessionStorage } from 'remix/session-storage-memcache'
-
-let sessionStorage = createMemcacheSessionStorage('127.0.0.1:11211', {
- keyPrefix: 'my-app:session:',
- ttlSeconds: 60 * 60 * 24 * 7,
-})
-```
-
-## Options
-
-- `useUnknownIds` (`boolean`, default: `false`)
-- `keyPrefix` (`string`, default: `'remix:session:'`)
-- `ttlSeconds` (`number`, default: `0`)
-
-Memcache storage uses TCP sockets and therefore requires a Node.js runtime.
-
-## Related packages
-
-- [`session`](https://github.com/remix-run/remix/tree/main/packages/session)
-- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware)
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session-storage-redis.md b/docs/agents/remix/session-storage-redis.md
deleted file mode 100644
index 96534c44..00000000
--- a/docs/agents/remix/session-storage-redis.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# session-storage-redis
-
-Source:
-https://github.com/remix-run/remix/tree/main/packages/session-storage-redis
-
-## README
-
-Redis-backed session storage for `remix/session`.
-
-## Installation
-
-```sh
-npm i remix redis
-```
-
-## Usage
-
-```ts
-import { createClient } from 'redis'
-import { createRedisSessionStorage } from 'remix/session-storage-redis'
-
-let redis = createClient({ url: process.env.REDIS_URL })
-await redis.connect()
-
-let sessionStorage = createRedisSessionStorage(redis, {
- keyPrefix: 'session:',
- ttl: 60 * 60 * 24,
-})
-```
-
-## Options
-
-- `keyPrefix` (`string`, default: `'session:'`)
-- `ttl` (`number` in seconds)
-- `useUnknownIds` (`boolean`, default: `false`)
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session/flash-and-security.md b/docs/agents/remix/session/flash-and-security.md
deleted file mode 100644
index 0f71be43..00000000
--- a/docs/agents/remix/session/flash-and-security.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Flash data and security
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-## Flash messages
-
-Flash messages are values that persist only for the next request, perfect for
-displaying one-time notifications:
-
-```ts
-async function requestIndex(cookie: string | null) {
- let session = await storage.read(cookie)
- return { session, cookie: await storage.save(session) }
-}
-
-async function requestSubmit(cookie: string | null) {
- let session = await storage.read(cookie)
- session.flash('message', 'success!')
- return { session, cookie: await storage.save(session) }
-}
-
-// Flash data is undefined on the first request
-let response1 = await requestIndex(null)
-assert.equal(response1.session.get('message'), undefined)
-
-// Flash data is undefined on the same request it is set. This response
-// is typically a redirect to a route that displays the flash data.
-let response2 = await requestSubmit(response1.cookie)
-assert.equal(response2.session.get('message'), undefined)
-
-// Flash data is available on the next request
-let response3 = await requestIndex(response2.cookie)
-assert.equal(response3.session.get('message'), 'success!')
-
-// Flash data is not available on subsequent requests
-let response4 = await requestIndex(response3.cookie)
-assert.equal(response4.session.get('message'), undefined)
-```
-
-## Regenerating session IDs
-
-For security, regenerate the session ID after privilege changes like a login.
-This helps prevent session fixation attacks by issuing a new session ID in the
-response.
-
-```ts
-import { createFsSessionStorage } from '@remix-run/session/fs-storage'
-
-let sessionStorage = createFsSessionStorage('/tmp/sessions')
-
-async function requestIndex(cookie: string | null) {
- let session = await sessionStorage.read(cookie)
- return { session, cookie: await sessionStorage.save(session) }
-}
-
-async function requestLogin(cookie: string | null) {
- let session = await sessionStorage.read(cookie)
- session.set('userId', 'mj')
- session.regenerateId()
- return { session, cookie: await sessionStorage.save(session) }
-}
-
-let response1 = await requestIndex(null)
-assert.equal(response1.session.get('userId'), undefined)
-
-let response2 = await requestLogin(response1.cookie)
-assert.notEqual(response2.session.id, response1.session.id)
-
-let response3 = await requestIndex(response2.cookie)
-assert.equal(response3.session.get('userId'), 'mj')
-```
-
-To delete the old session data when the session is saved, use
-`session.regenerateId(true)`. This can help to prevent session fixation attacks
-by deleting the old session data when the session is saved. However, it may not
-be desirable in a situation with mobile clients on flaky connections that may
-need to resume the session using an old session ID.
-
-## Destroying sessions
-
-When a user logs out, you should destroy the session using `session.destroy()`.
-
-This will clear all session data from storage the next time it is saved. It also
-clears the session ID on the client in the next response, so it will start with
-a new session on the next request.
-
-## Navigation
-
-- [Session overview](./index.md)
-- [Storage strategies](./storage-strategies.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/session/index.md b/docs/agents/remix/session/index.md
deleted file mode 100644
index 6e758ad1..00000000
--- a/docs/agents/remix/session/index.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# session
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-## Overview
-
-A full-featured session management library for JavaScript. This package provides
-a flexible and secure way to manage user sessions in server-side applications
-with a flexible API for different session storage strategies.
-
-## Features
-
-- **Multiple Storage Strategies:** Includes memory, cookie, and file-based
- storage strategies for different use cases
-- **Flash Messages:** Support for flash data that persists only for the next
- request
-- **Session Security:** Built-in protection against session fixation attacks
-
-## Installation
-
-```sh
-bun add @remix-run/session
-```
-
-## Usage
-
-The standard pattern is to read the session from the request, modify it, and
-save it back to storage and write the session cookie to the response.
-
-```ts
-import { createCookieSessionStorage } from '@remix-run/session/cookie-storage'
-
-// Create a session storage. This is used to store session data across requests.
-let storage = createCookieSessionStorage()
-
-// This function simulates a typical request flow where the session is read from
-// the request cookie, modified, and the new cookie is returned in the response.
-async function handleRequest(cookie: string | null) {
- let session = await storage.read(cookie)
- session.set('count', Number(session.get('count') ?? 0) + 1)
- return {
- session, // The session data from this "request"
- cookie: await storage.save(session), // The cookie to use on the next request
- }
-}
-
-let response1 = await handleRequest(null)
-assert.equal(response1.session.get('count'), 1)
-
-let response2 = await handleRequest(response1.cookie)
-assert.equal(response2.session.get('count'), 2)
-
-let response3 = await handleRequest(response2.cookie)
-assert.equal(response3.session.get('count'), 3)
-```
-
-The example above is a low-level illustration of how to use this package for
-session management. In practice, you would use the `session` middleware in
-[`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router)
-to automatically manage the session for you.
-
-## Navigation
-
-- [Flash data and security](./flash-and-security.md)
-- [Storage strategies](./storage-strategies.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/session/related.md b/docs/agents/remix/session/related.md
deleted file mode 100644
index 932ef560..00000000
--- a/docs/agents/remix/session/related.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Related packages
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-## Related packages
-
-- [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) -
- Cookie parsing and serialization
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router with built-in session middleware
-- [`session-storage-memcache`](https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache) -
- Memcache-backed session storage adapter
-- [`session-storage-redis`](https://github.com/remix-run/remix/tree/main/packages/session-storage-redis) -
- Redis-backed session storage adapter
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Session overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/session/storage-strategies.md b/docs/agents/remix/session/storage-strategies.md
deleted file mode 100644
index 2347c11b..00000000
--- a/docs/agents/remix/session/storage-strategies.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Storage strategies
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-Several strategies are provided out of the box for storing session data across
-requests, depending on your needs.
-
-A session storage object must always be initialized with a signed session
-cookie. This is used to identify the session and to store the session data in
-the response.
-
-## Filesystem storage
-
-Filesystem storage is a good choice for production environments. It requires
-access to a persistent filesystem, which is readily available on most servers.
-And it can scale to handle sessions with a lot of data easily.
-
-```ts
-import { createFsSessionStorage } from '@remix-run/session/fs-storage'
-
-let sessionStorage = createFsSessionStorage('/tmp/sessions')
-```
-
-## Cookie storage
-
-Cookie storage is suitable for production environments. In this strategy, all
-session data is stored directly in the session cookie itself, which means it
-doesn't require any additional storage.
-
-The main limitation of cookie storage is that the total size of the session
-cookie is limited to the browser's maximum cookie size, typically 4096 bytes.
-
-```ts
-import { createCookieSessionStorage } from '@remix-run/session/cookie-storage'
-
-let sessionStorage = createCookieSessionStorage()
-```
-
-## Memory storage
-
-Memory storage is useful in testing and development environments. In this
-strategy, all session data is stored in memory, which means no additional
-storage is required. However, all session data is lost when the server restarts.
-
-```ts
-import { createMemorySessionStorage } from '@remix-run/session/memory-storage'
-
-let sessionStorage = createMemorySessionStorage()
-```
-
-## Navigation
-
-- [Session overview](./index.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/static-middleware.md b/docs/agents/remix/static-middleware.md
deleted file mode 100644
index c5f9ac6d..00000000
--- a/docs/agents/remix/static-middleware.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# static-middleware
-
-Source: https://github.com/remix-run/remix/tree/main/packages/static-middleware
-
-## README
-
-Middleware for serving static files from the filesystem for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-Serves static files from a directory with support for ETags, range requests, and
-conditional requests.
-
-## Features
-
-- [ETag support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
- (weak and strong)
-- [Range request support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
- (HTTP 206 Partial Content)
-- [Conditional request support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
- (If-None-Match, If-Modified-Since)
-- Path traversal protection
-- Automatic fall through to next middleware/handler if file not found
-
-## Installation
-
-```sh
-bun add @remix-run/static-middleware
-```
-
-## Usage
-
-Static middleware is useful for serving static files from a directory.
-
-```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { staticFiles } from '@remix-run/static-middleware'
-
-let router = createRouter({
- middleware: [staticFiles('./public')],
-})
-
-router.get('/', () => new Response('Home'))
-```
-
-### With Cache Control
-
-Internally, the `staticFiles()` middleware uses the
-[`createFileResponse()` helper from `@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response/README.md#file-responses)
-to send files with full HTTP semantics. This means it also accepts the same
-options as the `createFileResponse()` helper.
-
-```ts
-let router = createRouter({
- middleware: [
- staticFiles('./public', {
- cacheControl: 'public, max-age=31536000, immutable', // 1 year
- }),
- ],
-})
-```
-
-### Filter Files
-
-```ts
-let router = createRouter({
- middleware: [
- staticFiles('./public', {
- filter(path) {
- // Don't serve hidden files
- return !path.startsWith('.')
- },
- }),
- ],
-})
-```
-
-### Multiple Directories
-
-```ts
-let router = createRouter({
- middleware: [
- staticFiles('./public'),
- staticFiles('./assets', {
- cacheControl: 'public, max-age=31536000',
- }),
- ],
-})
-```
-
-## Security
-
-- Prevents path traversal attacks (e.g., `../../../etc/passwd`)
-- Only serves files with GET and HEAD requests
-- Respects the configured root directory boundary
-
-## Related Packages
-
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router for the web Fetch API
-- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) -
- Used internally for streaming file contents
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/tar-parser.md b/docs/agents/remix/tar-parser.md
deleted file mode 100644
index 9f0b68d2..00000000
--- a/docs/agents/remix/tar-parser.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# tar-parser
-
-Source: https://github.com/remix-run/remix/tree/main/packages/tar-parser
-
-## README
-
-`tar-parser` is a fast, efficient parser for
-[tar archives]().
-
-Tar archives are ubiquitous in software development, used for distributing
-packages, backing up files, and transferring data. Most existing JavaScript tar
-parsers are either Node.js-specific or don't handle streaming efficiently,
-forcing you to buffer entire archives in memory. This makes them unsuitable for
-serverless environments or processing large archives.
-
-`tar-parser` can be used in any JavaScript environment (not just Node.js) and
-processes archives as streams, making it ideal for modern web development across
-all runtimes.
-
-## Features
-
-- **Universal Runtime** - Runs anywhere JavaScript runs
-- **Web Streams** - Built on the standard
- [web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API),
- so it's composable with `fetch()` streams
-- **Format Support** - Supports POSIX, GNU, and PAX tar formats
-- **Memory Efficient** - Does not buffer anything in normal usage
-- **Zero Dependencies** - No external dependencies
-
-## Installation
-
-Install from [npm](https://www.npmjs.com/):
-
-```sh
-bun add @remix-run/tar-parser
-```
-
-## Usage
-
-The main parser interface is the `parseTar(archive, handler)` function:
-
-```ts
-import { parseTar } from '@remix-run/tar-parser'
-
-let response = await fetch(
- 'https://github.com/remix-run/remix/archive/refs/heads/main.tar.gz',
-)
-
-await parseTar(
- response.body.pipeThrough(new DecompressionStream('gzip')),
- (entry) => {
- console.log(entry.name, entry.size)
- },
-)
-```
-
-If you're parsing an archive with filename encodings other than UTF-8, use the
-`filenameEncoding` option:
-
-```ts
-let response = await fetch(/* ... */)
-
-await parseTar(response.body, { filenameEncoding: 'latin1' }, (entry) => {
- console.log(entry.name, entry.size)
-})
-```
-
-## Benchmark
-
-`tar-parser` performs on par with other popular tar parsing libraries on
-Node.js.
-
-```
-> @remix-run/tar-parser@0.0.0 bench /Users/michael/Projects/remix-the-web/packages/tar-parser
-> node --disable-warning=ExperimentalWarning ./bench/runner.ts
-
-Platform: Darwin (24.0.0)
-CPU: Apple M1 Pro
-Date: 12/6/2024, 11:00:55 AM
-Node.js v22.8.0
-(index) | lodash npm package
-tar-parser | '6.23 ms +/- 0.58'
-tar-stream | '6.72 ms +/- 2.24'
-node-tar | '6.49 ms +/- 0.44'
-```
-
-## Related Packages
-
-- [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) -
- Fast, streaming multipart parser for JavaScript
-
-## Credits
-
-`tar-parser` is based on the excellent
-[tar-stream package](https://www.npmjs.com/package/tar-stream) (MIT license) and
-adopts the same core parsing algorithm, utility functions, and many test cases.
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/update.md b/docs/agents/remix/update.md
deleted file mode 100644
index 2528c50e..00000000
--- a/docs/agents/remix/update.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# Updating Remix package docs
-
-Use this checklist to refresh `docs/agents/remix/**/*.md` from upstream.
-
-## 1) List packages
-
-```sh
-gh api repos/remix-run/remix/contents/packages --jq '.[].name'
-```
-
-## 2) Refresh each package README
-
-For each package name, download the README from upstream:
-
-```sh
-curl -L "https://raw.githubusercontent.com/remix-run/remix/main/packages//README.md"
-```
-
-Replace the corresponding README content in docs:
-
-- For single-file packages, update `docs/agents/remix/.md`.
-- For split packages, update the README chunks under
- `docs/agents/remix//`.
-
-Keep each Markdown file to roughly 200 lines or fewer. If a README grows beyond
-that, split it into multiple files and update the package `index.md` to link the
-new chunks.
-
-## 3) Refresh component docs
-
-`component` is the only package with a `docs` directory. Sync every file from:
-
-```
-https://github.com/remix-run/remix/tree/main/packages/component/docs
-```
-
-Update the split files in `docs/agents/remix/component/` to match upstream. Keep
-all docs: `animate`, `components`, `composition`, `context`, `events`,
-`getting-started`, `handle`, `interactions`, `patterns`, `spring`, `styling`,
-`testing`, `tween`. If any single doc exceeds roughly 200 lines, split it into
-multiple files and add links in `component/index.md`.
-
-## 4) Keep the index current
-
-If a package is added or removed upstream, update `docs/agents/remix/index.md`:
-
-- Add/remove package rows in the table.
-- Update the "Start here" section if new docs are important.
-- If a package moves to a folder, update links to `.//index.md`.
-
-## 5) Audit export coverage
-
-Confirm `docs/agents/remix/index.md` package rows still cover all top-level
-exports from the installed `remix` package:
-
-```sh
-bun -e "const fs=require('fs');const pkg=JSON.parse(fs.readFileSync('node_modules/remix/package.json','utf8'));const top=[...new Set(Object.keys(pkg.exports).filter(k=>k!=='./package.json').map(k=>k.slice(2).split('/')[0]))].sort();const idx=fs.readFileSync('docs/agents/remix/index.md','utf8');const docs=[...new Set([...idx.matchAll(/^\\|\\s*([a-z0-9-]+)\\s*\\|/gm)].map(m=>m[1]).filter(x=>!['Package','--------------------------'].includes(x)))].sort();const missing=top.filter(x=>!docs.includes(x));console.log(missing.length===0?'No missing package docs in index.':'Missing docs for: '+missing.join(', '));"
-```
-
-If any package names are missing, add them to `docs/agents/remix/index.md` and
-add the corresponding docs file(s).
-
-## 6) Verify
-
-Run formatting and validation before committing:
-
-```sh
-bun run format
-bun run validate
-```
diff --git a/docs/agents/setup.md b/docs/agents/setup.md
index 8c7dd4f8..ce6562a5 100644
--- a/docs/agents/setup.md
+++ b/docs/agents/setup.md
@@ -167,8 +167,8 @@ If you ever need to do the same operations manually, use:
- `bun tools/ci/preview-resources.ts cleanup --worker-name `
- `bun tools/ci/production-resources.ts ensure --out-config `
-## Remix package docs
+## Remix skill
-Use the Remix package index for quick navigation:
+Use the packaged Remix skill for Remix-specific implementation guidance:
-- `docs/agents/remix/index.md`
+- `.agents/skills/remix/SKILL.md`
diff --git a/mock-servers/ai/db-tables.ts b/mock-servers/ai/db-tables.ts
index 70ba0102..d01b4437 100644
--- a/mock-servers/ai/db-tables.ts
+++ b/mock-servers/ai/db-tables.ts
@@ -1,17 +1,16 @@
-import { createTable } from 'remix/data-table'
-import { number, string } from 'remix/data-schema'
+import { column as c, table } from 'remix/data-table'
-export const aiCapturedRequestsTable = createTable({
+export const aiCapturedRequestsTable = table({
name: 'ai_captured_requests',
columns: {
- id: string(),
- token_hash: string(),
- received_at: number(),
- scenario: string(),
- last_user_message: string(),
- tool_names_json: string(),
- request_json: string(),
- response_text: string(),
+ id: c.text(),
+ token_hash: c.text(),
+ received_at: c.integer(),
+ scenario: c.text(),
+ last_user_message: c.text(),
+ tool_names_json: c.text(),
+ request_json: c.text(),
+ response_text: c.text(),
},
primaryKey: 'id',
})
diff --git a/mock-servers/resend/db-tables.ts b/mock-servers/resend/db-tables.ts
index 2dc050da..18828368 100644
--- a/mock-servers/resend/db-tables.ts
+++ b/mock-servers/resend/db-tables.ts
@@ -1,17 +1,16 @@
-import { createTable } from 'remix/data-table'
-import { number, string } from 'remix/data-schema'
+import { column as c, table } from 'remix/data-table'
-export const resendCapturedEmailsTable = createTable({
+export const resendCapturedEmailsTable = table({
name: 'resend_captured_emails',
columns: {
- id: string(),
- token_hash: string(),
- received_at: number(),
- from_email: string(),
- to_json: string(),
- subject: string(),
- html: string(),
- payload_json: string(),
+ id: c.text(),
+ token_hash: c.text(),
+ received_at: c.integer(),
+ from_email: c.text(),
+ to_json: c.text(),
+ subject: c.text(),
+ html: c.text(),
+ payload_json: c.text(),
},
primaryKey: 'id',
})
diff --git a/package.json b/package.json
index 318e13c4..e91c3463 100644
--- a/package.json
+++ b/package.json
@@ -14,14 +14,14 @@
"scripts": {
"dev": "bun cli.ts",
"dev:client": "concurrently --kill-others-on-fail -n web,mcp-apps -c green,cyan \"bun run dev:client:web\" \"bun run dev:mcp-apps\"",
- "dev:client:web": "esbuild client/entry.tsx --bundle --format=esm --target=es2022 --outdir=public --entry-names=client-entry --chunk-names=assets/[name] --asset-names=assets/[name] --jsx=automatic --jsx-import-source=remix/component --watch",
+ "dev:client:web": "esbuild client/entry.tsx --bundle --format=esm --target=es2022 --outdir=public --entry-names=client-entry --chunk-names=assets/[name] --asset-names=assets/[name] --jsx=automatic --jsx-import-source=remix/ui --watch",
"dev:mcp-apps": "esbuild client/mcp-apps/calculator-widget.ts --bundle --format=esm --target=es2022 --outdir=public/mcp-apps --platform=browser --watch",
"dev:worker": "bun ./wrangler-env.ts dev --local",
"dev:mock-resend": "bun ./wrangler-env.ts d1 migrations apply APP_DB --local --config mock-servers/resend/wrangler.jsonc && bun ./wrangler-env.ts dev --local --config mock-servers/resend/wrangler.jsonc",
"dev:mock-ai": "bun ./wrangler-env.ts d1 migrations apply APP_DB --local --config mock-servers/ai/wrangler.jsonc && bun ./wrangler-env.ts dev --local --config mock-servers/ai/wrangler.jsonc",
"deploy": "bun run build && bun ./wrangler-env.ts deploy",
"build:mcp-apps": "esbuild client/mcp-apps/calculator-widget.ts --bundle --format=esm --target=es2022 --outdir=public/mcp-apps --platform=browser",
- "build:client:web": "esbuild client/entry.tsx --bundle --format=esm --target=es2022 --outdir=public --entry-names=client-entry --chunk-names=assets/[name] --asset-names=assets/[name] --jsx=automatic --jsx-import-source=remix/component",
+ "build:client:web": "esbuild client/entry.tsx --bundle --format=esm --target=es2022 --outdir=public --entry-names=client-entry --chunk-names=assets/[name] --asset-names=assets/[name] --jsx=automatic --jsx-import-source=remix/ui",
"build:client": "bun run build:client:web && bun run build:mcp-apps",
"build": "bun run build:client && bun ./wrangler-env.ts build",
"lint": "oxlint .",
@@ -69,7 +69,7 @@
"@modelcontextprotocol/sdk": "1.26.0",
"agents": "^0.7.6",
"get-port": "^7.1.0",
- "remix": "3.0.0-alpha.3",
+ "remix": "3.0.0-beta.0",
"workers-ai-provider": "^3.1.2",
"zod": "^4.3.6"
}
diff --git a/server/handlers/account.ts b/server/handlers/account.ts
index 6fc3e9b4..907a3078 100644
--- a/server/handlers/account.ts
+++ b/server/handlers/account.ts
@@ -7,7 +7,7 @@ import { type routes } from '#server/routes.ts'
export const account = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (!session) {
diff --git a/server/handlers/auth-handler.test.ts b/server/handlers/auth-handler.test.ts
index f56946ca..e5820da8 100644
--- a/server/handlers/auth-handler.test.ts
+++ b/server/handlers/auth-handler.test.ts
@@ -20,7 +20,7 @@ function createAuthRequest(
const context = new RequestContext(request)
return {
- run: () => handler.action(context),
+ run: () => handler.handler(context),
}
}
diff --git a/server/handlers/auth-page.ts b/server/handlers/auth-page.ts
index 1266aa86..38c2a623 100644
--- a/server/handlers/auth-page.ts
+++ b/server/handlers/auth-page.ts
@@ -12,7 +12,7 @@ function normalizeRedirectTo(value: string | null) {
export function createAuthPageHandler() {
return {
middleware: [],
- async action({ request }: { request: Request }) {
+ async handler({ request }: { request: Request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (session) {
const url = new URL(request.url)
diff --git a/server/handlers/auth.ts b/server/handlers/auth.ts
index edde8955..eaa7eba8 100644
--- a/server/handlers/auth.ts
+++ b/server/handlers/auth.ts
@@ -31,7 +31,7 @@ export function createAuthHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request, url }) {
+ async handler({ request, url }) {
let body: unknown
try {
diff --git a/server/handlers/chat-threads.ts b/server/handlers/chat-threads.ts
index bcd1c21d..41653526 100644
--- a/server/handlers/chat-threads.ts
+++ b/server/handlers/chat-threads.ts
@@ -20,7 +20,7 @@ export function createChatThreadsHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const user = await readAuthenticatedAppUser(request, appEnv as Env)
if (!user) {
return jsonResponse(
@@ -69,7 +69,7 @@ export function createDeleteChatThreadHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const user = await readAuthenticatedAppUser(request, appEnv as Env)
if (!user) {
return jsonResponse(
@@ -122,7 +122,7 @@ export function createUpdateChatThreadHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const user = await readAuthenticatedAppUser(request, appEnv as Env)
if (!user) {
return jsonResponse(
diff --git a/server/handlers/chat.ts b/server/handlers/chat.ts
index 9a67f5cf..1cd48c5a 100644
--- a/server/handlers/chat.ts
+++ b/server/handlers/chat.ts
@@ -7,7 +7,7 @@ import { type routes } from '#server/routes.ts'
export const chat = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (!session) {
diff --git a/server/handlers/health-handler.test.ts b/server/handlers/health-handler.test.ts
index 37b6d7a5..933a41a5 100644
--- a/server/handlers/health-handler.test.ts
+++ b/server/handlers/health-handler.test.ts
@@ -10,7 +10,7 @@ function createHealthRequestContext() {
test('health handler returns ok with null commit SHA when unset', async () => {
const handler = createHealthHandler({ APP_COMMIT_SHA: undefined })
- const response = await handler.action(createHealthRequestContext())
+ const response = await handler.handler(createHealthRequestContext())
expect(response.status).toBe(200)
expect(response.headers.get('Cache-Control')).toBe('no-store')
@@ -23,7 +23,7 @@ test('health handler returns the configured commit SHA', async () => {
APP_COMMIT_SHA: 'f2d82dba4ba50cf2ad3f56f5c88f7b8ef5f97d8e',
})
- const response = await handler.action(createHealthRequestContext())
+ const response = await handler.handler(createHealthRequestContext())
expect(response.status).toBe(200)
expect(response.headers.get('X-App-Commit-Sha')).toBe(
diff --git a/server/handlers/health.ts b/server/handlers/health.ts
index e26f2f03..02cc3fce 100644
--- a/server/handlers/health.ts
+++ b/server/handlers/health.ts
@@ -9,7 +9,7 @@ type HealthEnv = {
export function createHealthHandler(appEnv: HealthEnv) {
return {
middleware: [],
- async action() {
+ async handler() {
const commitSha = appEnv.APP_COMMIT_SHA ?? null
return Response.json(
{ ok: true, commitSha },
diff --git a/server/handlers/home.ts b/server/handlers/home.ts
index 133fe298..d4aeddba 100644
--- a/server/handlers/home.ts
+++ b/server/handlers/home.ts
@@ -5,7 +5,7 @@ import { type routes } from '#server/routes.ts'
export const home = {
middleware: [],
- async action() {
+ async handler() {
return render(Layout({}))
},
} satisfies BuildAction
diff --git a/server/handlers/logout.ts b/server/handlers/logout.ts
index 3bd9be62..57169467 100644
--- a/server/handlers/logout.ts
+++ b/server/handlers/logout.ts
@@ -4,7 +4,7 @@ import { type routes } from '#server/routes.ts'
export const logout = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const cookie = await destroyAuthCookie(isSecureRequest(request))
const location = new URL('/login', request.url)
diff --git a/server/handlers/password-reset.ts b/server/handlers/password-reset.ts
index f275e1f8..e0ebe2d8 100644
--- a/server/handlers/password-reset.ts
+++ b/server/handlers/password-reset.ts
@@ -73,7 +73,7 @@ export function createPasswordResetRequestHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request, url }) {
+ async handler({ request, url }) {
let body: unknown
try {
body = await request.json()
@@ -192,7 +192,7 @@ export function createPasswordResetConfirmHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request, url }) {
+ async handler({ request, url }) {
let body: unknown
try {
body = await request.json()
diff --git a/server/handlers/session-handler.test.ts b/server/handlers/session-handler.test.ts
index 99c9b984..0956be67 100644
--- a/server/handlers/session-handler.test.ts
+++ b/server/handlers/session-handler.test.ts
@@ -48,7 +48,7 @@ test('session handler renews remembered sessions after two weeks', async () => {
)
const response = await withMockedNow(now, () =>
- session.action(createSessionRequestContext(cookie)),
+ session.handler(createSessionRequestContext(cookie)),
)
expect(response.status).toBe(200)
@@ -68,7 +68,7 @@ test('session handler keeps remembered sessions unchanged before renewal window'
)
const response = await withMockedNow(now, () =>
- session.action(createSessionRequestContext(cookie)),
+ session.handler(createSessionRequestContext(cookie)),
)
expect(response.status).toBe(200)
diff --git a/server/handlers/session.ts b/server/handlers/session.ts
index 34c47b3b..2c04398f 100644
--- a/server/handlers/session.ts
+++ b/server/handlers/session.ts
@@ -15,7 +15,7 @@ function jsonResponse(data: unknown, init?: ResponseInit) {
export const session = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (!session) {
return jsonResponse({ ok: false })
diff --git a/types/tsconfig-client.json b/types/tsconfig-client.json
index b9518f9b..74e53515 100644
--- a/types/tsconfig-client.json
+++ b/types/tsconfig-client.json
@@ -7,7 +7,7 @@
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": [],
"jsx": "react-jsx",
- "jsxImportSource": "remix/component"
+ "jsxImportSource": "remix/ui"
},
"include": ["../client/**/*.ts", "../client/**/*.tsx", "../shared/**/*.ts"]
}
diff --git a/worker/d1-data-table-adapter.ts b/worker/d1-data-table-adapter.ts
index 4b61be60..30c93444 100644
--- a/worker/d1-data-table-adapter.ts
+++ b/worker/d1-data-table-adapter.ts
@@ -2,9 +2,15 @@ import {
getTableName,
getTablePrimaryKey,
type AdapterCapabilityOverrides,
- type AdapterExecuteRequest,
- type AdapterResult,
- type AdapterStatement,
+ type AdapterCapabilities,
+ type DataManipulationOperation,
+ type DataManipulationRequest,
+ type DataManipulationResult,
+ type DataMigrationOperation,
+ type DataMigrationRequest,
+ type DataMigrationResult,
+ type SqlStatement,
+ type TableRef,
type DatabaseAdapter,
type TransactionOptions,
type TransactionToken,
@@ -48,11 +54,7 @@ type D1PreparedQuery = {
*/
export class D1DataTableAdapter implements DatabaseAdapter {
dialect = 'sqlite'
- capabilities: {
- returning: boolean
- savepoints: boolean
- upsert: boolean
- }
+ capabilities: AdapterCapabilities
#database: D1Database
#transactions = new Set()
@@ -69,51 +71,73 @@ export class D1DataTableAdapter implements DatabaseAdapter {
returning: options?.capabilities?.returning ?? true,
savepoints: options?.capabilities?.savepoints ?? false,
upsert: options?.capabilities?.upsert ?? true,
+ transactionalDdl: options?.capabilities?.transactionalDdl ?? false,
+ migrationLock: options?.capabilities?.migrationLock ?? false,
}
}
- async execute(request: AdapterExecuteRequest): Promise {
+ compileSql(
+ operation: DataManipulationOperation | DataMigrationOperation,
+ ): Array {
+ const statement =
+ operation.kind === 'raw' ||
+ operation.kind === 'select' ||
+ operation.kind === 'count' ||
+ operation.kind === 'exists' ||
+ operation.kind === 'insert' ||
+ operation.kind === 'insertMany' ||
+ operation.kind === 'update' ||
+ operation.kind === 'delete' ||
+ operation.kind === 'upsert'
+ ? compileSqliteStatement(operation)
+ : compileSqliteMigrationStatement(operation)
+ return [{ text: statement.text, values: statement.values }]
+ }
+
+ async execute(
+ request: DataManipulationRequest,
+ ): Promise {
if (
- request.statement.kind === 'insertMany' &&
- request.statement.values.length === 0
+ request.operation.kind === 'insertMany' &&
+ request.operation.values.length === 0
) {
return {
affectedRows: 0,
insertId: undefined,
- rows: request.statement.returning ? [] : undefined,
+ rows: request.operation.returning ? [] : undefined,
}
}
- const statement = compileSqliteStatement(request.statement)
+ const statement = compileSqliteStatement(request.operation)
const prepared = this.#database
.prepare(statement.text)
.bind(...statement.values) as unknown as D1PreparedQuery
const shouldReadRows =
- request.statement.kind === 'select' ||
- request.statement.kind === 'count' ||
- request.statement.kind === 'exists' ||
- hasReturningClause(request.statement)
+ request.operation.kind === 'select' ||
+ request.operation.kind === 'count' ||
+ request.operation.kind === 'exists' ||
+ hasReturningClause(request.operation)
if (shouldReadRows) {
const result = (await prepared.all()) as D1StatementResult
let rows = normalizeRows(result.results ?? [])
if (
- request.statement.kind === 'count' ||
- request.statement.kind === 'exists'
+ request.operation.kind === 'count' ||
+ request.operation.kind === 'exists'
) {
rows = normalizeCountRows(rows)
}
return {
rows,
affectedRows: normalizeAffectedRowsForReader(
- request.statement.kind,
+ request.operation.kind,
rows,
result.meta,
),
insertId: normalizeInsertIdForReader(
- request.statement.kind,
- request.statement,
+ request.operation.kind,
+ request.operation,
rows,
result.meta,
),
@@ -122,15 +146,43 @@ export class D1DataTableAdapter implements DatabaseAdapter {
const result = (await prepared.run()) as D1StatementResult
return {
- affectedRows: normalizeAffectedRowsForRun(request.statement.kind, result),
+ affectedRows: normalizeAffectedRowsForRun(request.operation.kind, result),
insertId: normalizeInsertIdForRun(
- request.statement.kind,
- request.statement,
+ request.operation.kind,
+ request.operation,
result,
),
}
}
+ async migrate(request: DataMigrationRequest): Promise {
+ const statements = this.compileSql(request.operation)
+ for (const statement of statements) {
+ await this.#database
+ .prepare(statement.text)
+ .bind(...statement.values)
+ .run()
+ }
+ return { affectedOperations: 1 }
+ }
+
+ async hasTable(table: TableRef): Promise {
+ const result = await this.#database
+ .prepare(
+ "select 1 from sqlite_master where type = 'table' and name = ? limit 1",
+ )
+ .bind(table.name)
+ .all()
+ return Boolean(result.results?.length)
+ }
+
+ async hasColumn(table: TableRef, column: string): Promise {
+ const result = await this.#database
+ .prepare(`pragma table_info(${quotePath(table.name)})`)
+ .all<{ name?: string }>()
+ return (result.results ?? []).some((field) => field.name === column)
+ }
+
async beginTransaction(
options?: TransactionOptions,
): Promise {
@@ -194,7 +246,7 @@ export function createD1DataTableAdapter(
return new D1DataTableAdapter(database, options)
}
-function hasReturningClause(statement: AdapterStatement) {
+function hasReturningClause(statement: DataManipulationOperation) {
return (
(statement.kind === 'insert' ||
statement.kind === 'insertMany' ||
@@ -237,7 +289,7 @@ function normalizeCountRows(rows: Array>) {
}
function normalizeAffectedRowsForReader(
- kind: AdapterStatement['kind'],
+ kind: DataManipulationOperation['kind'],
rows: Array>,
meta?: D1Meta,
) {
@@ -251,8 +303,8 @@ function normalizeAffectedRowsForReader(
}
function normalizeInsertIdForReader(
- kind: AdapterStatement['kind'],
- statement: AdapterStatement,
+ kind: DataManipulationOperation['kind'],
+ statement: DataManipulationOperation,
rows: Array>,
meta?: D1Meta,
) {
@@ -272,7 +324,7 @@ function normalizeInsertIdForReader(
}
function normalizeAffectedRowsForRun(
- kind: AdapterStatement['kind'],
+ kind: DataManipulationOperation['kind'],
result: D1StatementResult,
) {
if (kind === 'select' || kind === 'count' || kind === 'exists') {
@@ -282,8 +334,8 @@ function normalizeAffectedRowsForRun(
}
function normalizeInsertIdForRun(
- kind: AdapterStatement['kind'],
- statement: AdapterStatement,
+ kind: DataManipulationOperation['kind'],
+ statement: DataManipulationOperation,
result: D1StatementResult,
) {
if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
@@ -295,7 +347,7 @@ function normalizeInsertIdForRun(
return result.meta?.last_row_id
}
-function isWriteStatementKind(kind: AdapterStatement['kind']) {
+function isWriteStatementKind(kind: DataManipulationOperation['kind']) {
return (
kind === 'insert' ||
kind === 'insertMany' ||
@@ -305,14 +357,14 @@ function isWriteStatementKind(kind: AdapterStatement['kind']) {
)
}
-function isInsertStatementKind(kind: AdapterStatement['kind']) {
+function isInsertStatementKind(kind: DataManipulationOperation['kind']) {
return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
}
function isInsertStatement(
- statement: AdapterStatement,
+ statement: DataManipulationOperation,
): statement is Extract<
- AdapterStatement,
+ DataManipulationOperation,
{ kind: 'insert' | 'insertMany' | 'upsert' }
> {
return (
@@ -327,7 +379,7 @@ function isInsertStatement(
* adapter self-contained without depending on internal package paths.
*/
function compileSqliteStatement(
- statement: AdapterStatement,
+ statement: DataManipulationOperation,
): CompiledSqlStatement {
if (statement.kind === 'raw') {
return {
@@ -451,10 +503,138 @@ function compileSqliteStatement(
throw new Error('Unsupported statement kind')
}
+function compileSqliteMigrationStatement(
+ operation: DataMigrationRequest['operation'],
+): CompiledSqlStatement {
+ if (operation.kind === 'raw') {
+ return {
+ text: operation.sql.text,
+ values: [...operation.sql.values],
+ }
+ }
+
+ if (operation.kind === 'createTable') {
+ const columnDefinitions = Object.entries(operation.columns).map(
+ ([name, definition]) =>
+ quotePath(name) + ' ' + compileColumnDefinition(definition),
+ )
+ const constraints: Array = []
+ if (operation.primaryKey) {
+ const columns = operation.primaryKey.columns
+ .map((column) => quotePath(column))
+ .join(', ')
+ const name = operation.primaryKey.name
+ ? 'constraint ' + quotePath(operation.primaryKey.name) + ' '
+ : ''
+ constraints.push(name + 'primary key (' + columns + ')')
+ }
+ for (const unique of operation.uniques ?? []) {
+ const columns = unique.columns
+ .map((column) => quotePath(column))
+ .join(', ')
+ const name = unique.name
+ ? 'constraint ' + quotePath(unique.name) + ' '
+ : ''
+ constraints.push(name + 'unique (' + columns + ')')
+ }
+ return {
+ text:
+ 'create table ' +
+ (operation.ifNotExists ? 'if not exists ' : '') +
+ quoteTableRef(operation.table) +
+ ' (' +
+ [...columnDefinitions, ...constraints].join(', ') +
+ ')',
+ values: [],
+ }
+ }
+
+ if (operation.kind === 'dropTable') {
+ return {
+ text:
+ 'drop table ' +
+ (operation.ifExists ? 'if exists ' : '') +
+ quoteTableRef(operation.table),
+ values: [],
+ }
+ }
+
+ if (operation.kind === 'createIndex') {
+ const index = operation.index
+ const unique = index.unique ? 'unique ' : ''
+ const columns = index.columns.map((column) => quotePath(column)).join(', ')
+ const where = index.where ? ' where ' + index.where : ''
+ return {
+ text:
+ 'create ' +
+ unique +
+ 'index ' +
+ (operation.ifNotExists ? 'if not exists ' : '') +
+ quotePath(index.name) +
+ ' on ' +
+ quoteTableRef(index.table) +
+ ' (' +
+ columns +
+ ')' +
+ where,
+ values: [],
+ }
+ }
+
+ if (operation.kind === 'dropIndex') {
+ return {
+ text:
+ 'drop index ' +
+ (operation.ifExists ? 'if exists ' : '') +
+ quotePath(operation.name),
+ values: [],
+ }
+ }
+
+ if (operation.kind === 'alterTable') {
+ if (operation.changes.length !== 1) {
+ throw new Error('D1 adapter supports one alter-table change at a time')
+ }
+ const change = operation.changes[0]
+ if (!change) {
+ throw new Error('alterTable requires at least one change')
+ }
+ if (change.kind === 'addColumn') {
+ return {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' add column ' +
+ quotePath(change.column) +
+ ' ' +
+ compileColumnDefinition(change.definition),
+ values: [],
+ }
+ }
+ if (change.kind === 'renameColumn') {
+ return {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' rename column ' +
+ quotePath(change.from) +
+ ' to ' +
+ quotePath(change.to),
+ values: [],
+ }
+ }
+ }
+
+ throw new Error('Unsupported D1 migration operation: ' + operation.kind)
+}
+
function compileInsertStatement(
- table: Extract['table'],
+ table: Extract['table'],
values: Record,
- returning: Extract['returning'],
+ returning: Extract<
+ DataManipulationOperation,
+ { kind: 'insert' }
+ >['returning'],
context: SqliteCompileContext,
): CompiledSqlStatement {
const columns = Object.keys(values)
@@ -484,9 +664,12 @@ function compileInsertStatement(
}
function compileInsertManyStatement(
- table: Extract['table'],
+ table: Extract['table'],
rows: Array>,
- returning: Extract['returning'],
+ returning: Extract<
+ DataManipulationOperation,
+ { kind: 'insertMany' }
+ >['returning'],
context: SqliteCompileContext,
): CompiledSqlStatement {
if (rows.length === 0) {
@@ -536,7 +719,7 @@ function compileInsertManyStatement(
}
function compileUpsertStatement(
- statement: Extract,
+ statement: Extract,
context: SqliteCompileContext,
): CompiledSqlStatement {
const insertColumns = Object.keys(statement.values)
@@ -597,7 +780,7 @@ function compileUpsertStatement(
}
function compileFromClause(
- table: AdapterStatement extends infer T
+ table: DataManipulationOperation extends infer T
? T extends { table: infer tableType }
? tableType
: never
@@ -862,6 +1045,49 @@ function quotePath(path: string) {
.join('.')
}
+function quoteTableRef(table: TableRef) {
+ return table.schema
+ ? quotePath(table.schema + '.' + table.name)
+ : quotePath(table.name)
+}
+
+function compileColumnDefinition(definition: {
+ type: string
+ nullable?: boolean
+ primaryKey?: boolean
+ unique?: boolean | { name?: string }
+ default?: unknown
+ autoIncrement?: boolean
+ length?: number
+ precision?: number
+ scale?: number
+}) {
+ const parts = [compileColumnType(definition)]
+ if (definition.primaryKey) parts.push('primary key')
+ if (definition.autoIncrement) parts.push('autoincrement')
+ if (definition.nullable === false) parts.push('not null')
+ if (definition.unique) parts.push('unique')
+ return parts.join(' ')
+}
+
+function compileColumnType(definition: {
+ type: string
+ length?: number
+ precision?: number
+ scale?: number
+}) {
+ if (definition.type === 'integer' || definition.type === 'bigint')
+ return 'integer'
+ if (definition.type === 'decimal') {
+ return definition.precision === undefined
+ ? 'real'
+ : `decimal(${definition.precision}, ${definition.scale ?? 0})`
+ }
+ if (definition.type === 'boolean') return 'integer'
+ if (definition.type === 'binary') return 'blob'
+ return 'text'
+}
+
function pushValue(context: SqliteCompileContext, value: unknown) {
context.values.push(normalizeBoundValue(value))
return '?'
diff --git a/worker/db.ts b/worker/db.ts
index 346353c3..c6ac0b9b 100644
--- a/worker/db.ts
+++ b/worker/db.ts
@@ -1,43 +1,42 @@
-import { createDatabase, createTable, sql } from 'remix/data-table'
-import { nullable, number, optional, string } from 'remix/data-schema'
+import { column as c, createDatabase, table, sql } from 'remix/data-table'
import { createD1DataTableAdapter } from './d1-data-table-adapter.ts'
-export const usersTable = createTable({
+export const usersTable = table({
name: 'users',
columns: {
- id: number(),
- username: string(),
- email: string(),
- password_hash: string(),
- created_at: string(),
- updated_at: string(),
+ id: c.integer(),
+ username: c.text(),
+ email: c.text(),
+ password_hash: c.text(),
+ created_at: c.text(),
+ updated_at: c.text(),
},
primaryKey: 'id',
})
-export const passwordResetsTable = createTable({
+export const passwordResetsTable = table({
name: 'password_resets',
columns: {
- id: number(),
- user_id: number(),
- token_hash: string(),
- expires_at: number(),
- created_at: string(),
+ id: c.integer(),
+ user_id: c.integer(),
+ token_hash: c.text(),
+ expires_at: c.integer(),
+ created_at: c.text(),
},
primaryKey: 'id',
})
-export const chatThreadsTable = createTable({
+export const chatThreadsTable = table({
name: 'chat_threads',
columns: {
- id: string(),
- user_id: number(),
- title: string(),
- last_message_preview: string(),
- message_count: number(),
- created_at: string(),
- updated_at: string(),
- deleted_at: optional(nullable(string())),
+ id: c.text(),
+ user_id: c.integer(),
+ title: c.text(),
+ last_message_preview: c.text(),
+ message_count: c.integer(),
+ created_at: c.text(),
+ updated_at: c.text(),
+ deleted_at: c.text().nullable(),
},
primaryKey: 'id',
timestamps: {