Skip to content

Connecting files

The files connection points your Twin at one folder on the machine the API runs on, and keeps a searchable index of everything in it.

POST /api/connections { "provider": "files", "config": { "root": "C:/Users/you/Documents" } }

The server resolves the folder (path.resolve), refuses anything that isn’t a real, readable directory, saves it as your files connection, and immediately kicks off a first sync plus a live watcher. Without an explicit root, filesRootFor() (apps/api/src/twin/files.ts) falls back to FILES_ROOT from .env — but only for the owner. Every other being has to connect a folder of their own; FILES_ROOT points at the machine owner’s documents, so it never leaks to a citizen or a second registered human by default. See Environment variables.

What gets indexed

syncFiles() (apps/api/src/twin/files.ts) walks the folder recursively (max depth 8, max 4,000 files by default, TWIN_MAX_FILES to change it), skipping the usual noise — node_modules, .git, dist, build, __pycache__, dotfiles, and a dozen more — and indexes any file whose extension is one of three sets:

SetExamples
Text (summarisable)md, txt, json, js/ts/tsx/jsx, css, html, csv, py, sh, sql, env, and about 25 more
Images (indexed, not summarised)png, jpg, webp, heic, avif, …
Documents (summarisable)pdf, docx

Files over 25MB are skipped entirely. For each indexable file, a content hash (sha1 of size+mtime) decides whether anything changed since the last pass — unchanged files cost nothing.

Summarisation is rate-limited on purpose

Only text-like and .docx files are summarised, and only up to TWIN_MAX_SUMMARIES_PER_SYNC (20) per sync pass, even on a huge folder — this is the same “cheap models, batch, don’t loop” budget discipline from PLAN.md §7 applied to a walk that could otherwise touch thousands of files. Each summary is a two-sentence cheap-tier call over the first 4,000 characters (SUMMARY_CHARS), then embedded (embed tier) for semantic file search. .docx files are read with a small hand-rolled zip/XML reader (docxText()) rather than a dependency — it pulls word/document.xml out of the zip’s central directory and strips tags.

After a sync that changed anything, one extra memory is written directly (not through the reflection loop): “Owner keeps 340 files under Documents; notable: invoice-2026.pdf, resume.docx, …” (kind document, importance 0.35).

Live watching

Once a sync completes, watchFiles() starts a chokidar watcher on the root (depth 8, 800ms stability threshold so partial writes don’t trigger early). Changes are batched per 1.5 seconds and re-indexed incrementally (flushDirty()), each producing its own Twin activity line — “Noticed report.docx”, “Updated notes.md”, “Forgot draft.txt (removed).” Watching restarts automatically if you change the connection’s root, and stops cleanly when you disconnect the provider or the API shuts down.

Browsing and downloading

The Files widget on the desktop (and /twin’s Sources panel) browse the same index through the general files API — not a files-specific endpoint:

GET /api/files?scope=user&path=<relative> → FileEntry[] (directory listing)
GET /api/files/search?q=<text> → FileEntry[] (name/summary match)
GET /api/files/read?id=<fileId> → { content } or { base64 }
GET /api/files/download?id=<fileId> → the file, or a zip if it's a directory

Downloading a directory streams a zip on the fly (archiver, level-6 compression) rather than building one on disk first. The same file API also serves Studio project outputs (scope=project) and uploads (scope=upload, e.g. your own theme images) — see Outputs — so “browse and download, single file or zip” (PLAN.md §6) is one implementation shared by every kind of file your Twin or the Studio ever produces.

Status and disconnecting

GET /api/twin/status → providers[].files: { configured, connected, meta: { root, watching, indexed }, hint }
DELETE /api/connections/:id

configured is true only if the resolved root actually exists as a directory on this machine right now — a stale or deleted folder path shows configured: false with a plain hint rather than a raw filesystem error. Disconnecting stops the watcher immediately; already-indexed rows are left in place (they simply stop refreshing) until you delete them or reconnect a different root.