Metadata-Version: 2.5
Name: flieber
Version: 0.1.2
Summary: The flieber command: CLI and MCP server for Flieber commerce data (actual sales, adjusted sales, forecast).
Project-URL: Homepage, https://flieber.com
Author: Flieber
License: Proprietary
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Environment :: Console
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27
Requires-Dist: mcp<2,>=1.24.0
Requires-Dist: pydantic>=2.7
Requires-Dist: rich>=13
Requires-Dist: starlette>=0.40
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn>=0.30
Provides-Extra: hosted
Requires-Dist: clerk-backend-api<8,>=7.0.0; extra == 'hosted'
Provides-Extra: keyring
Requires-Dist: keyring>=25; extra == 'keyring'
Description-Content-Type: text/markdown

# flieber-cli

`flieber` is the command-line and agent interface to Flieber's commerce data. It gives developers, scripts and AI agents (Claude, Cursor, Lovable, custom agents) one stable, versioned way to read a tenant's:

- **actual sales**: what each channel recorded, per product × store;
- **adjusted sales**: actuals corrected for stockouts, outliers and price effects, used as the forecast input;
- **forecast**: expected demand per product × store, from today on (`meta.last_nonzero_forecast_period` is the latest period with a non-zero forecast; core-service exposes no horizon).

> **Status: v0 in design.** The architecture and contract v1 are [ADR-0001](docs/adr/0001-v0-architecture-and-contract-v1.md) (Accepted). The code arrives through the issues in the Linear project "Data Layer Product (Vibee) | Backend" (team Flieber AI - R&D, milestone M3). Only the `0.0.1` placeholder is published so far.

## Shape

One Python package: a shared core, and several surfaces built on it.

```
            ┌──────────────────── flieber (Python package) ────────────────────┐
            │  core: auth · GraphQL client · versioned tool contract · shaping  │
            └──────┬──────────────────────┬───────────────────────┬────────────┘
                   │                      │                       │
          CLI  `flieber …`      MCP (local)  `flieber mcp`   MCP (hosted)  `flieber mcp --http`
          humans & scripts      stdio, launched by           container behind a stable URL,
                                Claude / Cursor              used by the self-serve product
```

- **Core**
  - Authentication with a Flieber public-API key (`FLIEBER_API_KEY`, or the OS keyring or a 0600 config file, written by `flieber auth login`).
  - Sign-in with a Flieber account (`flieber login`): Clerk OAuth, then core-service REST with the access token and GraphQL with a short-lived exchanged JWT (ADR-0004). The API-key route stays available behind `FLIEBER_API_KEY_ROUTE` (default `on`).
  - A client for core-service's public GraphQL API.
  - **The versioned tool contract:** one definition per capability. It drives both the CLI command and the MCP tool, so the two cannot drift (a ratchet test checks the parity, ADR-0001 §7).
  - Output shaping for humans (tables) and for LLMs (compact `{columns, rows}`, point caps, pagination).
- **CLI:** `flieber status`, `flieber listings`, `flieber products`, `flieber mapping`, `flieber sales`, `flieber adjusted`, `flieber forecast`, `flieber trends`, and `flieber onboard …` (below). JSON output is available for scripts.
- **MCP:** the same capabilities as MCP tools, over stdio (local) or streamable HTTP (hosted, stateless). The hosted mode forwards the caller's own API key, or accepts a Flieber sign-in (a Clerk OAuth token it verifies with Clerk, for a claude.ai connector), and stores nothing.

**Design rule:** customers bind to this contract, never to the raw GraphQL schema. The backend behind it may change; the contract changes only through versioning.

## Signing in

`--env` is global: `flieber --env qa login`, not `flieber login --env qa`. `prod` is the default.

Two credentials, kept separate:

- **API key.** `flieber auth login` stores `<system_user_id>:<secret>`. `FLIEBER_API_KEY` overrides the stored key. `flieber auth logout` removes it. `flieber auth status` says where the key comes from and whether an account is signed in. It never prints a key or a token.
- **Flieber account.** `flieber login` opens a browser (or prints a URL, for an SSH session) and stores a Clerk OAuth token set under `oauth:<env>:<profile>`, so a QA login is never sent to production. `flieber logout` forgets it. GraphQL reads then use a JWT exchanged from that login, cached for the organisation it was exchanged for. Writes over this login stay off (`FLIEBER_OAUTH_WRITES` is off unless it is `1` or `true`).

`FLIEBER_API_KEY_ROUTE=off` turns the API-key route off without a code change. The default is `on`. On the hosted MCP this is a Railway variable, read when the process starts. While it is off, the hosted `/mcp` route refuses every API key; a Flieber sign-in (OAuth) still works, and `/healthz` stays open. The CLI asks you to `flieber login` instead.

