Metadata-Version: 2.4
Name: aethis-cli
Version: 0.35.0
Summary: CLI for the Aethis developer API — author, test, and publish rulesets
Author-email: Aethis <eng@aethis.ai>
License-Expression: MIT
Project-URL: Homepage, https://aethis.ai
Project-URL: Repository, https://github.com/Aethis-ai/aethis-cli
Project-URL: Documentation, https://github.com/Aethis-ai/aethis-cli#readme
Project-URL: Bug Tracker, https://github.com/Aethis-ai/aethis-cli/issues
Keywords: cli,eligibility,rules-engine,compliance,aethis
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12.0
Requires-Dist: rich>=13.5.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pyyaml>=6.0.1
Provides-Extra: keyring
Requires-Dist: keyring>=25.0.0; extra == "keyring"
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: respx>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
Requires-Dist: ruff<0.17,>=0.8.0; extra == "dev"
Dynamic: license-file

# aethis-cli

[![PyPI](https://img.shields.io/pypi/v/aethis-cli.svg)](https://pypi.org/project/aethis-cli/)
[![Docs](https://img.shields.io/badge/docs-docs.aethis.ai-blue)](https://docs.aethis.ai)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

CLI for the [Aethis](https://aethis.ai) developer API — evaluate eligibility, author rulesets, and publish from the terminal.

## Install

```bash
# Recommended — isolated, no venv juggling:
uv tool install aethis-cli

# Or, with pipx:
pipx install aethis-cli
```

Already installed? `aethis update` upgrades to the latest release in place
(it detects how the CLI was installed and runs the matching upgrade command).

## Quick start

> **Authoring is in private beta.** Decision tools (`decide`, `fields`, `explain`) are public — no key required. Authoring tools (rule generation, test refinement, publishing) require an invite. Request access at [aethis.ai/developer-access](https://aethis.ai/developer-access).

No sign-up needed. Decision tools work immediately.

```bash
# Browse the public showcase catalogue — no API key required
aethis rulesets list

# Inspect the input fields a ruleset expects
aethis fields -b <slug-or-ruleset_id>

# Get human-readable rule descriptions
aethis explain -b <slug-or-ruleset_id>

# Evaluate eligibility against a published ruleset
aethis decide -b <slug-or-ruleset_id> -i '{"some.field": 35, "other.field": true}'
```

## Authentication

Decision tools (`decide`, `fields`, `explain`) call public endpoints and never need a key. **Authoring tools** (`generate`, `publish`, `projects list`, etc.) need an API key — and the CLI manages that for you.

Authoring is in private beta — `aethis login` will only complete for invited developers. Request access at [aethis.ai/developer-access](https://aethis.ai/developer-access).

There are three ways the key can arrive:

**1. Explicit sign-in** (the canonical first-time setup):

```bash
aethis login
```

Opens your browser, completes Clerk OAuth, mints a fresh `ak_live_...` key, and stores it at `~/.config/aethis/credentials`. One-time per machine.

**2. Lazy auth** (the default — since v0.6.0):

If you skip step 1 and run an authenticated command directly, you'll get an inline prompt:

```
$ aethis init
No API key found. Open browser to sign in? [Y/n]
```

Hit enter, the browser flow runs, the command resumes. Same effect as `aethis login` followed by your original command, in one step.

**3. CI / scripts** — `--no-prompt`:

For any non-interactive context (`stdin` not a TTY, CI runners, piped commands) the CLI auto-skips the prompt and exits with a clear `AuthRequired` error. To force this behaviour even on a TTY:

```bash
aethis --no-prompt projects list
```

Pass `--api-key <ak_live_...>` to bypass the cache entirely (useful when you want to test a key without committing to it).

Manage existing keys with `aethis account keys` (list, masked) and `aethis account revoke <key_id>` (revoke). `aethis account generate` mints an *additional* key — for rotation, multi-machine setups, or scoped access. For first-time setup just use `aethis login`.

## Author your own rules (private beta)

Rule authoring is **invite-only private beta**. Decision tools (`aethis decide`, `aethis fields`, `aethis explain`) work immediately with no sign-up — this section is for approved beta tenants. [Request access →](https://aethis.ai/developer-access)

<!-- aethis-bible: public-messaging.md#5-how-rule-authoring-works -->
The authoring workflow follows a four-stage loop: discover, synthesise, test, publish. A ruleset cannot be published with failing tests.

```bash
# 1. Initialise a project (no-arg form prompts for a name; auto-runs `aethis login` if needed)
aethis init

# 2. Add source documents and guidance
#    (put PDFs/text in .aethis/sources/, hints in .aethis/guidance/hints.yaml)

# 3. Generate a rule ruleset
aethis generate

# 4. Run test cases
aethis test

# 5. Publish the ruleset
aethis publish
```

`aethis init` runs as a wizard: prompts for a project name (default = current directory), runs sign-in if you're not authed yet, scaffolds `.aethis/`, and prints the next-step ladder. Pass `--no-prompt` for scripted use (it'll fail fast on missing values rather than prompt).

## Try the example

A complete, runnable example is included in `examples/spacecraft-crew-rules/`:

```bash
cp -r examples/spacecraft-crew-rules my-first-rules && cd my-first-rules
aethis login
aethis generate --poll
aethis test
aethis decide -i '{"space.crew.species": "Human", "space.crew.age": 35, "space.crew.flight_hours": 600, "space.crew.has_pilot_license": true, "space.crew.has_gaa_exam": true, "space.medical.cert_valid": true, "space.mission.type": "suborbital", "space.crew.has_towel": true}'
```

See `examples/spacecraft-crew-rules/README.md` for details.

## Commands

### Decision (no API key required)

| Command | Description |
|---------|-------------|
| `aethis rulesets list` | Browse the public showcase catalogue (auto-falls through when no project context) |
| `aethis rulesets list --public` | Same, explicit form |
| `aethis decide -b <slug-or-ruleset_id> -i '<json>'` | Evaluate eligibility. Add `--explain` for trace output. |
| `aethis fields -b <slug-or-ruleset_id>` | Show input fields the ruleset expects |
| `aethis explain -b <slug-or-ruleset_id>` | Human-readable rule descriptions, their identity, and the sources behind them |

### Reading a decision

Every decision response carries the **immutable identity** of the rule
artefact that produced it — the ruleset id, the published version, and a
`sha256:` digest of the published rule content. A republish always changes the
digest, even when the version label is reused, so keeping those three with
your inputs is what lets you reproduce or audit the same answer later.

Output separates three things that are easy to conflate:

| Block | What it is |
|---|---|
| **Result** | The decision, and only the decision. The one field you may act on. |
| **Logic trace** | Which criteria were evaluated and how each came out. Explanatory — criterion statuses never aggregate into an outcome. |
| **Sources** | The publish-validated citations behind the criteria: document, authority, licence, the verbatim quoted text, a deep link, and the digest of the verified source. |

#### Rejected inputs are never a result

If the ruleset cannot apply an input you sent — an unknown field key, or a
value that does not match the field's type — the response carries blocking
`field_errors` and **no decision exists**. `aethis decide` prints the rejected
inputs instead of a verdict and exits `3`; JSON output reports
`"decision": "undetermined"` and records the block under
`aethis_cli_contract`. On every decide surface — `aethis decide` and
`aethis rulebooks decide` — there is no combination of flags that renders a
blocked evaluation as eligible or ineligible.

| Exit code | Meaning |
|---|---|
| `0` | A decision was reached (`eligible`, `not_eligible`, or `undetermined`). |
| `1` | The call failed — unreachable API, auth, or an error envelope. |
| `3` | One or more inputs were rejected, so there is no decision. |

`--output` is a root option, so it goes *before* the subcommand:

```bash
# Safe in a shell gate: a rejected input can never pass as success
if aethis --output json decide -b <ruleset> -i "$INPUTS" > result.json; then
  jq -r '.decision' result.json
else
  echo "no decision: $(jq -r '.field_errors // empty' result.json)"
fi
```

The same contract and the same exit codes apply to
`aethis rulebooks decide`.

### Authoring

| Command | Description |
|---------|-------------|
| `aethis init` | Initialise a new project in the current directory |
| `aethis generate [--poll]` | Upload sources + guidance, trigger generation. A successful `--poll` run publishes the ruleset, making it active |
| `aethis generate --no-publish` | The same run, leaving the ruleset an unpublished draft — for workflows where activation is separately gated |
| `aethis status` | Check generation job progress |
| `aethis test` | Run test cases against the latest ruleset |
| `aethis publish [--force]` | Set the latest ruleset as active |
| `aethis publish --source-targets <file>` | Publish with citations resolved from a YAML/JSON targets file |

### Citations at publish (`--source-targets`)

Where your criteria declare citation keys (`source_refs`), `--source-targets`
tells the engine what each key cites. Every entry names **exactly one** target:

- `url` — a public HTTPS document. The engine fetches it, checks your verbatim
  quote against the bytes it received, records their digest, and keeps a
  snapshot of them.
- `file` — a local file. The CLI uploads it to the project (or reuses an
  identical file already uploaded, matched by `sha256`) and the citation
  points at those retained bytes. Its download URL is **authenticated** — only
  a key with `projects:read` on that project can fetch it.

```yaml
# targets.yaml — keys match the source_refs your criteria declare
"BNA1981#Schedule1/P1.1":
  url: https://www.legislation.gov.uk/ukpga/1981/61/schedule/1
  title: British Nationality Act 1981, Schedule 1
  authority: UK Government
  licence: OGL-UK-3.0
  locator: Paragraph 1(1)(a)        # optional
  quote:
    exact: "is of full age and capacity"

"GUIDE#4.2":
  file: ./corpus/guidance.pdf       # relative to this file
  title: Applicant guidance
  authority: Example Authority
  licence: OGL-UK-3.0
  quote:
    exact: "You must have been resident"
```

```bash
aethis publish --source-targets targets.yaml
```

The quote must be **verbatim** — never a summary or a paraphrase. A citation
that cannot be resolved, is unlicensed, or whose quote does not occur in the
source rejects the whole publish, and every failing key is reported with its
reason; nothing is published half-cited. Malformed targets files fail locally,
before any upload, and a duplicate citation key is an error rather than a
silent overwrite.

Only the citation keys your criteria actually declare are resolved — a key
nothing declares is ignored by the engine. After publishing, the CLI reads the
ruleset back and tells you how many targets landed, naming any that did not.
If a publish fails after files were uploaded, those files stay in the project
and are reused on the next attempt (matched by `sha256`), so retrying does not
duplicate them.

### Guidance (project-level)

Project guidance lives in `.aethis/guidance/hints.yaml` and is uploaded by `aethis generate`. These commands manage hints server-side after upload.

| Command | Description |
|---------|-------------|
| `aethis guidance list` | List active hints for the current project |
| `aethis guidance export <file>` | Export the current project's hints to YAML |
| `aethis guidance import <file>` | Import hints from a YAML file |
| `aethis guidance deactivate <hint_id>` | Deactivate a specific hint |

### Projects & rulesets

| Command | Description |
|---------|-------------|
| `aethis projects list` | List your projects |
| `aethis projects show <project_id>` | Show project details |
| `aethis projects archive <project_id>` | Archive a project |
| `aethis rulesets list` | List published rulesets |
| `aethis rulesets archive <ruleset_id>` | Archive a ruleset |

### Rulebooks (the converged 2-term model, login required for authoring)

A Rulebook is the whole form — the unit `/decide` evaluates against. It owns a locked field vocabulary, an `outcome_logic` composition expression, rulebook-level test cases, and an integer version history. Rulesets are named, versioned members of a Rulebook.

| Command | Description |
|---------|-------------|
| `aethis rulebooks list` | List your tenant's rulebooks (or, with no key, the public catalogue) |
| `aethis rulebooks show <id-or-slug>` | Full configuration including outcome_logic |
| `aethis rulebooks create <name> --domain <d> [--slug ...]` | Create a draft rulebook |
| `aethis rulebooks set-fields <id> -f fields.yaml` | Replace the locked field vocabulary |
| `aethis rulebooks lock-fields <id>` / `unlock-fields <id>` / `get-fields <id>` | Manage the field-lock state |
| `aethis rulebooks set-logic <id> -f logic.yaml` | Set the composition expression (Expr AST) |
| `aethis rulebooks decide <id-or-slug> -i '<json>'` | Evaluate the composed rulebook (requires API key) |
| `aethis rulebooks activate <id>` / `archive <id>` | Lifecycle |
| `aethis rulesets create <rulebook> <ruleset_name>` | Create a draft ruleset inside a rulebook |
| `aethis rulesets list <rulebook>` | List rulesets in a rulebook |
| `aethis rulesets show <rulebook> <ruleset_name>` | Full version history for one ruleset name |
| `aethis rulesets promote-to-live <rulebook> <ruleset_name> <ruleset_id>` | Atomic promotion: demote prior live → archived, promote candidate, cut new Rulebook version |

### Account

| Command | Description |
|---------|-------------|
| `aethis login` | Sign in and store an API key locally (first-time setup) |
| `aethis login --profile <name>` | Sign in into a named profile slot (keeps `default` untouched) |
| `aethis account generate` | Mint an additional API key (rotation, multi-machine, scoped access) |
| `aethis account keys` | List your API keys (masked) |
| `aethis account revoke <key_id>` | Revoke a key |
| `aethis update [--check]` | Update the CLI to the latest release (`--check` only reports) |

### Profiles

Multiple credential profiles let you keep separate personas — say, an admin
key plus a "fresh signup" perspective — and switch between them without
re-authenticating. The reserved profile name `anonymous` forces unsigned
mode (no `X-API-Key` header sent), so you can verify exactly what an
unauthenticated user would see.

```bash
# Create profiles (or use `aethis login --profile <name>` for OAuth)
aethis profile add admin --api-key ak_live_…
aethis profile add new-dev --api-key ak_test_…

# See them, with `*` next to the active one
aethis profile list

# Switch the sticky default
aethis profile use new-dev

# One-off override (doesn't change the sticky default)
aethis --profile admin rulesets list
aethis --profile anonymous rulesets list   # forces public-mode

# Delete one
aethis profile remove new-dev
```

Profile selection order: `--profile` flag > `AETHIS_PROFILE` env > the
sticky `active_profile` field in `~/.config/aethis/credentials` >
`default`. The `AETHIS_API_KEY` env var still takes precedence over all
profile machinery — set it to short-circuit the cache for one
invocation.

| Command | Description |
|---------|-------------|
| `aethis profile list` | Show all profiles + which one is active |
| `aethis profile use <name>` | Set the sticky default profile |
| `aethis profile add <name> [--api-key …]` | Create or update a named profile |
| `aethis profile remove <name>` | Delete a profile |

## MCP one-liner

Wire up the [Aethis MCP server](https://github.com/Aethis-ai/aethis-mcp) in your AI editor without hand-editing JSON. Picks up the API key cached by `aethis login`, drops a canonical `aethis` server entry into the right config file, and preserves any other MCP servers you already have.

> Onboarding an AI coding agent end-to-end? See [docs.aethis.ai/agents/onboarding](https://docs.aethis.ai/agents/onboarding) — install + verify + auth + workflow patterns in one page.

```bash
# One editor at a time
aethis mcp install --target cursor
aethis mcp install --target claude-code      # writes ./.mcp.json (project-local)
aethis mcp install --target claude-desktop
aethis mcp install --target windsurf

# Or all four at once
aethis mcp install --target all

# Reverse it (only removes the `aethis` entry, leaves others alone)
aethis mcp uninstall --target cursor
```

The command is idempotent — re-run it after `aethis login` rotates your key and the entry updates in place. Restart your editor to pick up the change.

| Target | Config path |
|--------|-------------|
| `claude-code` | `<cwd>/.mcp.json` (project-scoped) |
| `cursor` | `~/.cursor/mcp.json` |
| `claude-desktop` | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` · Linux: `~/.config/Claude/claude_desktop_config.json` |
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` |

## Project structure

After `aethis init`, your project looks like:

```
my-rules/
  .aethis/
    aethis.yaml          # project config
    state.json           # tracked IDs (project, ruleset, job)
    sources/             # PDF/text source documents
    guidance/
      hints.yaml         # guidance hints for the code synthesizer
    tests/
      scenarios.yaml     # golden test cases
```

### aethis.yaml

```yaml
project: my-rules
api_key_env: AETHIS_API_KEY
```

### scenarios.yaml

```yaml
tests:
  - name: "Eligible — all requirements met"
    inputs:
      space.crew.age: 35
      space.crew.flight_hours: 600
      space.crew.has_pilot_license: true
    expect:
      outcome: eligible
  - name: "Not eligible — no medical cert"
    inputs:
      space.medical.cert_valid: false
    expect:
      outcome: not_eligible
```

This file is the authoritative suite: `aethis generate` uploads it in full and
**replaces** the project's test cases with it, so running generate twice leaves
one copy, not two. The upload reports how many existing cases it overwrote. An
older API host may not offer replacement — where that is so, `aethis generate`
says the cases were appended and that running again will add another copy,
rather than leaving you to discover it in an inflated pass-rate total.

## Environment variables

| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `AETHIS_API_KEY` | Your API key (`ak_live_...`). Bypasses the cached credential and any profile machinery. | Authoring only | — |
| `AETHIS_PROFILE` | Select a named credential profile (overrides the sticky default; see `aethis profile`). | No | `default` |
| `AETHIS_BASE_URL` | Override the API host (staff/dev use; staging or self-hosted). | No | `https://api.aethis.ai` |
| `ANTHROPIC_API_KEY` | Forwarded per-request to the generation endpoint when running `aethis generate`. Never stored server-side. | Authoring only | — |

## Verifying a release

Each published version is bound to the source it was built from. The release
workflow records the tuple — version, sdist and wheel `sha256`, source commit
— and then re-checks it against the files the registry actually serves, so
"same version string" can never stand in for "same artefact".

Reproduce either half locally:

```bash
uv build
uv run python scripts/release-integrity.py                    # the tuple
uv run python scripts/release-integrity.py --verify-registry  # vs PyPI

# Does it work for someone who has never run it? Temporary HOME/XDG/cache,
# no credentials, empty-cache install, across supported runtimes.
uv run python scripts/hermetic-install-check.py --source dist
uv run python scripts/hermetic-install-check.py --source dist --poison  # must detect contamination
```

Both scripts are non-interactive with bounded timeouts and print one JSON
record. The poisoned run is a negative control: it passes only when the
contamination trips the *specific* assertions it was meant to trip, and fails
loudly if it never reached them (a control that "passes" because nothing was
installed is not evidence).

Note what the digests do and do not prove. `uv build` is not byte-reproducible
here — the wheel is, with `SOURCE_DATE_EPOCH` set; the sdist is not, because
its gzip header carries a build timestamp. So the tuple attests *these are the
bytes that job built and published*, verified against what the registry
serves; it is not something you can re-derive by rebuilding the commit. Check
it yourself with `--verify-reproducible`.

## Extending with plugins

`aethis-cli` discovers third-party plugins via Python entry points under the `aethis_cli.plugins` group. A plugin is any installed package that exposes a `register(app: typer.Typer) -> None` callable; at startup the CLI calls it with the root Typer app and the plugin attaches extra commands.

Example `pyproject.toml`:

```toml
[project.entry-points."aethis_cli.plugins"]
my_plugin = "my_package.plugin:register"
```

Example `my_package/plugin.py`:

```python
import typer


def register(app: typer.Typer) -> None:
    @app.command()
    def hello() -> None:
        typer.echo("hello from my plugin")
```

Staff-only tools (DSL source viewer, IAM registry, domain-guidance management, `--base-url` override) live in the private `aethis-cli-internal` package, installed on request.

## Development

```bash
git clone https://github.com/aethis-ai/aethis-cli.git
cd aethis-cli
uv pip install -e ".[dev]"
pytest tests/ -v
```

## Shell completions

```bash
# Install tab completion for your shell
aethis --install-completion bash   # or zsh, fish, powershell
```

## Troubleshooting

**`aethis generate` times out but server continues**
The server finishes even if your client disconnects. Wait 10–15 min, then run `aethis rulesets list` — if the ruleset appeared, run `aethis test` and `aethis publish`. Do not re-trigger generation; that creates a duplicate run.

**`aethis publish` fails with "tests are failing"**
`publish` refuses a ruleset with failing tests. Fix with guidance + regenerate, or pass `--force` (not recommended for production).

**422 Validation error on `aethis decide`**
`DATE` fields must be passed as **integer ordinals**, not ISO strings. `2025-04-13` → `739354`:
```bash
python3 -c "from datetime import date; print(date(2025,4,13).toordinal())"
```

**`Auth error: …`**
Your API key is missing, expired, or revoked. Run `aethis login` to mint a new one. (As of v0.6.0 the CLI prompts for sign-in inline when an authenticated command runs without a key; use `--no-prompt` to suppress that in CI.)

**`403 Forbidden: missing scope`**
Your key lacks the required scope for the command. `aethis whoami` shows what your current key can do. Contact support to upgrade scopes.

## Benchmarks

See how the engine compares to frontier LLMs on real-world eligibility rules: [aethis-examples](https://github.com/Aethis-ai/aethis-examples)

## License

MIT
