Metadata-Version: 2.5
Name: xsync-cli
Version: 0.4.2
Summary: Sync OpenAI-compatible models into Codex, Claude Code, OpenCode, Pi, and OMP.
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pyyaml>=6.0
Requires-Dist: tomlkit>=0.13
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# xsync-cli

Point Codex, Claude Code, OpenCode, Pi, and OMP at any OpenAI-compatible
endpoint. Every endpoint model appears in the model picker.

```
$ xsync claude

  claude · 9router
    http://127.0.0.1:20128/v1
    home: ~/.config/xsync/homes/9router/claude
  ✓ 35 models ready. Your real claude is untouched.
```

A harness keeps its model list in a settings file. A person must write that
file by hand, and the file becomes wrong when the endpoint adds a model or
drops one. xSync writes the file for you.

xSync opens the harness in a home of its own. The settings of the user stay
as they are. Add `apply` when you do want to change them.

| Harness | Needs |
|---|---|
| Codex | An endpoint that answers `GET /v1/models`. |
| Claude Code | The same, and `POST /v1/messages` for the requests. |
| OpenCode | The same model endpoint, plus Chat Completions or Responses. |
| Pi | The same model endpoint and selected wire API. |
| OMP | The same model endpoint and selected wire API. |

## Install

    uv tool install xsync-cli

Or:

    pipx install xsync-cli

The package needs Python 3.11 or later. The command is `xsync`.

### Update

    xsync update

xSync finds the tool that installed it. An install from PyPI gets
`uv tool upgrade` or `pipx upgrade`. A git checkout gets `git pull --ff-only`,
then a reinstall. The command stops when the checkout has uncommitted
changes. Add `--dry-run` to see the commands and run nothing.

### Uninstall

    xsync uninstall

The command resets every harness that `apply` wrote, then removes the package.
Your profiles and the source folder of a checkout stay. Add `--dry-run` to see
the plan, or `--yes` to skip the question.

Warning: `--purge` also deletes your profiles, your API keys, and the isolated
homes. You cannot undo it.

    xsync uninstall --purge

## Start

### 1. Make a profile

    xsync setup

The command asks for the profile name, the base URL, and the API key. Leave
the key empty when the endpoint needs none. The command then reads the
endpoint and shows every model that it serves.

The command does not ask for the wire API. It uses `responses`, or the value
that Codex already has for the same URL. To change it, edit `wire_api` in the
profile file.

### 2. Open a harness

    xsync codex
    xsync claude
    xsync opencode
    xsync pi
    xsync omp

The command builds a home for the profile. It then starts the harness in
that home. The real harness configuration stays unchanged.

Arguments after `--` go to the harness:

    xsync claude -- --model cmc/deepseek/deepseek-v4-pro

### 3. Or write the real harness

    xsync codex apply
    xsync claude apply
    xsync opencode apply
    xsync pi apply
    xsync omp apply

The `apply` verb changes the real home of the user. Read **Safety** before
you use it.

## Commands

| Command | Action |
|---|---|
| `xsync setup` | Make a profile. |
| `xsync list` | Show the profiles. The active profile has a star. |
| `xsync use <name>` | Set the active profile. |
| `xsync remove <name>` | Delete a profile. |
| `xsync help` | Show the help. `xsync help codex` explains one command. |
| `xsync codex` | Open a Codex on the active profile, in its own home. |
| `xsync claude` | Open a Claude Code the same way. |
| `xsync opencode` | Open an OpenCode the same way. |
| `xsync pi` | Open a Pi the same way. |
| `xsync omp` | Open an OMP the same way. |
| `xsync codex -- <args>` | The arguments after `--` go to the harness. |
| `xsync codex apply` | Write the real Codex of the user. |
| `xsync codex reset` | Reset all original Codex settings with a dated backup. |
| `xsync claude apply` | Write the real Claude Code of the user. |
| `xsync opencode apply` | Write the real OpenCode of the user. |
| `xsync pi apply` | Write the real Pi model catalog. |
| `xsync omp apply` | Write the real OMP model catalog. |
| `xsync <harness> apply --dry-run` | Show the difference. Write nothing. |
| `xsync <harness> apply --reset` | Remove everything that xSync wrote. |
| `xsync <harness> apply --reset --force --yes` | Reset with no question. |
| `xsync <harness> --profile <name>` | Use another profile for one run. |
| `xsync <harness> --yolo` | Start the harness with no permission question. |

## The isolated home