The flieber CLI Clerk app is a public PKCE client. QA's client id is `OrZunJwBXl8yTnzX`; production's is `oQmB5XYQbPbW9uZ8`. `FLIEBER_OAUTH_CLIENT_ID` overrides either. Login listens on `127.0.0.1:8765` (Clerk requires that exact redirect). If that port is busy, login stops and names it. The tests here do not sign in against QA or production. core-service accepting this client is AIDEV-647 and is not proved by this package; production does not accept it until that allow-list is on.

### Which organisation

`--tenant` is global too (`flieber --tenant org_… <command>`, or `FLIEBER_TENANT`). It overrides the organisation stored at login for that call. `flieber login --tenant org_…` stores that organisation and beats `FLIEBER_TENANT`; the global `--tenant` still wins. The value is a tenant UUID or a Clerk organisation id (`org_…`).

On MCP (stdio and hosted) every tool takes an optional `tenant` argument instead (AIDEV-572). It wins over `--tenant`, `FLIEBER_TENANT` and the stored organisation for that one call, and a preview made in one organisation cannot be committed in another. On the CLI the global `--tenant` fills that argument; there is no per-command `--tenant`. An API key reads only its own tenant: with a key, `tenant` (or the global `--tenant`, including `flieber --tenant X mcp` and `flieber --tenant X auth login`) is refused with `invalid_input` rather than ignored. `FLIEBER_TENANT` is ignored with a key, because it is often set globally.

The portal's organisation switcher shows the organisation's **name**, not its id. The id is under the user menu → Manage organization. A person who belongs to one organisation can omit `--tenant`. A person who belongs to several, and does not pass one, gets `invalid_input` telling them to (on MCP: to pass the `tenant` argument). `flieber tenants` (the `tenants` tool on MCP) lists the organisations your sign-in belongs to, with the ids to pass as `tenant`; it needs core-service's AIDEV-656 endpoint, and an API key cannot use it.

### Environment variables

