# Synchain — instructions for AI agents

Synchain is an all-in-one music and audio production collaboration platform: a real-time
hi-fi Creative Space, plug-in direct connect (VST3 / AU) from your DAW, and project files,
scheduling, discussion and members — from the browser, or from your terminal with the
Synchain CLI.

This page is the machine-facing runbook for https://dev.synchain.ca. If you only need to know what
the product is and which pages exist, read [/llms.txt](https://dev.synchain.ca/llms.txt) instead.

Everything below describes **@synchain/cli >= 0.9.0**. Earlier releases
lack some or all of the global `--format`, `--dry-run`, the JSON error envelope, the
exit-code categories and `synchain doctor`: check `synchain --version` and upgrade before
relying on any of them.

## Install the CLI

```bash
npm i -g @synchain/cli     # global `synchain` binary, Node.js >= 20
npx @synchain/cli --help   # or run it without installing
synchain --version         # 0.9.0 or later
```

Package: <https://www.npmjs.com/package/@synchain/cli> · Source code (MIT): <https://github.com/synchain-oss/synchain-cli>

## Authenticate

1. A human generates a CLI access key in the web app under **Settings → CLI Access**.
   Keys look like `synch_live_sk_<48 hex>` and are shown **once**.
2. Hand the key to `synchain login` through the environment — never as a command-line
   argument. Argv ends up in shell history and `/proc/<pid>/cmdline`, which leaks the
   bearer:

```bash
export SYNCHAIN_TOKEN=synch_live_sk_…
synchain login --base-url https://dev.synchain.ca
synchain doctor --json    # offline check that a key is now stored
```

`SYNCHAIN_TOKEN` is read by `synchain login` **only**. `login` validates the key against
`GET /api/user/me` and stores it in the OS config directory (`%APPDATA%\synchain\config.json`
on Windows, `~/.config/synchain/config.json` mode `0600` on POSIX); every other command uses
that stored key, so setting the variable without running `login` authenticates nothing.
Keep `--base-url` on the command line: without it `login` stops at a base-URL prompt, which a
non-interactive run cannot answer. In ephemeral CI, keep `SYNCHAIN_TOKEN` set and run
`login` at the start of each job.

The same key works directly against the HTTP API:

```
Authorization: Bearer synch_live_sk_…
```

## What you can do

Pick a project first (`synchain project use <custom-id|uuid|prefix>`), then:

- **Files** — list, upload, download, move, rename and remove project files and folders.
- **Calendar** — list, add, edit and remove project events.
- **Discussion** — list threads, read a thread, post a thread, reply.
- **Members** — read-only listing of a project's members, permissions and roles.
- **Notifications** — list them and mark them read.

A project's canonical id is always a UUID (`projects[].id` from
`synchain project ls --json`). It may additionally carry a **custom ID**
(`projects[].customId`, `null` when unset) — a short, sayable one its admins claim in
the web app, e.g. `my-band`. `project use` accepts a custom ID, a full UUID, or a
unique 8-char UUID prefix, and stores the **UUID**. Everywhere else — `--project` and
every HTTP API path — takes the canonical UUID: a custom ID is not a valid identifier
there, so build requests from `projects[].id` and treat `customId` as a label.

## Machine-readable output

- **`--format json` is a global option.** It works in any position
  (`synchain --format json files ls` and `synchain files ls --format json` are the same
  call), turns on `--json` for every command that has it, switches errors to the JSON
  envelope below, and makes `--help` print JSON. `--json` on a single command still works.
- Results go to **stdout**; progress, warnings and errors go to **stderr**.
- **`synchain --help --format json`** prints the whole command tree — every command, its
  options and subcommands, with the installed `version` at the root — as one JSON document.
  It is the cheapest way to discover the CLI without parsing prose.
- An option **value** that happens to be `-h` (`discussion post --title -h …`) is a value:
  the command runs. Only a real `-h` / `--help` option prints help.

## Preview every write with --dry-run

The 13 commands that change remote data accept `--dry-run`: `files upload|mv|rename|rm`,
`folders mkdir|rename|rm`, `calendar add|edit|rm`, `discussion post|reply` and
`notifications read`. A dry run validates the arguments, resolves id prefixes to the full
record, prints what it would do, **sends no write** and exits `0` — a rehearsal that
completes is a success. `files rm --dry-run` stops before the confirmation prompt.

```bash
synchain files rm a1b2c3d4 --dry-run --format json
```