The normal harness commands do not change the settings of the user. Each
command builds a home under `~/.config/xsync/homes/<profile>/`.

Codex uses `CODEX_HOME`. Claude Code uses `CLAUDE_CONFIG_DIR`. OpenCode uses
isolated XDG config, data, cache, and state directories. Pi and OMP use
`PI_CODING_AGENT_DIR`.

The home starts as a copy of the real settings file, so the hooks of the
user and the trusted projects of the user stay. xSync then points the copy at
the endpoint of the profile.

The home shares the work of the user through a symbolic link:

| Harness | Shared |
|---|---|
| Codex | `skills`, `plugins`, `marketplaces`, `memories`, `AGENTS.md`, `auth.json` |
| Claude Code | `agents`, `skills`, `plugins`, `hooks`, `commands`, `CLAUDE.md` |
| OpenCode | `agents`, `commands`, `plugins`, `skills`, `AGENTS.md` |
| Pi | `agents`, `extensions`, `plugins`, `prompts`, `skills`, `themes` |
| OMP | `agents`, `extensions`, `plugins`, `prompts`, `skills`, `themes` |

A session, a cache, and a log stay inside the isolated home. They never mix
with the real home.

Delete a home at any time. The next command builds it again.

Exit codes: `0` for success, `1` for an error, `2` when the endpoint does not
answer.

## Reset All Original Codex Settings

Caution: This command removes every setting in the original `config.toml`.
This includes custom providers, MCP settings, permissions, and project trust settings.
Caution: The command disconnects Codex sessions that use this home.
Finish active work before you run the command.

    xsync codex reset

The `xsync codex --reset` command performs the same full reset.
The `xsync codex apply --reset` command removes only the applied xSync settings.

The command asks for confirmation.
Use `--yes` to confirm in a script.
Use `--dry-run` to inspect the scope without changes.
The command does not require an xSync profile or a working endpoint.

The command saves the exact configuration in `config.toml.<UTC timestamp>.bak`.
The backup permits access only by the file owner.
The command then clears `config.toml`, the model cache, and the xSync state file.
Codex supplies its defaults instead of values that xSync hard-codes.
An absent `config.toml` stays absent.

The command keeps authentication, sessions, skills, plugin files, and catalog files.
The command does not change project configuration files, Codex profile files, environment variables, or managed settings.
Those sources can still override Codex defaults.

Start the original Codex after the reset.

    CODEX_HOME="$HOME/.codex" codex

## Remove the Applied xSync Settings

Caution: Reset disconnects Codex sessions that use the original home.
Finish active work before you run the reset.

    xsync codex apply --reset

The command resets the original Codex home, even when you run it inside an xSync Codex session.
xSync ignores an inherited `CODEX_HOME` that points to an xSync profile home.
xSync uses an explicit custom `CODEX_HOME` outside its profile homes.

The reset removes the recorded catalog and clears `models_cache.json`.
The reset also stops the daemon for this home, because the daemon can retain the old model catalog.
The command asks for confirmation before it stops the daemon.
Use `--yes` to confirm in a script.
If the daemon stop fails, the reset keeps the configuration, catalog, cache, and state file unchanged.
The reset also removes the endpoint model selection when it restores the OpenAI provider.
The reset keeps your authentication, sessions, skills, plugins, and unrelated settings.
If the configuration is already clear, the command clears the cache without `--force`.

Restart the original Codex after the reset.
Set the original home explicitly if the terminal inherits an xSync `CODEX_HOME`.

    CODEX_HOME="$HOME/.codex" codex

Do not use `xsync codex` to start Codex without an xSync profile.
The `xsync codex` command creates the profile catalog again.

## Refresh the Original Codex Catalog

Caution: Apply disconnects Codex sessions that use the original home.
Finish active work before you apply the catalog.

    xsync codex apply

The command writes the endpoint catalog into the original Codex home.
The command stops this home's daemon before it writes the catalog.
This prevents a new Codex terminal from reusing the daemon's old model list.
The command asks for confirmation before it stops the daemon.
Use `--yes` to confirm in a script.
If the daemon stop fails, the command keeps the configuration and catalog unchanged.
The command does not stop a daemon or write files with `--dry-run`.

Start the original Codex after apply.

    CODEX_HOME="$HOME/.codex" codex

## Claude Code

    xsync claude --init
    xsync claude

