diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index bef6ba475..6787c37bc 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -63,15 +63,18 @@ jobs: # lagging a publish by a few seconds. run: | get_script() { - if [ "$SOURCE" = "url" ]; then curl -fsSL https://kitaru.ai/install; else cat install.sh; fi + if [ "$SOURCE" = "url" ]; then curl -fsSL https://kitaru.ai/install; else cat "$GITHUB_WORKSPACE/install.sh"; fi } + # Run from an empty directory: the checkout is itself a Python project, + # which would put the installer into project mode. + cd "$(mktemp -d)" for attempt in 1 2 3 4 5; do - get_script | bash -s -- --no-login && exit 0 + get_script | bash -s -- && exit 0 echo "installer attempt $attempt failed; retrying in 30s" >&2 sleep 30 done echo "::group::verbose rerun for diagnostics" - get_script | bash -s -- --no-login --verbose || true + get_script | bash -s -- --verbose || true echo "::endgroup::" exit 1 - name: Verify @@ -98,13 +101,26 @@ jobs: | grep -q ".local/bin" || { echo "PATH not persisted"; exit 1; } - name: Re-run is an upgrade, not an error shell: bash - run: cat install.sh | bash -s -- --no-login --no-skills --no-mcp --quiet + run: cd "$(mktemp -d)" && cat "$GITHUB_WORKSPACE/install.sh" | bash -s -- + --no-skills --no-mcp --quiet + - name: Inside a project, install into that project's environment + shell: bash + run: | + set -euxo pipefail + export PATH="$HOME/.local/bin:$PATH" + cd "$(mktemp -d)" + uv init --quiet -p 3.12 --name demo-agent . + cat "$GITHUB_WORKSPACE/install.sh" | bash -s -- --no-skills --no-mcp + grep -q 'kitaru\[cli,mcp,worker\]' pyproject.toml + uv run kitaru --version + uv run kitaru-mcp --help >/dev/null - name: A failed install must exit nonzero (quiet path) shell: bash # Regression for the status-swallowing bug in `quiet`: an impossible # version pin makes `uv tool install` fail, and that must surface. run: |- - if cat install.sh | bash -s -- --no-login --no-skills --no-mcp --version=99.99.99; then + cd "$(mktemp -d)" + if cat "$GITHUB_WORKSPACE/install.sh" | bash -s -- --no-skills --no-mcp --version=99.99.99; then echo "installer exited 0 on a failed install" >&2; exit 1 fi echo "failed install correctly exited nonzero" diff --git a/README.md b/README.md index bdc311677..23008fa93 100644 --- a/README.md +++ b/README.md @@ -44,25 +44,25 @@ Kitaru turns that history into something you can test: ## ⚡ Get started -**1. Install and log in.** One line installs the CLI and MCP server (via `uv`), the coding-agent skills, registers the MCP server with Claude Code and Codex, and starts the local server if Docker is running: +**1. Install.** Open a terminal in your agent's repository and run one line. It adds Kitaru to that project's environment with `uv` (the worker that replays your agent has to live next to its dependencies), installs the coding-agent skills, and registers the MCP server with Claude Code and Codex. It ends by printing the two ways to get a server: `kitaru login --local` (Docker, free) or `kitaru login` for the managed cloud (14-day trial, no credit card required). ```bash curl -fsSL https://kitaru.ai/install | bash ``` -Already in Claude Code, Codex, or Cursor? Paste this instead and let it run the same installer: +Already in Claude Code, Codex, or Cursor? Open the repository there and paste this instead: ``` -Set up Kitaru on this machine by following https://kitaru.ai/install.md. Use the one-line installer, tell me what it did, and stop before logging in if Docker is not running. +Set up Kitaru in this repository by following https://kitaru.ai/install.md. Use the one-line installer and tell me what it did. ``` -Prefer to do it by hand, or want Kitaru inside your project's environment? Choose managed cloud with `kitaru login`, or provision the local FastAPI + Postgres server with Docker: +Prefer to do it by hand? Add Kitaru to your project's environment, then choose managed cloud with `uv run kitaru login`, or provision the local FastAPI + Postgres server with Docker: ```bash uv add "kitaru[cli,worker,mcp]" kitaru-pydantic-ai # or: pip install -kitaru login # managed cloud; 14-day trial, no credit card required -kitaru login --local # local server in Docker -# or: kitaru login +uv run kitaru login # managed cloud; 14-day trial, no credit card required +uv run kitaru login --local # local server in Docker +# or: uv run kitaru login ``` **2. Make your coding assistant Kitaru-capable.** This is the intended way to drive Kitaru: skills teach the method, and the MCP server gives your assistant bounded operations. diff --git a/docs/book/agent-native/setup.md b/docs/book/agent-native/setup.md index cf4f6aaac..5207bd5a0 100644 --- a/docs/book/agent-native/setup.md +++ b/docs/book/agent-native/setup.md @@ -13,7 +13,7 @@ Kitaru observes your production agents; your coding assistant is how you talk to Skills and MCP work together: the skills say how to work, and the server bounds what can be touched. {% hint style="success" %} -Used the [one-line installer](../getting-started/installation.md)? It already installed the MCP server, registered it with Claude Code and Codex (user scope, pointed at `http://localhost:8000` in `standard` mode), and installed the skills. Skip to [Capability modes and tools](#capability-modes-and-tools) unless you use another assistant or a different server URL. +Used the [one-line installer](../getting-started/installation.md)? It already installed the MCP server, registered it with Claude Code (in your repo's `.mcp.json` when run inside a repository, user scope otherwise) and Codex, pointed at `http://localhost:8000` in `standard` mode, and installed the skills. Skip to [Capability modes and tools](#capability-modes-and-tools) unless you use another assistant or a different server URL. {% endhint %} ## Install the MCP server diff --git a/docs/book/deploy/authentication.md b/docs/book/deploy/authentication.md index f7bf9f604..5c4fa0a1c 100644 --- a/docs/book/deploy/authentication.md +++ b/docs/book/deploy/authentication.md @@ -18,7 +18,7 @@ Two schemes, set by `KITARU_SERVER_AUTH_SCHEME`: kitaru login https://kitaru.internal.example.com ``` -Interactive login uses a device flow (the CLI shows a short `XXXX-XXXX` code and opens your browser) or a password prompt. Credentials are stored separately for each server. Logging in selects that server for later commands; you can override it with `--server` or `KITARU_API_URL`. Non-interactive variants: +Interactive login uses a device flow (the CLI opens your browser to complete the login, or prints a verification link when browser opening is disabled) or a password prompt. Credentials are stored separately for each server. Logging in selects that server for later commands; you can override it with `--server` or `KITARU_API_URL`. Non-interactive variants: ```bash kitaru login https://... --username you --password-stdin diff --git a/docs/book/getting-started/installation.md b/docs/book/getting-started/installation.md index c4f89df9e..68297e391 100644 --- a/docs/book/getting-started/installation.md +++ b/docs/book/getting-started/installation.md @@ -5,37 +5,59 @@ icon: download # Installation -One command installs everything a first session needs. It needs [Docker](https://docs.docker.com/get-started/get-docker/) for the local server (the CLI installs without it) and works on macOS, Linux, WSL, and Git Bash on Windows: +Open a terminal **in your agent's repository** and run: ```bash curl -fsSL https://kitaru.ai/install | bash ``` -What it does, in order: +That one command: -1. Installs the `kitaru` CLI and the `kitaru-mcp` server into an isolated [uv](https://docs.astral.sh/uv/) environment, installing uv first if you do not have it. No system Python is required. -2. Installs the [agent skills](../agent-native/setup.md) into `~/.agents/skills`, and into `~/.claude/skills` and `~/.codex/skills` when Claude Code or Codex is installed. -3. Registers the MCP server with Claude Code and Codex. -4. Runs `kitaru login --local`, which starts the server and PostgreSQL in Docker and logs you in. If Docker is not running, it prints that command for later instead. +1. Adds `kitaru[cli,mcp,worker]` to the project's environment with `uv add`. The worker that replays your agent has to live next to your agent's dependencies, so this is the environment that matters. uv is installed first if you do not have it; no system Python and no `sudo` are needed. +2. Installs the [agent skills](../agent-native/setup.md) into `~/.agents/skills`, plus `~/.claude/skills` and `~/.codex/skills` when Claude Code or Codex is installed. +3. Registers the MCP server with Claude Code (in the repo's `.mcp.json`) and Codex, as `uv run --directory kitaru-mcp`. +4. Prints the two ways to get a server, and stops: -Nothing needs `sudo`, everything lands under your home directory, and running it again upgrades. Options: `--server https://your-team.kitaru.ai` logs in to a team server instead of starting a local one, `--with kitaru-pydantic-ai` adds an adapter to the same environment, `--no-login`, `--no-skills`, `--no-mcp` skip steps, and `--no-modify-path` leaves your shell rc files alone. `bash -s -- --help` after the pipe lists everything. +``` +uv run kitaru login --local local, in Docker. Free, open source. +uv run kitaru login managed cloud. 14-day trial, no credit card required. +``` + +(Inside a project Kitaru is not on your PATH, hence `uv run`. The isolated install uses plain `kitaru`.) + +Works on macOS, Linux, WSL, and Git Bash on Windows. Running it again upgrades. + +{% hint style="info" %} +**Not in a repository?** Run it anywhere and it installs an isolated `kitaru` CLI on your PATH instead (a `uv tool` environment under `~/.local/share/uv/tools/kitaru`). That is enough to log in, import traces, run evaluators, and serve MCP, but replays need Kitaru inside the agent's own project, so re-run the installer there when you have one. `--project` and `--global` force either mode. +{% endhint %} + +| Option | Effect | +| --- | --- | +| `--version 0.24.0` | Pin a Kitaru release (`--pre` allows pre-releases) | +| `--with kitaru-pydantic-ai` | Also install a package into the same environment (repeatable) | +| `--server https://your-team.kitaru.ai` | Point the MCP server at a team server instead of `http://localhost:8000` | +| `--project` / `--global` | Force the in-project or the isolated install | +| `--no-skills`, `--no-mcp` | Skip those steps | +| `--no-modify-path` | Leave your shell rc files alone (global mode) | + +`curl -fsSL https://kitaru.ai/install | bash -s -- --help` lists everything, with environment-variable equivalents. -Prefer to do it by hand? The installer is three commands, which you can run yourself: +**Prefer to do it by hand?** Inside your repository, the installer is equivalent to: ```bash -uv tool install "kitaru[cli,mcp,worker]" # CLI + MCP server, isolated from your projects -npx skills add zenml-io/kitaru-skills # the coding-agent skills -kitaru login # managed cloud; 14-day trial, no credit card required -kitaru login --local # local server in Docker -# or: kitaru login # an existing managed or self-hosted workspace +uv add "kitaru[cli,mcp,worker]" kitaru-pydantic-ai # into this project; pick your adapter +npx skills add zenml-io/kitaru-skills # the coding-agent skills +uv run kitaru login # managed cloud; 14-day trial, no credit card required +uv run kitaru login --local # local server in Docker +# or: uv run kitaru login # an existing managed or self-hosted workspace ``` -then point your assistant at `kitaru-mcp` as described in [Set up your coding agent](../agent-native/setup.md). +plus registering `uv run kitaru-mcp --server http://localhost:8000 --mode standard` with your assistant, as described in [Set up your coding agent](../agent-native/setup.md). -Already inside Claude Code, Codex, or Cursor? Paste this instead and it runs the same installer for you: +**Already inside Claude Code, Codex, or Cursor?** Open your agent's repository there, paste this, and it runs the same installer for you: ``` -Set up Kitaru on this machine by following https://kitaru.ai/install.md. Use the one-line installer, tell me what it did, and stop before logging in if Docker is not running. +Set up Kitaru in this repository by following https://kitaru.ai/install.md. Use the one-line installer and tell me what it did. ``` ## Verify @@ -44,7 +66,7 @@ Set up Kitaru on this machine by following https://kitaru.ai/install.md. Use the kitaru doctor ``` -It checks the CLI, the server connection, authentication, and whether the skills are installed. If the installer skipped login because Docker was not running, start Docker and run `kitaru login --local`; the section below covers the local server's lifecycle. +It checks the CLI, the server connection, authentication, and whether the skills are installed. Server connection and authentication fail until you have run `kitaru login --local` (needs [Docker](https://docs.docker.com/get-started/get-docker/)) or `kitaru login` for the managed cloud; the sections below cover both. Then read the [Quickstart](quickstart.md). It is written as prompts for your coding agent, and everything it needs is now in place. @@ -85,7 +107,7 @@ Node applications can also reuse a developer's selected CLI login without export ## Other ways to install -The installer is the right choice for the CLI on a laptop. The paths below are for when Kitaru has to live inside a specific environment: your agent's own code, a Node project, CI, or a machine where you only want the skills. +The installer run inside your agent's repository already installs into that project. The paths below are for adding the SDK by hand, Node projects, CI, or a machine where you only want the skills. Kitaru is three pieces: the **SDK + CLI**, a **server** your team shares (self-hosted, one per team), and **workers** that execute replays and evaluations in your environment. The CLI, server, and workers require **Python 3.11 or newer**; TypeScript agents use Node **22.22 or newer in the Node 22 release line** and connect to the same server. The server stores everything in **PostgreSQL**, provisioned for you locally by `kitaru login --local`; a [self-hosted deployment](../deploy/README.md) brings its own. Workers are plain processes (`kitaru worker start`) that run wherever your agent's environment lives; for containerized fleets, the published `zenmldocker/kitaru-worker` image works out of the box (see [Workers in production](../deploy/workers.md)). diff --git a/install.sh b/install.sh index 4ce9a5420..bd729f16b 100755 --- a/install.sh +++ b/install.sh @@ -3,20 +3,25 @@ # # curl -fsSL https://kitaru.ai/install | bash # curl -fsSL https://kitaru.ai/install | bash -s -- --pre -# curl -fsSL https://kitaru.ai/install | bash -s -- --server https://team.kitaru.ai # # What it does, in order: # 1. Makes sure `uv` is available (installs it from astral.sh if not). -# 2. `uv tool install kitaru[cli,mcp,worker]` into an isolated environment, -# with a uv-managed Python if the machine has none. Puts `kitaru` and -# `kitaru-mcp` on PATH (~/.local/bin) for future terminals. +# 2. Installs kitaru[cli,mcp,worker]. Where depends on where you run it: +# - inside a Python project (pyproject.toml or uv.lock in the current +# directory): `uv add` into that project's environment, so its worker +# can replay your agent alongside your agent's own dependencies; +# - anywhere else: `uv tool install` into an isolated environment, with +# `kitaru` and `kitaru-mcp` on PATH (~/.local/bin). Good for the CLI, +# MCP server, imports and evaluators; replays need the project form. +# --project / --global force either. # 3. Installs the Kitaru agent skills (zenml-io/kitaru-skills) from the # repository tarball into ~/.agents/skills, plus ~/.claude/skills and # ~/.codex/skills when those CLIs are installed. No Node needed. # 4. Registers the Kitaru MCP server with Claude Code and Codex if their # CLIs are installed; prints the JSON for everything else. -# 5. If Docker is running and there is a terminal, runs `kitaru login --local` -# to start the local server. Otherwise tells you the one command to run. +# 5. Stops there and prints the two ways to get a server: local in Docker +# (`kitaru login --local`) or the managed cloud (`kitaru login`). Login +# is a decision, so the script does not make it for you. # # Nothing here needs sudo. Everything lands under $HOME. Re-running upgrades. # @@ -36,14 +41,14 @@ set -euo pipefail # --------------------------------------------------------------------------- KITARU_VERSION="${KITARU_VERSION:-}" # pin, e.g. 0.24.0 KITARU_PRE="${KITARU_PRE:-0}" # allow pre-releases -KITARU_SERVER="${KITARU_SERVER:-}" # team server URL instead of --local +KITARU_SERVER="${KITARU_SERVER:-}" # point the MCP server at a team server KITARU_SKIP_SKILLS="${KITARU_SKIP_SKILLS:-0}" KITARU_SKIP_MCP="${KITARU_SKIP_MCP:-0}" -KITARU_SKIP_LOGIN="${KITARU_SKIP_LOGIN:-0}" KITARU_QUIET="${KITARU_QUIET:-0}" KITARU_VERBOSE="${KITARU_VERBOSE:-0}" KITARU_WITH=() # extra packages, e.g. kitaru-pydantic-ai -KITARU_PYTHON="${KITARU_PYTHON:-3.12}" # uv-managed Python if none suitable +KITARU_PYTHON="${KITARU_PYTHON:-3.12}" # uv-managed Python if none suitable (global mode) +KITARU_SCOPE="${KITARU_SCOPE:-auto}" # auto | project | global KITARU_SKILLS_REPO="${KITARU_SKILLS_REPO:-zenml-io/kitaru-skills}" KITARU_LOCAL_URL="${KITARU_LOCAL_URL:-http://localhost:8000}" KITARU_MCP_MODE="${KITARU_MCP_MODE:-standard}" @@ -58,18 +63,21 @@ Usage: install.sh [options] --pre Allow pre-release versions --with=PKG Also install PKG into the same environment (repeatable), e.g. --with=kitaru-pydantic-ai --with=kitaru-langgraph - --server=URL Log in to a team/self-hosted server instead of starting - a local one + --project Install into the Python project in the current directory + (uv add). Default when a pyproject.toml or uv.lock is here. + --global Install into an isolated uv tool environment on PATH. + Default anywhere else. + --server=URL Point the MCP server at a team/self-hosted server + (default: the local server, http://localhost:8000) --no-skills Skip installing the coding-agent skills --no-mcp Skip registering the MCP server with Claude Code / Codex - --no-login Skip `kitaru login` (just install the CLI) --no-modify-path Do not edit shell rc files; you add ~/.local/bin yourself --quiet Only print errors --verbose Print every command -h, --help This text Environment equivalents: KITARU_VERSION, KITARU_PRE=1, KITARU_SERVER, -KITARU_SKIP_SKILLS=1, KITARU_SKIP_MCP=1, KITARU_SKIP_LOGIN=1, +KITARU_SCOPE=project|global, KITARU_SKIP_SKILLS=1, KITARU_SKIP_MCP=1, KITARU_NO_MODIFY_PATH=1, KITARU_QUIET=1, KITARU_VERBOSE=1, KITARU_PYTHON (default 3.12), NO_COLOR. USAGE @@ -84,9 +92,10 @@ while [ $# -gt 0 ]; do --with) shift; KITARU_WITH+=("${1:-}") ;; --server=*) KITARU_SERVER="${1#*=}" ;; --server) shift; KITARU_SERVER="${1:-}" ;; + --project) KITARU_SCOPE=project ;; + --global) KITARU_SCOPE=global ;; --no-skills) KITARU_SKIP_SKILLS=1 ;; --no-mcp) KITARU_SKIP_MCP=1 ;; - --no-login) KITARU_SKIP_LOGIN=1 ;; --no-modify-path) KITARU_NO_MODIFY_PATH=1 ;; --quiet) KITARU_QUIET=1 ;; --verbose) KITARU_VERBOSE=1 ;; @@ -218,42 +227,82 @@ persist_path() { } # --------------------------------------------------------------------------- -# 2. kitaru CLI + MCP server, in an isolated tool environment +# 2. kitaru CLI + MCP server: into this project, or an isolated tool env # --------------------------------------------------------------------------- SPEC="kitaru[cli,mcp,worker]" [ -n "$KITARU_VERSION" ] && SPEC="${SPEC}==${KITARU_VERSION}" +EXTRA_PKGS=() +for pkg in "${KITARU_WITH[@]:-}"; do [ -n "$pkg" ] && EXTRA_PKGS+=("$pkg"); done -UV_ARGS=(tool install --upgrade --quiet --python "$KITARU_PYTHON") -[ "$KITARU_PRE" = "1" ] && UV_ARGS+=(--prerelease allow) -for pkg in "${KITARU_WITH[@]:-}"; do - [ -n "$pkg" ] && UV_ARGS+=(--with "$pkg") -done - -step "Installing $SPEC" -quiet "$UV" "${UV_ARGS[@]}" "$SPEC" || die "uv tool install failed. Re-run with --verbose for details." +PROJECT_DIR="$PWD" +if [ "$KITARU_SCOPE" = "auto" ]; then + if [ -f "$PROJECT_DIR/pyproject.toml" ] || [ -f "$PROJECT_DIR/uv.lock" ]; then + KITARU_SCOPE=project + else + KITARU_SCOPE=global + fi +fi -# uv puts tool executables in its tool bin dir; make sure future shells see it. -TOOL_BIN="$("$UV" tool dir --bin 2>/dev/null || echo "$HOME/.local/bin")" -case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) TOOL_BIN="$(cygpath -u "$TOOL_BIN" 2>/dev/null || echo "$TOOL_BIN")" ;; esac -ensure_path "$TOOL_BIN" -if [ "$KITARU_NO_MODIFY_PATH" = "1" ]; then - note "Not editing shell rc files (--no-modify-path). Make sure $TOOL_BIN is on your PATH." +if [ "$KITARU_SCOPE" = "project" ]; then + # ---- into the project in the current directory --------------------------- + # The worker runs your agent, so it has to live where your agent's + # dependencies live. `uv add` puts kitaru into this project's environment + # (creating .venv if needed) and records it in pyproject.toml. + [ -f "$PROJECT_DIR/pyproject.toml" ] || die "--project needs a pyproject.toml in $PROJECT_DIR (run \`uv init\` first, or use --global)." + step "Installing $SPEC into this project ($PROJECT_DIR)" + UV_ADD=(add --quiet) + [ "$KITARU_PRE" = "1" ] && UV_ADD+=(--prerelease allow) + quiet "$UV" "${UV_ADD[@]}" "$SPEC" "${EXTRA_PKGS[@]:+${EXTRA_PKGS[@]}}" \ + || die "uv add failed (uv's message is above; Kitaru needs Python >= 3.11). To install outside this project instead: --global" + VENV_DIR="${UV_PROJECT_ENVIRONMENT:-$PROJECT_DIR/.venv}" + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) TOOL_BIN="$VENV_DIR/Scripts" ;; + *) TOOL_BIN="$VENV_DIR/bin" ;; + esac + KITARU_BIN="$TOOL_BIN/kitaru" + [ -x "$KITARU_BIN" ] || [ -x "$KITARU_BIN.exe" ] || die "uv reported success but $KITARU_BIN is missing. Re-run with --verbose." + ok "kitaru $("$KITARU_BIN" --version 2>/dev/null) installed into this project's environment" + note "environment: $VENV_DIR (recorded in pyproject.toml)" + note "run it as: uv run kitaru ... (or activate the environment)" + # MCP: run through uv so the assistant gets this project's environment. + MCP_CMD=("$UV" run --directory "$PROJECT_DIR" kitaru-mcp) + CLAUDE_SCOPE=project else - quiet "$UV" tool update-shell || true - persist_path "$TOOL_BIN" -fi + # ---- isolated tool environment on PATH ----------------------------------- + UV_ARGS=(tool install --upgrade --quiet --python "$KITARU_PYTHON") + [ "$KITARU_PRE" = "1" ] && UV_ARGS+=(--prerelease allow) + for pkg in "${EXTRA_PKGS[@]:+${EXTRA_PKGS[@]}}"; do UV_ARGS+=(--with "$pkg"); done + + step "Installing $SPEC (isolated, on PATH)" + quiet "$UV" "${UV_ARGS[@]}" "$SPEC" || die "uv tool install failed. Re-run with --verbose for details." + + # uv puts tool executables in its tool bin dir; make sure future shells see it. + TOOL_BIN="$("$UV" tool dir --bin 2>/dev/null || echo "$HOME/.local/bin")" + TOOL_ENV="$("$UV" tool dir 2>/dev/null || echo "$HOME/.local/share/uv/tools")/kitaru" + case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) TOOL_BIN="$(cygpath -u "$TOOL_BIN" 2>/dev/null || echo "$TOOL_BIN")" ;; esac + ensure_path "$TOOL_BIN" + if [ "$KITARU_NO_MODIFY_PATH" = "1" ]; then + note "Not editing shell rc files (--no-modify-path). Make sure $TOOL_BIN is on your PATH." + else + quiet "$UV" tool update-shell || true + persist_path "$TOOL_BIN" + fi -KITARU_BIN="$TOOL_BIN/kitaru" -[ -x "$KITARU_BIN" ] || [ -x "$KITARU_BIN.exe" ] || die "uv reported success but $KITARU_BIN is missing. Re-run with --verbose." -ok "kitaru $("$KITARU_BIN" --version 2>/dev/null) installed" -note "$TOOL_BIN/kitaru, $TOOL_BIN/kitaru-mcp" -# Warn if a different kitaru was already reachable on the PATH the user -# started with (a pip or pipx install, say): depending on their rc order it -# may keep winning in new terminals. -hash -r -RESOLVED_KITARU="$(PATH="$ORIG_PATH" command -v kitaru 2>/dev/null || true)" -if [ -n "$RESOLVED_KITARU" ] && [ "$RESOLVED_KITARU" != "$KITARU_BIN" ] && [ "$RESOLVED_KITARU" != "$KITARU_BIN.exe" ]; then - warn "Another kitaru at $RESOLVED_KITARU ($("$RESOLVED_KITARU" --version 2>/dev/null || echo unknown)) shadows the one just installed. Remove it or put $TOOL_BIN first on PATH." + KITARU_BIN="$TOOL_BIN/kitaru" + [ -x "$KITARU_BIN" ] || [ -x "$KITARU_BIN.exe" ] || die "uv reported success but $KITARU_BIN is missing. Re-run with --verbose." + ok "kitaru $("$KITARU_BIN" --version 2>/dev/null) installed" + note "isolated environment: $TOOL_ENV (uv tool; no other Python touched)" + note "commands: $TOOL_BIN/kitaru, $TOOL_BIN/kitaru-mcp" + # Warn if a different kitaru was already reachable on the PATH the user + # started with (a pip or pipx install, say): depending on their rc order it + # may keep winning in new terminals. + hash -r + RESOLVED_KITARU="$(PATH="$ORIG_PATH" command -v kitaru 2>/dev/null || true)" + if [ -n "$RESOLVED_KITARU" ] && [ "$RESOLVED_KITARU" != "$KITARU_BIN" ] && [ "$RESOLVED_KITARU" != "$KITARU_BIN.exe" ]; then + warn "Another kitaru at $RESOLVED_KITARU ($("$RESOLVED_KITARU" --version 2>/dev/null || echo unknown)) shadows the one just installed. Remove it or put $TOOL_BIN first on PATH." + fi + MCP_CMD=("$TOOL_BIN/kitaru-mcp") + CLAUDE_SCOPE=user fi # --------------------------------------------------------------------------- @@ -324,7 +373,7 @@ fi # 4. MCP server registration # --------------------------------------------------------------------------- MCP_SERVER_URL="${KITARU_SERVER:-$KITARU_LOCAL_URL}" -MCP_BIN="$TOOL_BIN/kitaru-mcp" +MCP_ARGS=(--server "$MCP_SERVER_URL" --mode "$KITARU_MCP_MODE") if [ "$KITARU_SKIP_MCP" = "1" ]; then note "Skipping MCP registration (--no-mcp)" @@ -332,18 +381,20 @@ else step "Registering the Kitaru MCP server" registered=0 if have claude; then - # User-scope MCP servers live in ~/.claude.json. Snapshot it so a failed - # replace can put the previous entry back instead of leaving none. - CLAUDE_CFG="$HOME/.claude.json"; CLAUDE_CFG_BAK="" + # User-scope MCP servers live in ~/.claude.json, project-scope ones in + # ./.mcp.json. Snapshot the file so a failed replace can put the previous + # entry back instead of leaving none. + if [ "$CLAUDE_SCOPE" = "project" ]; then CLAUDE_CFG="$PROJECT_DIR/.mcp.json"; else CLAUDE_CFG="$HOME/.claude.json"; fi + CLAUDE_CFG_BAK="" if [ -f "$CLAUDE_CFG" ]; then CLAUDE_CFG_BAK="$(mktemp)"; cp "$CLAUDE_CFG" "$CLAUDE_CFG_BAK" fi if claude mcp get kitaru >/dev/null 2>&1; then - quiet claude mcp remove --scope user kitaru || true + quiet claude mcp remove --scope "$CLAUDE_SCOPE" kitaru || true fi - if quiet claude mcp add --scope user kitaru -- "$MCP_BIN" --server "$MCP_SERVER_URL" --mode "$KITARU_MCP_MODE" \ + if quiet claude mcp add --scope "$CLAUDE_SCOPE" kitaru -- "${MCP_CMD[@]}" "${MCP_ARGS[@]}" \ && claude mcp get kitaru >/dev/null 2>&1; then - ok "Claude Code: MCP server 'kitaru' (user scope)"; registered=1 + ok "Claude Code: MCP server 'kitaru' ($CLAUDE_SCOPE scope)"; registered=1 else if [ -n "$CLAUDE_CFG_BAK" ]; then cp "$CLAUDE_CFG_BAK" "$CLAUDE_CFG"; fi warn "Claude Code: could not register MCP server; previous config left as it was." @@ -352,7 +403,7 @@ else fi if have codex; then # `codex mcp add` overwrites an existing name, so no remove first. - if quiet codex mcp add kitaru -- "$MCP_BIN" --server "$MCP_SERVER_URL" --mode "$KITARU_MCP_MODE"; then + if quiet codex mcp add kitaru -- "${MCP_CMD[@]}" "${MCP_ARGS[@]}"; then ok "Codex: MCP server 'kitaru'"; registered=1 else warn "Codex: could not register MCP server" @@ -360,97 +411,49 @@ else fi if [ "$registered" = "0" ]; then note "No Claude Code or Codex CLI found. For Cursor or any MCP client, add:" - note " {\"mcpServers\":{\"kitaru\":{\"command\":\"$MCP_BIN\",\"args\":[\"--server\",\"$MCP_SERVER_URL\",\"--mode\",\"$KITARU_MCP_MODE\"]}}}" + MCP_JSON_ARGS="" + for a in "${MCP_CMD[@]:1}" "${MCP_ARGS[@]}"; do MCP_JSON_ARGS="$MCP_JSON_ARGS${MCP_JSON_ARGS:+,}\"$a\""; done + note " {\"mcpServers\":{\"kitaru\":{\"command\":\"${MCP_CMD[0]}\",\"args\":[$MCP_JSON_ARGS]}}}" fi fi -# --------------------------------------------------------------------------- -# 5. Login (local server via Docker, or a team server) -# --------------------------------------------------------------------------- -DOCKER_HINT="" -docker_running() { - have docker || { DOCKER_HINT="Docker is not installed."; return 1; } - local err; err="$(docker info 2>&1 >/dev/null)" && return 0 - case "$err" in - *"permission denied"*|*"Permission denied"*) - DOCKER_HINT="Docker is installed but this user cannot reach it. Add yourself to the docker group (sudo usermod -aG docker \$USER, then log out and in), or start Docker Desktop." ;; - *) DOCKER_HINT="Docker is not running." ;; - esac - return 1 -} -# A real terminal we can read from: stdin is one, or /dev/tty actually opens -# (checking the device node's permission bits is not enough in containers). -has_tty() { [ -t 0 ] || { : /dev/null; } - -login_cmd() { - if [ -n "$KITARU_SERVER" ]; then printf 'kitaru login %s' "$KITARU_SERVER" - else printf 'kitaru login --local'; fi -} - -# `kitaru login --local` refuses to replace an existing local server container -# from an older release unless told to with --upgrade (the database is kept). -# Running the installer is that approval: its documented contract is that a -# re-run upgrades. So on exactly that conflict, retry with --upgrade. -LOCAL_UPGRADE_HINT="kitaru login --local --upgrade" -login_local() { - # $1 = stdin source: /dev/tty when there is one, /dev/null otherwise - # (`kitaru login --local` needs no interaction). - local in="$1" out; out="$(mktemp)" - if "$KITARU_BIN" login --local <"$in" 2>&1 | tee "$out"; then - rm -f "$out"; LOGGED_IN=1; return 0 - fi - if grep -qi -- "conflict\|--upgrade" "$out"; then - rm -f "$out" - note "An older local server is running. Upgrading it to match kitaru $("$KITARU_BIN" --version); your database is kept." - if "$KITARU_BIN" login --local --upgrade <"$in"; then LOGGED_IN=1; return 0; fi - warn "Local server upgrade did not complete. Run: $LOCAL_UPGRADE_HINT" - return 1 - fi - rm -f "$out" - warn "Local server did not start. Run: kitaru login --local" - return 1 -} - -LOGGED_IN=0 -if [ "$KITARU_SKIP_LOGIN" = "1" ]; then - note "Skipping login (--no-login)" -elif [ -z "$KITARU_SERVER" ] && ! docker_running; then - warn "$DOCKER_HINT The local Kitaru server was not started." - note "Fix that, then run: kitaru login --local" - note "Or use a team server: kitaru login https://.kitaru.ai" -elif [ -n "$KITARU_SERVER" ]; then - # Team login uses a device flow and needs a terminal. - if has_tty; then - step "Logging in ($(login_cmd))" - "$KITARU_BIN" login "$KITARU_SERVER" /dev/null 2>&1 || [ "${TOOL_BIN}" != "$(dirname "$(command -v kitaru)")" ]; then +# In project mode kitaru is not on PATH; every command goes through uv run. +if [ "$KITARU_SCOPE" = "project" ]; then K="uv run kitaru"; else K="kitaru"; fi +if [ "$KITARU_SCOPE" = "project" ]; then + say " Installed into this project's environment, so run it as ${C_BOLD}uv run kitaru ...${C_RESET}" + say " (or activate $VENV_DIR)." + say "" +elif [ "$(PATH="$ORIG_PATH" command -v kitaru 2>/dev/null || true)" != "$KITARU_BIN" ]; then say " Open a new terminal so 'kitaru' is on your PATH." + say "" fi -if [ "$LOGGED_IN" = "0" ]; then - say " 1. $(login_cmd)" - say " 2. Open your coding agent in your agent's repo and say:" +if [ -n "$KITARU_SERVER" ]; then + say " Next, log in to your server:" + say "" + say " ${C_BOLD}$K login $KITARU_SERVER${C_RESET}" else - say " Open your coding agent in your agent's repo and say:" + say " Next, pick where your Kitaru server lives:" + say "" + say " ${C_BOLD}$K login --local${C_RESET} local, in Docker. Free, open source." + say " ${C_BOLD}$K login${C_RESET} managed cloud. 14-day trial, no credit card required." fi say "" -say " ${C_BOLD}Use kitaru-investigation to investigate this agent.${C_RESET}" +say " Then, in your agent's repo, tell your coding agent:" +say " ${C_BOLD}Use kitaru-investigation to investigate this agent.${C_RESET}" +if [ "$KITARU_SCOPE" = "global" ]; then + say "" + say " Replays run your agent, so they need Kitaru inside the agent's own project:" + say " re-run this installer from that repo, or ${C_BOLD}uv add \"kitaru[cli,worker]\" kitaru-${C_RESET} there." +fi say "" say " No agent yet? ${C_BOLD}Use kitaru-guided-tour to show me Kitaru on the example agent.${C_RESET}" -say " Check setup: kitaru doctor" +say " Check setup: $K doctor" say " Docs: https://docs.zenml.io/kitaru" say "" } diff --git a/src/kitaru/cli/auth.py b/src/kitaru/cli/auth.py index 866c3f8c6..5bf93d09b 100644 --- a/src/kitaru/cli/auth.py +++ b/src/kitaru/cli/auth.py @@ -544,7 +544,7 @@ def _show_device_prompt( uri = authorization.verification_uri_complete or authorization.verification_uri else: uri = authorization.verification_uri_complete - message = f"Open {uri} and confirm code {authorization.user_code}." + message = f"Open {uri} to continue." write_interaction(message) diff --git a/src/kitaru/client/control_plane.py b/src/kitaru/client/control_plane.py index 661d6a605..f27534e01 100644 --- a/src/kitaru/client/control_plane.py +++ b/src/kitaru/client/control_plane.py @@ -198,13 +198,13 @@ async def device_login_with_metadata( ) -> ControlPlaneToken: """Authorize this machine against the control plane. - The call blocks until a signed-in account confirms the user code in a + The call blocks until a signed-in account completes authorization in a browser, or until the authorization expires. Args: open_browser: Whether to open the verification page. prompt: Called with the authorization so the caller can show the - user code. Defaults to logging it. + verification URL. Defaults to logging it. workspace_id: Workspace ID preselected on the verification page. Raises: @@ -235,9 +235,8 @@ async def device_login_with_metadata( prompt(authorization) else: logger.info( - "Open %s and confirm the code %s.", + "Open %s to continue.", verification_uri, - authorization.user_code, ) if open_browser: webbrowser.open(verification_uri) diff --git a/src/kitaru/client/device_auth.py b/src/kitaru/client/device_auth.py index b99c41f5a..4eb3fe7a0 100644 --- a/src/kitaru/client/device_auth.py +++ b/src/kitaru/client/device_auth.py @@ -38,7 +38,7 @@ async def device_login( ) -> ApiToken: """Authorize this machine against a server and store the token it gets. - The call blocks until a signed-in account confirms the user code in a + The call blocks until a signed-in account completes authorization in a browser, or until the authorization expires. Args: @@ -46,8 +46,8 @@ async def device_login( base_url: Server base URL credentials are stored under. store: Credential store the device authorization is written to. open_browser: Whether to open the verification page. - prompt: Called with the authorization so the caller can show the user - code. Defaults to logging it. + prompt: Called with the authorization so the caller can show the + verification URL. Defaults to logging it. Raises: DeviceLoginError: The authorization expired or was refused. @@ -66,9 +66,8 @@ async def device_login( prompt(authorization) else: logger.info( - "Open %s and confirm the code %s.", + "Open %s to continue.", authorization.verification_uri_complete, - authorization.user_code, ) if open_browser: webbrowser.open(authorization.verification_uri_complete) diff --git a/tests/cli/test_auth.py b/tests/cli/test_auth.py index 959db767a..4271028d4 100644 --- a/tests/cli/test_auth.py +++ b/tests/cli/test_auth.py @@ -21,7 +21,7 @@ import pytest -from kitaru.api_models.v1.auth import TokenResponse +from kitaru.api_models.v1.auth import DeviceAuthorizationResponse, TokenResponse from kitaru.api_models.v1.info import AuthScheme, ServerInfoResponse from kitaru.cli import auth from kitaru.cli.output import CLIError @@ -101,6 +101,32 @@ async def close(self) -> None: WORKSPACE_ID = uuid.UUID("11111111-1111-1111-1111-111111111111") +def test_device_prompt_uses_the_complete_verification_url( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Direct people to the verification page without a redundant code prompt.""" + messages: list[str] = [] + monkeypatch.setattr(auth, "write_interaction", messages.append) + + auth._show_device_prompt( + DeviceAuthorizationResponse( + device_id=WORKSPACE_ID, + device_code="device-code", + user_code="ABCD-EFGH", + verification_uri="https://cloud.example.com/devices/verify", + verification_uri_complete=( + "https://cloud.example.com/devices/verify?user_code=ABCD-EFGH" + ), + expires_in=300, + interval=5, + ) + ) + + assert messages == [ + "Open https://cloud.example.com/devices/verify?user_code=ABCD-EFGH to continue." + ] + + def managed_workspace( *, status: str = "available", server_url: str | None = "https://managed.example.com" ) -> ControlPlaneWorkspace: