Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion web/content/docs/file/api-reference.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,12 +65,13 @@ curl -sS \

### Write a file

`PUT /v1/workspaces/{workspaceId}/fs/file` creates or updates one file.
`PUT /v1/workspaces/{workspaceId}/fs/file` creates or updates one file. It requires **`If-Match`** as well as the usual `X-Correlation-Id`: without it the request fails `412 precondition_failed`. `If-Match: *` is create-or-overwrite; pass a revision for a conditional write.

```bash
curl -sS -X PUT \
-H "Authorization: Bearer ${RELAYFILE_TOKEN}" \
-H "X-Correlation-Id: ${RELAYFILE_CORRELATION_ID}" \
-H "If-Match: *" \
-H "Content-Type: application/json" \
"${RELAYFILE_BASE_URL}/v1/workspaces/${RELAYFILE_WORKSPACE}/fs/file" \
-d '{ "path": "/docs/guide.md", "content": "# agent guide", "contentType": "text/markdown" }' | jq .
Expand Down
72 changes: 64 additions & 8 deletions web/content/docs/file/cli.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,20 +56,18 @@ The default path delegates to `agent-relay login`. `--api-key` keeps the self-ho

## `relayfile seed`

Bulk-upload a local directory into a workspace, respecting `.gitignore` and `--exclude` patterns.
Bulk-upload a local directory into a workspace, respecting `.gitignore`.

```bash
relayfile seed my-workspace ./src
```

| Flag | Default | Description |
|---|---|---|
| `--exclude` | (none) | Glob patterns to exclude (repeatable) |
| `--dry-run` | `false` | List files that would upload, without uploading |
| `--batch-size` | `50` | Files per bulk API request |

It walks the directory and posts batches to the bulk write endpoint, printing progress.

<Warning>
Two limits as of `0.10.41`: `seed` takes no flags — `--dry-run`, `--exclude`, and `--batch-size` are rejected as undefined — and it resolves credentials only from `~/.relayfile/credentials.json`, so it does not work from a relay Cloud session (`relayfile login --api-key`, or `--token`, is required). `relayfile ops list` has the same credential limitation and degrades to local-only results with a warning.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
</Warning>

## `relayfile tree`

List a remote workspace path without mounting.
Expand All@@ -85,6 +83,55 @@ relayfile tree my-workspace / --depth 2

Prints a compact human-readable tree by default; `--json` is for scripts. `relayfile read <workspace> <path>` (alias `relayfile cat`) reads a single file the same way.

## `relayfile listen`

Stream the workspace event feed, optionally running a command per event.

```bash
relayfile listen \
--path "/linear/issues/by-state/triage/**" \
--event file.created \
--run "claude --print 'Triage this: {{path}}'"
```

| Flag | Default | Description |
|---|---|---|
| `--provider` | all | Only events from one provider |
| `--path` | all | Path glob to filter on |
| `--event` | all | Event type (`file.created`, `file.updated`, `file.deleted`) |
| `--run` | (none) | Command to execute per event, with placeholder substitution (below) |
| `--format` | `text` | `text` or `json` (one event object per line) |
| `--background` | `false` | Detach and keep listening |

`--run` substitutes five placeholders into the command:

| Placeholder | Expands to | Example |
|---|---|---|
| `{{path}}` | the changed path | `/runs/pr-59/findings/security.json` |
| `{{type}}` | the event type | `file.created` |
| `{{provider}}` | the owning provider | `github` |
| `{{revision}}` | the file's revision | `rev_2936535` |
| `{{event}}` | **the whole event**, as space-separated `key:value` pairs | `type:file.created path:/runs/… revision:rev_… eventId:evt_… origin:agent_write …` |

<Warning>
Quote `{{event}}`. It expands to the entire event — a dozen or more space-separated tokens — so an unquoted `--run "my-agent --event {{event}}"` splatters them across `argv`. Write `--run "my-agent --event '{{event}}'"`. The single-value placeholders are passed as one argument each.
</Warning>

Events carry `eventId`, `type`, `path`, `revision`, `provider`, `origin`, `correlationId`, and — for small files — the content inlined. See [Events and webhooks](/docs/file/events).

To keep a subscriber running across reboots, `relayfile supervisor install` accepts every `listen` flag and embeds them verbatim into a launchd (macOS) or systemd (Linux) unit that restarts on failure:

```bash
relayfile supervisor install \
--path "/linear/issues/by-state/triage/**" --event file.created \
--run "claude --print 'New triage issue at {{path}}. Assign it.'"
relayfile supervisor status
```

<Note>
`listen` is missing from `relayfile --help` and `relayfile help listen` prints the generic help, but `relayfile listen --help` shows its usage. Expect to supervise it: on a busy workspace the stream can end mid-message, and reconnecting immediately earns a `429` on the WebSocket handshake — run it with `--background` or under `relayfile supervisor install`, and back off between reconnects.
</Note>

## `relayfile mount`

Mount a workspace to a local directory, syncing changes in real time. This replaces the standalone daemon for end users.
Expand DownExpand Up@@ -138,10 +185,19 @@ See [Run locally](/docs/file/run-locally) for the daemon in context and [Local d
| `relayfile export` | Download a snapshot (`--format tar\|json\|patch`) |
| `relayfile status` | Per-provider sync state, lag, conflicts, denials |
| `relayfile integration connect / list / disconnect` | Manage provider integrations after setup |
| `relayfile integration available / search` | Browse or search the live provider catalog (`--refresh`, `--json`) |
| `relayfile integration set-metadata` | Replace flat provider metadata (Jira/Confluence `cloudId`) |
| `relayfile integration bind / unbind` | Route a provider path glob to a channel webhook |
| `relayfile pull` | Force a reconcile of a path or the whole workspace |
| `relayfile permissions` | Show writable paths and expected schema for a path |
| `relayfile writeback status / list / retry` | Local pending, failed, and dead-lettered writebacks |
| `relayfile ops list / replay` | Inspect and replay dead-lettered writeback ops |
| `relayfile restart` / `relayfile supervisor` | Restart a mount, or install it as a launchd/systemd service |
| `relayfile stop` / `relayfile logs` | Control and read a background mount daemon |
| `relayfile observer` | Open the hosted file observer for a workspace |

<Note>
There is no `relayfile permissions` command. To find out what a path expects, read the provider's `.adapter.md` and `.schema.json` under `/discovery/<provider>/…`; to see what was denied, read `.relay/state.json` → `deniedPaths` in the mirror.
</Note>

## Global flags

Expand Down
27 changes: 19 additions & 8 deletions web/content/docs/file/events.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,16 +23,24 @@ Every change — webhook, sync, or another agent's write — surfaces as the sam

```json
{
"eventId": "evt_01HQ8K7M2YV3R0XW9F4ZB6T2QA",
"type": "file.updated",
"path": "/linear/issues/AGE-16__87389837-62b1-4e1a-a237-59218bab2974.json",
"revision": "rev_42",
"provider": "linear",
"origin": "provider_sync",
"timestamp": "2026-05-13T14:32:01Z"
"eventId": "evt_2507297",
"type": "file.created",
"path": "/runs/pr-59/findings/security.json",
"revision": "rev_2935117",
"provider": "runs",
"origin": "agent_write",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"contentType": "application/json",
"contentHash": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"content": "{}",
"inlineContent": true,
"encoding": "utf-8",
"correlationId": "lt-1-1788520629",
"timestamp": "2026-09-04T11:17:09.694Z"
}
```

Small files arrive with their content inlined (`inlineContent: true`), so a handler often needs no follow-up read. `correlationId` carries through from the write that caused the event, which is how you tie an event back to the request that produced it.

- **`type`** is one of `file.created`, `file.updated`, `file.deleted`.
- **`path`** is the canonical file that changed. The path itself carries context — you know which provider and which record without parsing a payload.
- **`revision`** monotonically increases per file. Use it to order events and to fetch the prior state for a diff.
Expand All@@ -49,13 +57,15 @@ relayfile listen \
--run "claude --print 'Triage this: {{path}}'"
```