```json
{
  "dryRun": true,
  "action": "files.rm",
  "target": {
    "project": "11111111-2222-4333-8444-555555555555",
    "file": { "id": "a1b2c3d4-7d4e-4a10-9c55-0e1f2a3b4c5d", "name": "mix.wav", "size": 1234, "folderId": null }
  }
}
```

Without JSON the plan is printed as text on stdout, starting with `[dry-run] would …`.
`action` is a stable `<group>.<command>` identifier; the shape of `target` depends on the
action.

Use it. Object ids (files, folders, events, posts, notifications) accept an
**8-char prefix** that the CLI resolves for you, and a prefix that matches the wrong object
is the one mistake that silently destroys someone else's work. A dry run makes the same
read-only lookups as the real run, so for a prefix it shows which record — id **and** name —
the command is about to touch, and it needs the same key and scopes. It cannot rehearse
rules the server checks only when the write arrives: a non-empty folder still fails the
real `folders rm` with `409 folder_not_empty`. There is no sandbox or test tenant —
anything run without `--dry-run` against https://dev.synchain.ca changes real data.

## Errors

In JSON mode a failing command writes **one line** of JSON to stderr, and nothing to stdout:

```json
{"error":{"code":"scope_denied","status":403,"url":"https://dev.synchain.ca/api/projects/11111111-2222-4333-8444-555555555555/files","detail":"…"}}
```

Which format applies, first match wins:

1. `--json` or `--format json` on the command line → JSON.
2. `SYNCHAIN_ERROR_FORMAT=json` → JSON; `SYNCHAIN_ERROR_FORMAT=text` → coloured prose.
3. Otherwise JSON whenever **stderr is not a terminal** (a pipe, a file, CI, an agent
   harness), prose when it is.

All four fields are always present: `status` is `0` when there was no HTTP response, and
`url` is `""` when no request was made.

| `code` | When |
| --- | --- |
| The server's own code, e.g. `scope_denied`, `project_scope_denied`, `folder_not_empty` | The API refused the request and said why |
| `http_<status>`, e.g. `http_401`, `http_502` | The API failed without a usable code (a gateway error page, a sentence) |
| `unknown_command`, `unknown_option`, `invalid_argument`, `missing_argument` | The command line did not parse |
| `not_logged_in` | No key is stored — run `synchain login` |
| `client_error` | No HTTP response at all: DNS or connection failure, timeout, a local file error, an id prefix that matches nothing or more than one record |

Treat the list as open — servers add codes, and some CLI-side checks carry their own. Branch
on the **exit code** for the category and on `code` only for the reasons you handle
specifically; show `detail` to a human, never parse it.

The only other line the CLI can emit on stderr is an advisory — an orphaned storage key
after a failed `files upload`, printed just before the error:

```json
{ "warning": { "code": "orphaned_upload_key", "storageKey": "…", "detail": "…" } }
```

## Exit codes

| Exit code | Meaning |
| --- | --- |
| 0 | Success, including a completed `--dry-run`, `--help` and `--version` |
| 1 | Any other failure: network, a local check, an HTTP status not listed below |
| 2 | Usage: unknown command or option, missing argument, invalid value |
| 3 | Not authenticated: no stored key, or `401` |
| 4 | Forbidden: `403`, including `scope_denied` and `project_scope_denied` |
| 5 | Not found: `404` |
| 6 | Conflict or validation: `409`, `400`, `422` |
| 7 | Rate limited: `429` |
| 8 | Server error: `5xx` |

Every failure is non-zero, so "zero or not" checks keep working. A script that compares
against `1` needs updating: CLI 0.8.0 and earlier used `1` for every failure.

## Check your setup offline: synchain doctor

```bash
synchain doctor --json
```

`synchain doctor` makes **no network request**. It checks that Node.js meets the CLI's
requirement (Node.js >= 20), that the config file exists and parses (and is `0600`
on POSIX), that the stored key has the `synch_live_sk_<48 hex>` shape (shown only as its
first 8 and last 4 characters), that the base URL passes the CLI's safety check, and whether
an active project is set. `--json` prints `{ ok, checks: [{ id, status, detail }] }`, and
the exit code is `1` when any check has `"status": "fail"`. A check is `fail` only when
the CLI cannot work at all: a Node.js below that requirement, a config file that does not
parse, no stored key and no `SYNCHAIN_TOKEN`, or an unsafe base URL. Everything else it
flags, including no active project yet, is a `warn`, so `doctor` right after a successful
`login` exits `0` before you have run `project use`. Whether the key is still **valid**
only the server can say: `synchain whoami` asks it.