| Variable | Meaning |
|---|---|
| `FLIEBER_ENV` | `prod` (default) or `qa`. |
| `FLIEBER_API_URL` | GraphQL URL override. |
| `FLIEBER_REST_URL` | REST base override. Defaults: `https://app.flieber.com/api/v1`, `https://app.flieberqa.com/api/v1`. |
| `FLIEBER_API_KEY` | Public-API key. Ignored when `FLIEBER_API_KEY_ROUTE=off`. |
| `FLIEBER_API_KEY_ROUTE` | `on` (default) or `off`. |
| `FLIEBER_OAUTH_CLIENT_ID` | Clerk public client id. Defaults: QA `OrZunJwBXl8yTnzX`, production `oQmB5XYQbPbW9uZ8`. |
| `FLIEBER_OAUTH_ISSUER` | Clerk Frontend API origin. Defaults: QA `https://one-pug-9.clerk.accounts.dev`, production `https://clerk.flieber.com`. |
| `FLIEBER_OAUTH_SCOPES` | Default `profile email offline_access`. `openid` is not requested. |
| `FLIEBER_OAUTH_REDIRECT_PORT` | Loopback port. Default `8765` (the port Clerk has registered). `0` is rejected. |
| `FLIEBER_OAUTH_WRITES` | `1` or `true` enables writes over OAuth. Default off. `on` does not enable it. |
| `FLIEBER_TENANT` | Tenant UUID or `org_…`. The global `--tenant` flag wins. |
| `FLIEBER_PROFILE` | Credential profile. Default `default`. |
| `FLIEBER_ONBOARDING_MODE` | `mcp` (default) or `rest`. Which onboarding adapter `flieber login` uses. |
| `FLIEBER_ONBOARDING_URL` | Onboarding tunnel URL. Defaults to the Railway doors for `prod` and `qa`. |
| `FLIEBER_ONBOARDING_TIMEOUT_S` | Tunnel timeout in seconds. Default `190`. A value below `190` is rejected. |
| `FLIEBER_CLERK_SECRET_KEY` | Hosted server only (`flieber mcp --http`): the Clerk secret key it verifies OAuth bearers (`oat_…`) with. Never printed or logged. Unset: an `oat_` bearer is refused with 503. |
| `FLIEBER_HOSTED_*` | Hosted server only: the gate in front of Clerk. See [Hosted server: the gate in front of Clerk](#hosted-server-the-gate-in-front-of-clerk). |
| `FLIEBER_PUBLIC_URL` | Hosted server only: its https origin (`https://host[:port]`, no path), for the OAuth discovery documents and the 401 challenge. Defaults to `https://$RAILWAY_PUBLIC_DOMAIN`; unset with neither: no discovery. `flieber mcp --http` refuses any other value; no other command reads it. |

Onboarding uses the `flieber login` session. When an API key and a session are both present, GraphQL stays on the key and the key's tenant; onboarding and the write checks use the session's tenant. REST mode refuses a tenant with more than 1000 stores, locations or store settings; use `--mode mcp` for that tenant.

### Hosted server: the gate in front of Clerk

`flieber mcp --http` verifies every Flieber sign-in (`oat_…`) with Clerk's Backend API, under a secret shared with the ai-lab agent tunnel. In front of that call stands a gate, in memory, per process (one replica per environment, ADR-0002), built by `flieber.app.build_hosted_gate` ([ADR-0004](docs/adr/0004-clerk-oauth-login-and-api-key-toggle.md), "Production gate"). In order, for an `oat_` bearer:

1. **Verification cache.** A token Clerk verified is remembered by its SHA-256 (never the token) for the TTL, and never past the expiry Clerk reports for it. A token Clerk rejected (`auth_invalid`) is answered 401 from memory for the negative TTL. A Clerk outage (503) is never remembered. Neither answer spends a bucket.
2. **Single flight.** Concurrent requests for the same new token wait for one Clerk call and share its answer.
3. **Per-client bucket**, then **global bucket.** Each Clerk call takes one token from the client's bucket and one from the process-wide bucket. An empty bucket is 429 `rate_limited` with `Retry-After` and `retry_after_s`, and Clerk is not called.

API keys (`<system_user_id>:<secret>`) never pass through the gate; they keep the client-side bucket per key. A revoked token keeps working for up to the cache TTL. Every value is read when the process starts; a bad value stops `flieber mcp --http` with `invalid_input`.

| Variable | Default | Allowed | Meaning |
|---|---|---|---|
| `FLIEBER_HOSTED_VERIFY_CACHE_TTL_S` | `60` | `0`–`300` | Seconds a verified token is trusted without asking Clerk. `0` turns the positive cache off. Also the longest a revoked token keeps working. |
| `FLIEBER_HOSTED_VERIFY_CACHE_SIZE` | `10000` | `1`–`1000000` | Tokens remembered (each of the positive and negative caches); the least recently used goes first. |
| `FLIEBER_HOSTED_VERIFY_NEGATIVE_TTL_S` | `5` | `0`–`60` | Seconds a rejected token is answered 401 without asking Clerk. `0` turns it off. |
| `FLIEBER_HOSTED_VERIFY_PER_CLIENT_PER_MINUTE` | `120` | `1`–`6000` | Clerk calls a minute per client address (burst of the same size). An IPv6 client counts as its /64. 120, not lower: a claude.ai connector calls from Anthropic's servers, so one address carries many users' connectors (each costing one Clerk call a minute at most, with the 60 s cache). Watch `limited` lines with `bucket` `client`. |
| `FLIEBER_HOSTED_VERIFY_GLOBAL_PER_SECOND` | `20` | `1`–`1000` | Clerk calls a second for the whole process (burst of the same size). |
| `FLIEBER_HOSTED_CLIENT_IP_HEADER` | `X-Real-IP` | a header name, or `none` | The header that carries the client's address. Its right-most value is used; without it, or with a value that is not an IP address, the TCP peer is. `none` uses the TCP peer only. |

**The client address on Railway.** Railway's edge sets `X-Real-IP` to the client's address ([specs](https://docs.railway.com/networking/public-networking/specs-and-limits)) and overwrites any copy the client sends, together with `X-Forwarded-For` (Railway staff, September 2026). `X-Forwarded-For` has been observed to arrive as `client, CDN hop`: its right-most entry is a shared edge address, so it is not used by default. The TCP peer is Railway's proxy (a `100.x` address shared by everyone), so without the header every client would share one bucket. Behind any other proxy, set the header that proxy sets and strips; with no proxy, set `none`.

**Logs.** Each gate decision is one `oauth_verify_gate` line with `outcome`, the token's 8-character fingerprint (`key_sha8`) and the Clerk `subject`. Never the token, and never the client's address. Answers from memory (`cache_hit`, `rejected_cached`, `joined`) are DEBUG, so they do not appear at the hosted server's INFO level; `verified`, `rejected` and `limited` are INFO; `unavailable` (Clerk could not decide) is WARNING. `limited` adds `bucket` (`client` or `global`) and `retry_after_s`, and is sampled: under a flood, at most one line per bucket key every 5 seconds, the next one carrying `suppressed` (how many it held back).

## Onboarding (`flieber onboard`, AIDEV-644)

`flieber onboard …` onboards the signed-in user's selected tenant: connect stores, map SKUs, set up inventory locations, link stores to locations, and finish setup ([ADR-0003](docs/adr/0003-writes-and-onboarding.md)). The same operations are MCP tools named `onboard_*`.

> **Not yet proven against QA.** These commands need a Flieber sign-in (`flieber login`, above). Without one they answer `auth_missing`. Writes over that sign-in stay off until `FLIEBER_OAUTH_WRITES` is set (`writes_disabled`). No mode has a recorded QA run yet (invariants §5).

| Command | What it does |
|---|---|
| `flieber onboard status` | The tenant's onboarding state as the backend reports it: live or not, whether finishing is allowed, what blocks it. |
| `flieber onboard connect <provider>` | amazon, ebay, etsy: prints and opens the consent URL (`--no-browser` prints only), then waits for the new connection to finish importing (`--no-wait`, `--timeout SECONDS`, default 600; a timeout exits 5). Amazon takes `--region` and `--account-type`. walmart, bigcommerce and shopify print where to connect instead; flieber never takes their credentials. |
| `flieber onboard connections` | The connections and their import state. |
| `flieber onboard stores list\|create\|link` | List stores; create one; set a store's fulfillment and storage locations (`--clear-…-location` clears one; an option left out keeps it). |
| `flieber onboard skus list\|map` | List a store's SKUs (`--page`); map SKUs to products from `--file` (a CSV whose headers are `sku,product_code,product_name[,is_archived,thumbnail_url]`) or repeated `--row`. |
| `flieber onboard locations list\|create\|update` | Inventory locations (`--link CONNECTION_ID:EXTERNAL_LOCATION_ID`). |
| `flieber onboard finish` | Finish setup. Irreversible: it sets the tenant live. You confirm by typing the tenant's name; there is no `--yes`. |

**Every write previews first.** The command shows the preview, asks once, then commits exactly what was previewed. `--yes` answers the question for scripts; it does not skip the preview. Without a terminal and without `--yes`, the preview is the output and nothing is applied (exit 0). A declined prompt also exits 0 ("Nothing applied."). Messages and prompts go to stderr; stdout holds one parseable result.

**Large SKU files.** `skus map` previews the file in batches of at most 500 rows. Rows the backend would reject are listed by file row (1 = the first row after the header) and left out; the remaining rows are previewed again. The command asks once, then commits batch by batch. At the first batch that fails, or whose outcome is unknown, it stops and says which file rows were applied, rejected, unknown or not applied, and not attempted. Run it again with `--from-row N` to resume. Committing a row again is safe: the mapping is an upsert.

**`--mode mcp|rest`** (on `flieber onboard`, default `FLIEBER_ONBOARDING_MODE`, else `mcp`) picks the backend: ai-lab's onboarding tools (MCP), or core-service's REST endpoints. In MCP mode the onboarding tools hold the step logic; REST mode cannot tell readiness for a tenant with the Flieber app, and says so.

**`FLIEBER_OAUTH_WRITES=1`** enables writes over a sign-in, onboarding writes included. It is off by default, because core-service's token exchange still grants every write to OAuth users (AIDEV-646); until then a write over OAuth answers `writes_disabled`.

## Principles and how to contribute

- **What the project is,** and the rules no change may break (the three signals, least privilege, no secrets at rest, bounded cost and output, honest results): [`docs/00-invariants.md`](docs/00-invariants.md). It ranks above everything else.
- **How work is done** (proof before done, test-first, ports and adapters, one authoritative path per process): [`AGENTS.md`](AGENTS.md). It applies to humans and agents alike.
- **Decisions:** [`docs/adr/`](docs/adr/README.md).

## Tooling (v0)

- Python ≥ 3.12.
- Packaging with `uv` and `pyproject.toml`; installable as `uvx flieber …` / `pipx install flieber`.
- Tests with `pytest`, run as `uv run pytest` (ADR-0001 §8).

`flieber` is published on PyPI (the `0.0.1` placeholder, 2026-09-29, CI pipeline 2894945075). Releases, the version scheme and the changelog rules: [`docs/release.md`](docs/release.md).

## Development

```
uv sync                                        # Python ≥ 3.12, with the dev dependency group
uv run pytest                                  # tests never reach the network
uv run ruff check src tests scripts && uv run ruff format --check src tests scripts
uv run python -m flieber.contract.document --write   # after a contract change; review the diff
python scripts/pin_schema.py <core-service ref>      # re-pin core-service's schema
scripts/release_check.sh                       # build and verify dist/ exactly as CI does (docs/release.md)
uv run flieber --env qa ping                   # the proof run against QA (needs a QA key)
```

How work is done here, including what counts as proof, is in [`AGENTS.md`](AGENTS.md).

## Related

- **core-service:** the public GraphQL API at `/api/graphql`, and public-API keys with per-resource grants.
- **flieber-build-front:** the self-serve portal, which issues API keys and shows the MCP and CLI configuration.
- **ai-lab `tools/flieber_agent_tunnel`:** the existing `ask_flieber` MCP server. It is the reference for the streamable-HTTP MCP skeleton and the Clerk OAuth pieces.