`relayfile listen [WORKSPACE] [--provider PROVIDER] [--path GLOB] [--event TYPE] [--run CMD] [--format text|json] [--background]` streams the workspace's event feed. `--run` substitutes `{{path}}`, `{{type}}`, `{{provider}}`, and `{{revision}}` as single values, plus `{{event}}` for the whole event as space-separated `key:value` pairs — quote that one, or its dozen-odd tokens splatter across the command's arguments. `--format json` prints one event object per line, for piping into anything that isn't a shell command. To fan events out to a channel instead of a local process, bind the glob to a webhook with `relayfile integration bind <provider> <glob> --channel … --webhook … --webhook-token …`.

Or from the SDK with `onWrite`, which subscribes over the same WebSocket stream and dispatches by pattern:

```typescript
import { onWrite } from '@relayfile/sdk';

onWrite('/linear/issues/**', async (event) => {
if (event.source === 'agent') return; // ignore our own writes
if (event.origin === 'agent_write') return; // ignore our own writes
await agent.handle(event);
}, { client, workspaceId, operations: ['create', 'update'] });
```
Expand All@@ -67,6 +77,7 @@ See [Agents](/docs/file/agents) for the framework helpers built on this.
- **At-least-once.** Events can repeat. Deduplicate on `eventId`; treat handlers as idempotent.
- **Ordering.** Per file, `revision` is the source of truth — wall-clock `timestamp` can be close together under bursty traffic.
- **Catch-up.** A subscriber that connects with a cursor receives the events it missed while disconnected, so a restart doesn't drop changes. If the WebSocket can't open, the SDK degrades to HTTP polling rather than going silent.
- **Reconnect.** Long-lived subscribers do get dropped — a busy workspace can end a stream mid-message — and a reconnect storm is answered with `429` on the WebSocket handshake. Supervise the subscriber (`relayfile listen --background`, or `relayfile supervisor install`) and back off between reconnects rather than looping immediately.