## Rules you must follow

- **Attribution is automatic and cannot be disabled.** Every discussion post or reply
  made through a CLI key is flagged `is_ai_generated = true` server-side and rendered
  with an AI badge in the web UI. Do not attempt to work around it.
- **Scopes are the AND of account scopes and project scopes.** A `403 scope_denied` or
  `403 project_scope_denied` (exit code `4`) means a human has to widen the grant —
  retrying or logging in again will not help. The `members` scope is off by default.
- **Write access needs member-or-admin permission.** A viewer is read-only.
- **Rehearse before you write.** Run a mutating command with `--dry-run` first whenever an
  id came from a prefix or from another tool's output.

## Synchain Bridge

Synchain Bridge is the DAW plug-in (VST3 on Windows x64, VST3 / AU on Apple Silicon Macs)
behind plug-in direct connect: inserted on a track, it streams session audio to the user's
own browser tab over a local WebSocket on `ws://127.0.0.1:9420` (it moves up through the next
few ports when that one is taken), and the tab carries it into the project's Creative Space.
That socket is an audio stream between two programs on the user's machine, not an
automation interface: an agent does not need it and should not connect to it. Everything an
agent can do goes through the CLI or the HTTP API above.

- Download, requirements and install steps: [/download.md](https://dev.synchain.ca/download.md)
- Source code: <https://github.com/synchain-oss/synchain-bridge> — open source under GPL-3.0-or-later
- Every published release, with SHA-256 checksums: <https://github.com/synchain-oss/synchain-bridge/releases>

Synchain Bridge and the Synchain CLI are open source;
the Synchain platform itself is closed source.

## Do not crawl these paths

None of them is content: they are the authenticated app surface, the HTTP API and the
login view. Without a session they answer with a redirect to the login page or a `401`,
so whatever you index from them describes the login wall, not Synchain. Use the CLI or
the API with a key instead. The same list is served in
[/robots.txt](https://dev.synchain.ca/robots.txt) as `Disallow` rules:

- `/dashboard`
- `/projects`
- `/settings`
- `/admin`
- `/api`
- `/auth`

Everything else on https://dev.synchain.ca is public marketing content and is explicitly open to AI
training crawlers, AI search indexers and user-triggered fetchers — see
[/robots.txt](https://dev.synchain.ca/robots.txt) for the per-user-agent policy and
[/sitemap.xml](https://dev.synchain.ca/sitemap.xml) for the full URL list with hreflang alternates.

## Read those pages as Markdown, not HTML

Every marketing page is served in a Markdown form as well. **Append `.md` to its URL**:

```bash
curl https://dev.synchain.ca/features.md      # English
curl https://dev.synchain.ca/zh/faq.md        # Chinese
curl -H "Accept: text/markdown" https://dev.synchain.ca/features   # same document
```

The three home pages have no path segment to suffix, so they are `/index.md` (English),
`/zh.md` and `/fr.md`. Every other page is `<page-url>.md`, in all three languages.

Both forms return `Content-Type: text/markdown; charset=utf-8` and open with YAML
frontmatter — `title`, `description`, `url`, `canonical_url`, `language` and
`last_updated` — so you can tell what a page is, which HTML URL is canonical for it and
when it last changed without parsing the body. The body is the same content as the HTML
page with the navigation, footer and decorative markup removed, which is why you should
prefer it: it costs a fraction of the tokens and nothing has to be scraped out of it.

The `Accept: text/markdown` header only switches the response when it outranks
`text/html`, so an ordinary browser request is never affected. If you would rather not
depend on header negotiation, use the `.md` URL — it is unambiguous. To load every English
page and this runbook in one request, fetch [/llms-full.txt](https://dev.synchain.ca/llms-full.txt).

## Full documentation

- [CLI command reference](https://github.com/synchain-oss/synchain-cli/blob/dev/docs/reference.md) — every command and option, output
  shapes, dry runs, the error codes and the exit codes, maintained next to the CLI source.
- [CLI guide for AI agents](https://github.com/synchain-oss/synchain-cli/blob/dev/docs/install-for-agents.md) — the long form of this runbook:
  non-interactive login, response shapes, common patterns and safe trial runs.
- [The package on npm](https://www.npmjs.com/package/@synchain/cli) — published versions and the README.
- `synchain --help --format json` — the command tree of the version you actually installed,
  offline.
