Skip to content
Open
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
42 changes: 42 additions & 0 deletions dev-notes/architecture/pi-config-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Pi configuration import

Tau can now import a best-effort subset of an existing Pi configuration into Tau's provider settings.

## What was added

- `tau config import-pi <path>` imports provider/model settings from a Pi JSON or TOML config file.
- `--dry-run` prints the resulting Tau `providers.json` payload without writing.
- `--yes` allows updating an existing `~/.tau/providers.json`; without it, existing provider settings are protected.
- `--default-pi-config` searches common Pi locations such as `~/.pi/config.json` and `~/.config/pi/config.toml`.

## Mapping decisions

The importer lives in `tau_coding` because it deals with CLI behavior and user-level config files. The portable `tau_agent` package remains independent of Pi/Tau filesystem conventions.

Supported import fields are intentionally conservative:

- provider name (`provider`, `default_provider`, provider table/list names)
- model (`model`, `default_model`, `defaultModel`)
- base URL (`base_url`, `baseUrl`, `baseURL`, `api_base`)
- API key env var (`api_key_env`, `apiKeyEnv`, `api_key_env_var`)

Raw API keys are not copied into Tau. When the Pi config contains a raw key, Tau warns and records only the expected environment variable reference. Unknown fields also generate warnings so users can decide whether to migrate them manually.

## How it maps to Pi

Pi and Tau share provider/model configuration concepts, but Tau splits durable provider metadata (`catalog.toml`) from runtime preferences (`providers.json`). The importer writes through Tau's existing provider settings helpers so custom provider definitions are persisted using the same path as `tau setup` and provider/model picker updates.

## How to test

```bash
uv run pytest tests/test_pi_config_import.py
uv run pytest
uv run ruff check .
uv run mypy
```

Manual dry run example:

```bash
tau --dry-run config import-pi ~/.pi/config.json
```
77 changes: 77 additions & 0 deletions src/tau_coding/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
from tau_coding import __version__
from tau_coding.catalog_loader import user_catalog_path
from tau_coding.credentials import FileCredentialStore
from tau_coding.pi_config_import import (
PiConfigImportError,
default_pi_config_path,
import_pi_config,
plan_pi_config_import,
)
from tau_coding.provider_config import (
DEFAULT_MODEL,
DEFAULT_PROVIDER_NAME,
Expand Down Expand Up @@ -129,6 +135,40 @@ def setup_command(
typer.echo(f"Set {provider.api_key_env} before running Tau with this provider.", err=True)


def import_pi_config_command(
source: Path | None,
*,
use_default: bool = False,
dry_run: bool = False,
yes: bool = False,
) -> None:
"""Import a Pi provider/model config into Tau."""
resolved_source = source
if resolved_source is None:
if not use_default:
raise RuntimeError("Usage: tau config import-pi <path> [--dry-run] [--yes]")
resolved_source = default_pi_config_path()
if resolved_source is None:
raise RuntimeError("No Pi config found in a default location")

if dry_run:
plan = plan_pi_config_import(resolved_source)
typer.echo(plan.to_json_text(), nl=False)
_render_pi_import_warnings(plan.warnings)
return

plan, path = import_pi_config(resolved_source, overwrite_existing=yes)
typer.echo(
f"Imported Pi providers {', '.join(plan.imported_providers)} from {plan.source} to {path}"
)
_render_pi_import_warnings(plan.warnings)


def _render_pi_import_warnings(warnings: tuple[str, ...]) -> None:
for warning in warnings:
typer.echo(f"Warning: {warning}", err=True)


@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
Expand Down Expand Up @@ -178,6 +218,18 @@ def main(
bool,
typer.Option("--set-default/--no-set-default", help="Make setup provider the default."),
] = True,
config_import_default: Annotated[
bool,
typer.Option("--default-pi-config", help="Import from the first known Pi config path."),
] = False,
config_import_dry_run: Annotated[
bool,
typer.Option("--dry-run", help="Print imported Tau config without writing."),
] = False,
config_import_yes: Annotated[
bool,
typer.Option("--yes", "-y", help="Update existing Tau provider settings."),
] = False,
cwd: Annotated[
Path | None,
typer.Option("--cwd", help="Working directory for built-in coding tools."),
Expand Down Expand Up @@ -259,6 +311,19 @@ def main(
)
raise typer.Exit()

if prompt_option is None and command == "config":
try:
config_source = _parse_config_import_cli_args(positional_args[1:])
import_pi_config_command(
config_source,
use_default=config_import_default,
dry_run=config_import_dry_run,
yes=config_import_yes,
)
except (RuntimeError, PiConfigImportError) as exc:
raise typer.BadParameter(str(exc)) from exc
raise typer.Exit()

if prompt_option is None:
notice = _startup_update_notice()
try:
Expand Down Expand Up @@ -366,6 +431,18 @@ async def export_session_command(
)


def _parse_config_import_cli_args(args: list[str]) -> Path | None:
if not args:
raise RuntimeError("Usage: tau config import-pi <path> [--dry-run] [--yes]")
if args[0] != "import-pi":
raise RuntimeError("Usage: tau config import-pi <path> [--dry-run] [--yes]")
if len(args) == 1:
return None
if len(args) == 2:
return Path(args[1]).expanduser()
raise RuntimeError("Usage: tau config import-pi <path> [--dry-run] [--yes]")


def _parse_export_cli_args(args: list[str]) -> tuple[str, Path | None, str | None]:
if not args:
raise RuntimeError("Usage: tau export <session-id-or-jsonl> [--format html|jsonl] [output]")
Expand Down
Loading