# Files

Give a house something to know, then ask about it. Drop a PDF, an image, or a text file into a house — every member, human or bot, can search it and ground answers in it with citations.

```
give a house something to know
  └─ add a file  (web drop · CLI `arbe files add` · POST /api/houses/:id/files)
        ├─ blob   → Supabase Storage `house-files/{houseId}/…`     canonical, RLS-gated
        └─ index  → file-index store, one per house, lazily created  derived, rebuildable
              └─ ask about it
                    ├─ bot turn   → `search_files` / `list_files` / `create_file` tools → citations in reply
                    ├─ web app    → house "Files" panel + thread drag-and-drop
                    └─ devs       → POST /api/houses/:id/files/search · @arbe/core client
```

Files are uploaded to a **house**, not a thread — threads inherit their house's files. You add them from the house Files panel, by dragging them onto a thread, or from the CLI. The index runs a vision model over images and PDF pages as it ingests, captioning their visual content, so a scan or a photo is searchable as text — not just files with an embedded text layer.

## Canonical vs derived

Supabase Storage holds the bytes (`house-files/{houseId}/{uuid}-{filename}`, bucket `house-files`, RLS by house membership — same `house_id` scoping as synced tables). The index holds a derived store: pages, chunks, embeddings, captions. Lose the index, rebuild it from the bucket. The retrieval index is an implementation detail over canonical data, never the only copy — same shape as [memory](../thinking/memory.md).

Scope is the **house**. `houses.rag_store_id` (nullable text, service-role-write only) binds a house to its store. Created lazily on first upload: no store until a house has a file. Per-thread or per-agent stores are a non-goal until proven needed.

## Surface naming

arbe's surface says **files**, **search**, **citations**. The index provider (ragthis) and the words "rag" / "store" never appear in a route, tool name, CLI command, or UI string — it is one implementation behind one server module, swappable. We import the provider's `Citation` shape as-is (chunk text, `file_id` + `filename`, page/line coordinates, score) because it is the product and it's already right.

## One module owns the boundary

`packages/core/files.ts` wraps the index client and the Storage orchestration: resolve house → store (`getHouseStoreId`), create-on-demand (`ensureHouseStore`), upload (`addFile` / `uploadHouseFile`), list (`listFiles` / `listHouseFiles`), search (`searchFiles`), delete with blob+index cascade (`deleteHouseFileByPath` / `deleteHouseFileById`), plus citation/file-list formatters. The www API routes, the bot tools, and the CLI all go through it. `RAGTHIS_API_URL` + `RAGTHIS_API_KEY` live in server env (`apps/www/src/lib/server/file-index.ts`, read from `$env/dynamic/private`); documented in [environment-variables](./environment-variables.md). If either is unset, the file tools are simply not offered — no error, no fallback.

## HTTP routes

Under `apps/www/src/routes/api/houses/[id]/files/`. Every route checks house membership first.

| Method | Path | Purpose |
| ------ | ---- | ------- |
| GET    | `/api/houses/:id/files` | List Storage blobs merged with index status |
| POST   | `/api/houses/:id/files` | Multipart upload → Storage → index |
| DELETE | `/api/houses/:id/files?path=…` | Delete by Storage path, cascade to index |
| POST   | `/api/houses/:id/files/search` | Query → citations (`{ query, topK }`) |
| GET    | `/api/houses/:id/files/content?path=…` | Download / text preview |
| GET    | `/api/houses/:id/files/:file_id/chunks` | Inspect the chunks search matches against |
| DELETE | `/api/houses/:id/files/:file_id` | Delete by index file id |

Write path: membership check → blob to Storage (unique object path, duplicate filenames allowed) → forward to the index → return the file entry with `status`. Indexing is async (`pending → completed`/`failed`); the panel and CLI poll or re-list.

The `@arbe/core` client wraps these: `listHouseFiles`, `addHouseFile`, `searchHouseFiles` (`packages/core/client.ts`).

## Bot tools

`packages/core/dispatch/file-tools.ts`, registered in `buildAgentTools(ctx)` like `send_gif`:

- `search_files` — `{ query, top_k? }`, returns formatted `[n]` citations (filename, location, snippet) so the model cites inline.
- `list_files` — no args, returns the file list with status.
- `create_file` — `{ name, content }`, lets a bot write a text file into the house store (validates filename, requires an admin Supabase client).

`search_files` and `list_files` are offered whenever the house has file-index context; with no store yet they report "no files" rather than vanishing. `create_file` is gated stricter (needs `adminSupabase` for lazy store creation). We deliberately skip the index provider's `chat` endpoint — arbe owns the agent loop; the index is retrieval only.

## CLI

`apps/cli/src/commands/files.ts`. All accept `--house <id>` and `--json`.

```sh
arbe files add <path...>      # upload one or more files (globs ok); polls until completed
arbe files list               # alias: ls, l — filename, status, size, age
arbe files search <query>     # alias: find — numbered citations; --limit/-n N
```

## How we know it works

End to end: ask a bot about a fact that exists only in an uploaded file; the reply quotes it with a citation; the index key appears nowhere client-side. Proof script: `scripts/bot-cites-house-file.ts` (create house → upload a file with a planted nonce → wait for `completed` → `@bot` question → assert reply contains the nonce and cites the filename → assert the key never appears in the thread). Unit coverage: `packages/core/files.test.ts` (formatting, paths, preview) and `packages/core/dispatch/file-tools.test.ts` (tools against a mocked HTTP boundary, credential never surfaces, conditional offering).

## Future ideas

The same index could eat **thread transcripts**. A house's durable streams are canonical history; feeding closed threads into the house store would turn search into the retrieval half of [memory](../thinking/memory.md) — "what did we decide about X?" answered with a citation into the actual thread. Same store, same tools, a second feeder. Not built; the house scope and single-module boundary exist so it stays buildable.

## Out of scope

- The index provider's `chat` endpoint — arbe owns the loop.
- Per-thread or per-agent stores.
- Reindex/cancel UI — admin-only via CLI passthrough if ever needed.
- Multi-tenant index — single-tenant behind one shared key; isolation is arbe's job (membership checks before every call).

Code: `packages/core/files.ts`, `packages/core/dispatch/file-tools.ts`, `apps/www/src/routes/api/houses/[id]/files/`, `apps/cli/src/commands/files.ts`, migrations `20260609100000_house_file_index.sql` / `20260609130000_house_files_storage.sql` / `20260610100000_lock_house_file_index_binding.sql`.<br>
See [storage](./storage.md), [secrets](./secrets.md), [memory (thinking)](../thinking/memory.md).