Claude Code holds no catalog file. It reads the rows of the `/model`
picker from `modelPicker.options` in `~/.claude/settings.json`, and it reads
the endpoint from the `env` block of the same file. `xsync claude` writes
those two keys and nothing else.

The endpoint must answer the Anthropic API at `POST /v1/messages`. An
endpoint that answers only the OpenAI API works with Codex, not with Claude
Code.

For a profile name that contains `9router`, xSync also sets
`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` unless the user already set it.
This avoids 9Router's current `safeguards` rejection when the router changes
Claude Code beta headers. This setting disables Claude Code beta-only
features. Remove the override after 9Router preserves the `anthropic-beta`
header.

### The map to a known model

Claude Code refuses a model that it does not know:

    "cmc/deepseek/deepseek-v4-pro" isn't described by this version's model
    catalog; update Claude Code, or map it with behavesAs on a modelPicker row

Therefore each row carries `behavesAs`: the id of a model that Claude Code
knows. The prompt profile, the capability defaults, and the effort defaults
of that model then apply. The model id that Claude Code sends does not
change.

A model name that already holds a known model id, such as
`ed3n/claude-sonnet-5`, gets no `behavesAs`. It keeps its native handling.

Edit `adapters/rules/claude.toml` to change a map.

### The built-in models stay

xSync sets `replaceBuiltInOptions` to `false`. The built-in models stay at
the top of the picker, and the router models come after them. A router that
stops therefore leaves a working picker.

## OpenCode

    xsync opencode
    xsync opencode apply

xSync writes one custom provider with every model from `GET /v1/models`.
It uses the profile name as the OpenCode provider id.

xSync selects `@ai-sdk/openai-compatible` for `chat`. It selects
`@ai-sdk/openai` for `responses`.

An isolated session copies the global OpenCode config files. It links the
user agents, commands, plugins, skills, and `AGENTS.md`.

The isolated session uses separate XDG data, cache, and state directories.
The real OpenCode state stays unchanged.

`xsync opencode apply` writes only `provider.<profile>` in
`~/.config/opencode/opencode.json`.

## Pi and OMP

    xsync pi
    xsync omp
    xsync pi apply
    xsync omp apply

Pi reads custom providers from `~/.pi/agent/models.json`. OMP reads them
from `~/.omp/agent/models.yml`.

xSync selects `openai-completions` for `chat`. It selects
`openai-responses` for a generic Responses endpoint.

A CLIProxy profile uses `openai-codex-responses`. This keeps Codex-specific
request and response handling.

The isolated commands copy regular settings and credential files. They link
extensions, plugins, prompts, skills, themes, and agents.

The isolated commands keep sessions and XDG state inside the xSync home.

`xsync pi apply` writes only `providers.<profile>` in `models.json`.
`xsync omp apply` writes only `providers.<profile>` in `models.yml`.

## CLIProxy

Use this command when the profile name does not contain `cliproxy`:

    xsync setup --endpoint-type cliproxy

xSync also detects CLIProxy from a profile name that contains `cliproxy`.
It detects the default CLIProxy port `8317`.

Use `responses` for a CLIProxy profile that serves Codex. xSync selects
`responses` as the setup default for CLIProxy.

For Codex, xSync writes `requires_openai_auth = true`. It writes the profile
key as `experimental_bearer_token`.

The isolated Codex home copies `auth.json`. This preserves the Codex login
that CLIProxy uses with its recommended OAuth mode.

For OpenCode, xSync selects `@ai-sdk/openai` for the CLIProxy Responses API.

For Pi and OMP, xSync selects `openai-codex-responses`. It does not install
an external provider plugin or change provider accounts.

## Switch between endpoints

Each profile gets a home of its own. Therefore two endpoints never mix:

    xsync codex --profile openrouter
    xsync codex --profile 9router

A harness holds one model provider. Therefore `apply` writes one profile at
a time:

    xsync codex apply --profile openrouter