<CardGroup cols={2}>
<Card title="Real-time sync" href="/docs/file/realtime-sync">
Expand Down
45 changes: 25 additions & 20 deletions web/content/docs/file/mount-layout.mdx
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,47 @@
---
title: 'Mount layout'
description: 'Every Relayfile mount is self-describing: LAYOUT.md, per-integration layout files, _index.json, canonical naming, and four alias views.'
description: 'Every Relayfile mount is self-describing: LAYOUT.md, per-integration layout files, _index.json, canonical naming, and per-adapter alias views.'
---

Every mount is self-describing. The agent never needs to learn paths from external documentation — `cat mount/LAYOUT.md` lists everything, and per-integration `.layout.md` files document the tree shape for each provider. This is deliberate: an agent oriented by reading the tree itself doesn't carry path knowledge in its prompt or schema.
Every mount is self-describing. The agent never needs to learn paths from external documentation — `cat mount/LAYOUT.md` lists everything, and per-integration `LAYOUT.md` files document the tree shape for each provider. This is deliberate: an agent oriented by reading the tree itself doesn't carry path knowledge in its prompt or schema.

## The tree

```
mount/
├── LAYOUT.md # virtual, read-only — top-level guide
├── LAYOUT.md # top-level guide: every provider root
├── _index.json # root listing
├── linear/
│ ├── .layout.md # linear-specific tree shape
│ ├── LAYOUT.md # linear-specific tree shape
│ ├── issues/
│ │ ├── _index.json
│ │ ├── AGE-12__fix-login-bug.json # canonical: <slug>__<id>
│ │ ├── by-title/AGE-12-fix-login-bug.json
│ │ ├── by-id/AGE-12.json
│ │ └── by-state/in-progress/AGE-12__fix-login-bug.json
│ └── users/by-name/dana.json
└── github/
└── repos/
├── _index.json
├── acme/api/
│ └── pulls/42__bump-deps/meta.json
└── by-name/acme__api.json
├── github/
│ ├── LAYOUT.md
│ └── repos/
│ ├── _index.json
│ └── acme/api/
│ ├── pulls/_index.json
│ └── pulls/42__bump-deps/meta.json # <number>__<slug>
└── github/repos/acme__api/ # flat alias namespace
└── pulls/by-id/42.json
```

## Self-describing files

Three kinds of metadata files make the tree navigable without prior knowledge:

- **`LAYOUT.md`** at the root is a virtual, read-only guide to the top-level structure. It's the first thing an agent should `cat` after mounting.
- **`<integration>/.layout.md`** documents the tree shape for a single provider — what directories exist under `linear/`, what each holds.
- **`<integration>/LAYOUT.md`** documents the tree shape for a single provider — what directories exist under `linear/`, what each holds, and which paths are artifacts rather than records. Uppercase, no leading dot.
- **`_index.json`** in every directory lists the rows that directory contains, so an agent can read one file to understand a directory rather than walking every entry.

## Canonical naming

Entity files follow a `<sanitized-name>__<id>` convention so identifiers are recoverable from any filename. The ID is always the **last** `__`-separated segment:
Entity files put an identifier and a human-readable name in one filename, separated by `__`, so identifiers are recoverable from a listing. **Which side the id sits on is per-adapter**: Linear writes `<sanitized-name>__<uuid>` (id last), GitHub writes `<number>__<slug>` (id first). Read the provider's `LAYOUT.md` rather than assuming, and prefer `_index.json` rows for lookup:

```text
inbox/threads/Re_Welcome__01HXYZ.json
Expand All@@ -48,19 +51,21 @@ inbox/threads/01HXYZ.json
The first path is the canonical human-readable form. The second is a legacy fallback that remains readable during the naming transition — consumers should accept both the new-style filename and the bare `<id>` basename while producers are updated.

<Note>
Because the ID is recoverable from the filename, an agent can read `AGE-12__fix-login-bug.json`, derive the identifier `AGE-12`, and write back to the canonical path without a separate lookup.
Take the exact filename from a listing or `_index.json` rather than assembling one. Some adapters require the full `<number>__<slug>` directory on reads — for GitHub, `pulls/59/meta.json` returns `404` while `pulls/59__<slug>/meta.json` resolves — and the same adapter may still accept a bare id on the write path.
</Note>

## The four alias views
## Alias views

Each resource type ships four alias views out of the box, so an agent can navigate the same records by whichever key it has in hand:
Alias views let an agent navigate the same records by whichever key it has in hand — `by-id/`, `by-title/`, `by-state/`, `by-edited/`, `by-assignee/`, `by-creator/`, `by-priority/`, `by-name/`, `by-uuid/`. They are views over the same canonical entities, not copies you have to keep in sync.

- **`by-title/`** — slug lookups (`AGE-12-fix-login-bug.json`).
- **`by-id/`** — identifier lookups (`AGE-12.json`).
- **`by-name/`** — human-readable name lookups (`dana.json`, `acme__api.json`).
- **`by-state/`** — grouped by issue or PR state (`by-state/in-progress/...`).
**Which views exist, and where they live, is decided by the adapter and the resource** — there is no fixed set. Two things to check in the provider's `LAYOUT.md` before building a path:

These are views over the same canonical entities, not copies you have to keep in sync. An agent that knows only a state can `ls by-state/in-progress/`; one that knows only a name can read `by-name/dana.json`.
- **Location.** Linear puts them under the canonical subtree (`/linear/issues/by-id/AR-100.json`). GitHub puts them in a *flat sibling namespace* keyed `<owner>__<repo>` (`/github/repos/acme__api/pulls/by-id/42.json`) — there is no `by-name` under `/github/repos/`.
- **Set.** On a live workspace, GitHub pulls expose `by-creator`, `by-edited`, `by-id`, `by-state`, and `by-title`; GitHub issues expose four of those; Linear issues add `by-assignee`, `by-priority`, and `by-uuid`.

<Note>
A flat alias directory also has an `_index.json`, and it means something different: it is a manifest of which alias subdirectories exist (`{ "rows": [ { "title": "by-id", "file": "by-id/" } ] }`), not a record index. Only the canonical tree's `_index.json` lists records, as a bare JSON array.
</Note>

## Lazy repo materialization

Expand Down
4 changes: 4 additions & 0 deletions web/content/docs/file/quickstart.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,10 @@ grep -rl '"state":"Todo"' ./relayfile-mount/linear/issues/

See [Reads and writes](/docs/file/reads-and-writes) for the full PATCH / CREATE / DELETE model.

<BannerLink href="/docs/file/review-bot" icon="bot">
Guide: build a PR review bot — add providers, mount the same workspace in every sandbox, and post the review by writing a file.
</BannerLink>

## Next steps

<CardGroup cols={2}>
Expand Down
9 changes: 6 additions & 3 deletions web/content/docs/file/reads-and-writes.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,13 +56,16 @@ Writes are durable: they're recorded in Relayfile's writeback queue and processe

## Discovering schemas in-tree

You don't need an out-of-band schema registry. Per-resource schemas are discoverable in the tree itself at `<resource>/.schema.json`:
You don't need an out-of-band schema registry. Every writable resource advertises a JSON Schema and a create example in the workspace's own `/discovery` tree, whose paths carry literal placeholder segments:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

```bash
cat mount/linear/issues/.schema.json
cat mount/discovery/github/.adapter.md # operations + ID patterns
cat mount/discovery/linear/issues/.schema.json # full record schema
cat mount/discovery/linear/issues/.create.example.json # minimal create payload
cat "mount/discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.schema.json"
```

An agent that wants to create or patch a record reads the adjacent schema file to learn the expected shape, then writes a conforming JSON file. This keeps the contract co-located with the data — the same self-describing principle as [`LAYOUT.md` and `.layout.md`](/docs/file/mount-layout).
`.adapter.md` is the per-provider contract: which resources are writable, the ID pattern that decides whether a filename is a canonical record or a create draft, and which operations each resource supports. `.schema.json` is JSON Schema draft 2020-12 for the full synced record; fields marked `readOnly` are server-managed and are rejected on write. Read them before composing a payload — the same self-describing principle as [`LAYOUT.md`](/docs/file/mount-layout).

The exact write semantics and field mapping per provider are defined by the [adapters](/docs/file/adapters-and-providers), which own webhook-to-path mapping and writeback behavior.

Expand Down
Loading
Loading