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
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

Command-line interface for [skore](https://github.com/probabl-ai/skore).

`skore-cli` installs a single `skore` command with three areas:
`skore-cli` installs a single `skore` command with four areas:

- **skills** — discover, install and manage [Agent Skills](https://agentskills.io)
from the [probabl-ai/skills](https://github.com/probabl-ai/skills) catalog
- **agent** — connect a project to the Skore Hub agent, write harness config
and launch a local coding agent
- **hub** — generate, store and inspect Skore Hub API keys
- **sync** — synchronize report projects across local storage, Skore Hub, and MLflow

## Installation
Expand Down Expand Up @@ -56,8 +57,12 @@ skore skills remove # remove installed skills
### Agent

On the first run, `skore agent` logs in when needed, lets you pick a workspace
and harness, creates a workspace API key, writes the harness configuration and
launches the agent. Supported harnesses: **Bob Shell**, **Bob IDE**, **Claude CLI**,
and harness, and saves the workspace and hub URI to `.skore` in the project
directory (gitignored). The API key is not stored there: it comes from the
credential registry, and `skore agent` runs `skore hub api-key generate
--host=<host> --workspace=<workspace>` for you when no key is stored yet.

Supported harnesses: **Bob Shell**, **Bob IDE**, **Claude CLI**,
**Claude UI**, **Claude Plugin** (Cursor, VS Code, or VS Code Insiders),
**Cursor IDE**, **Cursor CLI**, **OpenCode**,
**Pi**, **Copilot in VSCode**, **Copilot CLI** and **Codex CLI**
Expand All @@ -72,21 +77,41 @@ detected harnesses first, then the ones that are not installed.
accepts `--harness bobide` — the name of the command its installer puts on
`PATH` — as an alias for `--harness bob-ide`. Later runs reuse
`.skore` in the project directory (gitignored); passing `--hub-url` or
`--harness` re-writes `.skore` with the new values (a different `--hub-url` also
mints a fresh API key against that hub). Use `SKORE_HUB_URI` (or `--hub-url`)
to point at a non-default hub.
`--harness` re-writes `.skore` with the new values (a different `--hub-url`
re-authenticates and uses the API key stored for that hub). Use `SKORE_HUB_URI`
(or `--hub-url`) to point at a non-default hub. A frontend URL is resolved to
the Hub API via `/.well-known/skore-hub.json`.

Launching a harness exports the `.skore` credentials as `SKORE_HUB_API_KEY` and
`SKORE_HUB_URI`, so `skore.login()` in the scripts the agent runs authenticates
with that key instead of opening a browser. Values already set in your
environment are left untouched.
Reading `.skore` sets `SKORE_HUB_URI` so the `skore` package talks to the same
hub. The workspace API key stays in the credential registry and is picked up
automatically. `--hub-url` / `--host` override the URI for that process.

```bash
skore agent
skore agent --harness claude # non-interactive harness choice
skore agent --workspace ./myapp # configure another project directory
```

### Hub

Store and inspect Hub API keys locally via skore's credential registry
(`~/.skore.hub/credentials.json`). `--host` selects a non-default hub (omit it
to use `SKORE_HUB_URI` or the public hub); `--workspace` is required when
generating or revoking a key. `generate` stores one key per machine for a host
and workspace; pass `--force` to revoke **this** machine's Hub key and mint a
replacement. `revoke` removes the local entry and revokes only that stored Hub
key.

```bash
skore hub api-key generate --workspace=<workspace>
skore hub api-key generate --workspace=<workspace> --expires 3
skore hub api-key generate --workspace=<workspace> --force
skore hub api-key generate --host=<host> --workspace=<workspace>
skore hub api-key revoke --workspace=<workspace>
skore hub api-key revoke --host=<host> --workspace=<workspace>
skore hub api-key list
```

### Sync

Synchronize a source project to a destination mode. The source defaults to local when
Expand All @@ -104,8 +129,10 @@ SKORE_HUB_API_KEY=... skore sync production \
skore sync experiment --to=mlflow --tracking-uri=http://localhost:5000
```

Hub synchronization requires `SKORE_HUB_API_KEY`. Use `--hub-url` to target a custom
Hub API. Install `skore[mlflow]` to synchronize with MLflow.
Hub synchronization uses `SKORE_HUB_API_KEY` when set, otherwise the key stored by
`skore hub api-key`. Use `--hub-url` to target a custom Hub API; a frontend URL
is resolved via `/.well-known/skore-hub.json`. Install `skore[mlflow]` to
synchronize with MLflow.

## Agent detection

Expand Down
1 change: 0 additions & 1 deletion pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ dependencies = [
"rich>=14.2",
"rich-click>=1.9",
"textual",
# The `agent` command reuses skore's authentication machinery and calls the
# hub identity API directly with httpx.
"skore[hub]>=0.24.0",
"httpx",
"skore[hub]>=0.27",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -171,8 +168,8 @@ ignore_missing_imports = true
module = [
"rich_click.*",
"textual.*",
"httpx",
"httpx.*",
"skore",
"skore.*",
]

[tool.ty.environment]
Expand Down
18 changes: 16 additions & 2 deletions src/skore_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

import importlib.util

import rich_click as click

import skore_cli._style # noqa: F401 (applies the CLI palette and rich-click config)
Expand All @@ -18,20 +20,23 @@
# importing them (to build the CLI / show `--help`) stays instant. Each merges its
# own `rich_click.COMMAND_GROUPS` entry, so import order does not matter.
from skore_cli.agent import agent
from skore_cli.hub import hub
from skore_cli.skills import skills
from skore_cli.sync import sync

click.rich_click.COMMAND_GROUPS = {
**getattr(click.rich_click, "COMMAND_GROUPS", {}),
"cli": [
{"name": "Agent", "commands": ["agent"]},
{"name": "Hub", "commands": ["hub"]},
{"name": "Projects", "commands": ["sync"]},
{"name": "Skills", "commands": ["skills"]},
],
}

_COMMANDS = [
("agent", "Authenticate, configure and launch a Skore Hub agent harness."),
("hub", "Manage Skore Hub credentials."),
("skills", "Install and manage Agent Skills from the probabl-ai/skills release."),
("sync", "Synchronize report projects across local storage, Skore Hub and MLflow."),
]
Expand Down Expand Up @@ -86,8 +91,9 @@ def _render_help(detected: Agent | None) -> str:
def cli(ctx) -> None:
"""Skore command-line interface.

Use ``skore agent`` to connect a project to the Skore Hub agent, ``skore sync``
to synchronize report projects, and ``skore skills`` to install probabl-skills.
Use ``skore agent`` to connect a project to the Skore Hub agent, ``skore hub``
to manage Hub credentials, ``skore sync`` to synchronize report projects, and
``skore skills`` to install probabl-skills.
"""
if ctx.invoked_subcommand is None:
if is_non_interactive():
Expand All @@ -99,9 +105,17 @@ def cli(ctx) -> None:

cli.add_command(skills)
cli.add_command(agent)
cli.add_command(hub)
cli.add_command(sync)

# Commands contributed by other packages via the `skore_cli.plugins` entry-point
# group. Kept for third-party extensibility; the built-in `agent` command no
# longer goes through it so the CLI never imports `skore` just to show help.
load_plugins(cli)

# Ensure ``skore`` is available without importing it explicitly
if importlib.util.find_spec("skore") is None:
raise click.ClickException(
"this command needs the `skore` package (install it with `pip install "
"skore-cli` or `pip install skore`)."
)
39 changes: 14 additions & 25 deletions src/skore_cli/_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@
CODEX_PROVIDER_NAME = "Skore Agent"
CODEX_PROJECT_CONFIG = ".codex/skore-provider.toml"
CODEX_API_KEY_ENV = "SKORE_AGENT_API_KEY"
SDK_API_KEY_ENV = "SKORE_HUB_API_KEY"
SDK_URI_ENV = "SKORE_HUB_URI"


@dataclass(frozen=True)
Expand Down Expand Up @@ -783,23 +781,29 @@ def _configure_copilot_cli(_ctx: HarnessContext) -> dict[str, Any]:
from skore_cli._style import console

console.print(
"[skore.muted]Copilot CLI uses the Hub key from[/] [skore.path].skore[/] "
"[skore.muted]at launch; no extra project file is written.[/]"
"[skore.muted]Copilot CLI uses the Hub key from the credential "
"registry at launch; no extra project file is written.[/]"
)
return {}


def _launch_copilot_cli(workspace: Path, model_id: str) -> None:
"""Start ``copilot`` with the same provider env as ``skore-copilot cli``."""
from skore._plugins.hub.authentication import key

from skore_cli.agent._skore_file import SkoreConfig

config = SkoreConfig.load(workspace)
if config is None or not config.api_key or not config.hub_url:
if config is None or not config.hub_url:
raise RuntimeError(
"missing .skore; run skore agent --harness copilot-cli first."
)
api_key = key.get(host=config.hub_url, workspace=config.workspace)
if not api_key:
raise RuntimeError(
"missing Hub API key; run skore agent --harness copilot-cli first."
)
hub_url = config.hub_url
api_key = config.api_key
env = os.environ.copy()
env["COPILOT_PROVIDER_TYPE"] = "openai"
env["COPILOT_PROVIDER_BASE_URL"] = f"{hub_url.rstrip('/')}/v1"
Expand Down Expand Up @@ -847,24 +851,6 @@ def _launch_codex(workspace: Path, model_id: str) -> None:
_exec_harness("codex", argv, env=env)


def _export_sdk_credentials(workspace: Path) -> None:
"""Publish the ``.skore`` credentials the ``skore`` package reads.

The harness config only authenticates the harness itself. ``skore.login()``
looks the API key up in ``SKORE_HUB_API_KEY`` and otherwise falls back to an
interactive browser flow, so every experiment process the agent spawns would
open its own OAuth tab. Values already present in the environment win, so a
user's own key or hub keeps precedence.
"""
from skore_cli.agent._skore_file import SkoreConfig

config = SkoreConfig.load(workspace)
if config is None:
return
os.environ.setdefault(SDK_API_KEY_ENV, config.api_key)
os.environ.setdefault(SDK_URI_ENV, config.hub_url)


def _exec_harness(
name: str, argv: list[str], *, env: dict[str, str] | None = None
) -> None:
Expand Down Expand Up @@ -1210,5 +1196,8 @@ def launch_harness(
console.print(
f"[skore.ok]Launching[/] [skore.skill]{agent.harness_display_name}[/] ..."
)
_export_sdk_credentials(workspace)
from skore_cli.agent._skore_file import SkoreConfig

if "SKORE_HUB_URI" not in os.environ:
SkoreConfig.load(workspace)
agent.launch(workspace, model_id)
70 changes: 0 additions & 70 deletions src/skore_cli/_hub_auth.py

This file was deleted.

Loading
Loading