`apply` always points the harness at the endpoint of the profile, so the
model list and the endpoint always agree. A dry run does not point anything
at anything, so it warns when the two disagree:

    the rows would come from "openrouter" (https://openrouter.ai/api/v1)
    but Claude Code talks to http://127.0.0.1:20128/v1

## The profile file

Path: `~/.config/xsync/profiles.toml`. Mode: `0600`.

    active = "9router"

    [profiles.9router]
    base_url = "http://127.0.0.1:20128/v1"
    api_key = "sk-..."
    wire_api = "responses"
    endpoint_type = "cliproxy"

    [profiles.openrouter]
    base_url = "https://openrouter.ai/api/v1"
    api_key_env = "OPENROUTER_API_KEY"
    wire_api = "chat"
    endpoint_type = "generic"
    exclude = ["*-embedding-*"]

A profile uses `api_key` or `api_key_env`, but not both. Use `api_key_env`
when you share the file. A profile with neither key sends no `Authorization`
header.

The `endpoint_type` value is `generic` or `cliproxy`. Existing profiles use
`generic` when the field is absent.

## The filters

A glob pattern selects a model by name. The sign `*` means any characters.

| Rule | Effect |
|---|---|
| `include = []` | Keep every model. This is the default. |
| `include = ["openai/*", "anthropic/*"]` | Keep only the models of those two providers. |
| `exclude = ["*embedding*"]` | Drop every model with `embedding` in the name. |
| `exclude = ["*-image-*", "*-tts-*"]` | Drop the image models and the speech models. |

The `exclude` list always wins over the `include` list. Use the filters when
an endpoint serves many models that you never choose in a harness.

The match ignores the letter case.

## Safety

**The everyday commands write nothing outside `~/.config/xsync/`.** Only
`apply` touches a real harness configuration.

- The catalog write is atomic. xSync writes a temporary file, confirms the
  JSON, and then replaces the target. It keeps one backup with the suffix
  `.bak`.
- A harness writes its own settings file while it runs. Therefore `apply`
  hashes the file before the edit and again before the replace. On a
  difference it stops and writes nothing.
- Claude Code rewrites `settings.json` from its own memory. An `apply` that
  runs while Claude Code is open can therefore disappear. Close Claude Code
  first, or use `xsync claude` and change nothing.
- `apply --dry-run` writes nothing at all. It does not point the harness at
  the endpoint either.
- `apply --reset` removes the recorded settings and restores the previous values of replaced keys.
  The command does not restore an old copy of the complete settings file.
  Codex reset also clears the model cache and removes the endpoint model selection when it restores the OpenAI provider.
- `apply --reset` keeps the backup `<name>.bak`. The backup is the only way
  back after a reset.
- With no state file, `apply --reset` prints the keys that it would remove
  and then stops. Add `--force` to continue. The command then asks a
  question before it removes anything. Add `--yes` to answer the question in
  a script.
- Codex reset does not require `--force` when no custom catalog or provider configuration needs removal.
- The setup hides the API key while you type it. It then shows the first
  five characters only, and it never shows the length of the key.
- xSync never prints a whole API key.

## The color

xSync writes color for a terminal. It writes plain text for a pipe, for a
dumb terminal, and when `NO_COLOR` holds a value. Set `FORCE_COLOR=1` to
keep the color in a pipe.

## What xSync reads and what it writes

| File | Read | Write |
|---|---|---|
| `{base_url}/models` | yes | no |
| `~/.config/xsync/profiles.toml` | yes | yes |
| `~/.config/xsync/homes/...` | yes | yes |
| `~/.codex/config.toml` | yes | only with `apply` |
| `~/.codex/<profile>-models.json` | yes | only with `apply` |
| `~/.claude/settings.json` | yes | only with `apply` |
| `~/.config/opencode/opencode.json` | yes | only with `apply` |
| `~/.pi/agent/models.json` | yes | only with `apply` |
| `~/.omp/agent/models.yml` | yes | only with `apply` |
| `.xsync-state.json` in each home | yes | only with `apply` |

xSync copies the real settings file when it builds an isolated home. It
reads that file. It never writes it.

## The catalog fields

The Codex catalog needs 33 fields for each model. The endpoint supplies
approximately 8 of them:

| Codex field | Source |
|---|---|
| `slug` | the model id |
| `context_window`, `max_context_window` | the context window |
| `input_modalities` | the vision flag |
| `supported_reasoning_levels` | the reasoning flag |
| `supports_parallel_tool_calls` | the tool flag |
| `supports_search_tool` | the search flag |

The other fields come from `adapters/rules/codex.toml`. The rules apply in
four layers. A later layer wins:

1. `[defaults]`
2. `[prefix.<first slug segment>]`
3. `[name."<last slug segment>"]`
4. `[slug."<exact slug>"]`

Some endpoints omit capability fields. In that case, the Codex rules supply
the capability values.

Every default value comes from a catalog that works with codex-cli 0.153.4.

## Develop

    uv run --extra dev pytest

No test opens a network connection.

## License

MIT.
