Metadata-Version: 2.4
Name: appxen
Version: 0.2.7
Summary: AppXen CLI — manage your MCP Gateway and RAG Engine from the terminal
Author-email: AppXen <support@appxen.ai>
License-Expression: LicenseRef-Proprietary
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: rich>=13.0.0
Requires-Dist: tomli-w>=1.0.0
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
Requires-Dist: typer>=0.16.0
Provides-Extra: dev
Requires-Dist: pip-audit>=2.7.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
Requires-Dist: pytest>=8.3.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Description-Content-Type: text/markdown

# AppXen CLI

Command-line tool for managing your AppXen MCP Gateway and RAG Engine. Ingest documents, search your knowledge base, and manage sources — all from the terminal.

## Install

```bash
curl -sSL https://appxen.ai/install.sh | sh
```

Or install from PyPI:

```bash
pip install appxen
```

For local development:

```bash
pip install -e products/appxen-cli/
```

Requires Python 3.10+.

## Quick Start

### 1. Log in with your API key

Get an API key from your appliance's console (https://<your-appliance-host>) under **API Keys**, then:

```bash
appxen login
```

By default the CLI talks to `https://localhost` — the appliance's own Caddy origin,
which fronts the console, gateway, and orchestrator behind a self-signed certificate.
Running the CLI on the box itself, add `--insecure` (or set `APPXEN_INSECURE=1`) to skip
TLS verification for that cert:

```bash
appxen login --insecure
```

Running the CLI from elsewhere? Point it at your appliance's own host:

```bash
appxen login --endpoint https://your-appliance-host
```

If the appliance has a domain name (set with `sudo appxenctl set-domain <your-domain>`),
always pass `--endpoint https://<your-domain>`, on the box itself too. Caddy then serves
only that domain, with its managed certificate, so the default `https://localhost` fails
the TLS handshake and `--insecure` does not help. The default works only on a box without
a domain, whose Caddy listens on `:443` with the self-signed certificate.

```bash
appxen login --endpoint https://appliance.example.com
```

In a terminal you are prompted for your key (input is hidden). In a script, pipe it: when stdin
is not a terminal, the key is the first line of stdin, and nothing is echoed:

```bash
printf '%s\n' "$APPXEN_KEY" | appxen login --endpoint https://your-appliance-host
```

The CLI checks the key's format (`axgw_*`),
then verifies it with the appliance on an authenticated route. Only a key the appliance
accepted is saved: a refused key, or one that could not be verified (the appliance is
unreachable, its certificate is refused), is not saved and the command exits 1. For an
appliance that cannot be reached yet, `appxen login --no-verify` saves the key unchecked.

Your key is stored in `~/.config/appxen/config.toml`, mode `0600`, in a directory of mode `0700`.

Against the appliance's self-signed certificate (an appliance without a domain) the CLI refuses
the connection and says so. `--insecure` (or `APPXEN_INSECURE=1`) skips the verification: use it
for your own appliance, over a network path you trust, until it has a domain
(`sudo appxenctl set-domain <name>`); never against a host you do not control. Every command
that skips the verification says so, in one warning line on stderr.

### 2. Check connectivity

```bash
appxen status
```

```
CLI:       0.2.7
Endpoint:  https://localhost
Service:   mcp-gateway-pro
Gateway:   0.1.0
Status:    Connected
RAG:       12 sources, 347 chunks
```

### 3. Ingest documents

Single file:

```bash
appxen ingest ./report.pdf
```

Entire directory (recursive by default):

```bash
appxen ingest ./docs/
```

The CLI uploads each file, then polls until ingestion completes (chunking, embedding, indexing). You'll see progress bars for both stages:

```
Found 14 file(s) to ingest.
  Uploading architecture.pdf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14/14
Waiting for 14 file(s) to process...
  Processing architecture.pdf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14/14

OK 14 file(s) ingested successfully.
```

### 4. Search

```bash
appxen search "how does authentication work"
```

```
╭──────────── #1 auth-design.md (score: 0.847) ────────────╮
│ Authentication uses passkeys (WebAuthn) as the primary    │
│ method with magic link email as a fallback...             │
╰──────────────────────── chunk: a1b2c3d4-e5f ─────────────╯
╭──────────── #2 api-keys.md (score: 0.723) ────────────────╮
│ API keys use the format axgw_{mode}_{version}_{hex} and   │
│ are stored as SHA-256 hashes in DynamoDB...               │
╰──────────────────────── chunk: f6g7h8i9-j0k ─────────────╯
```

## Commands

### `appxen login`

Save your API key to the local config.

```bash
appxen login --insecure                                    # default profile, on a box without a domain
appxen login --profile staging                              # named profile
appxen login --endpoint https://your-appliance-host          # remote appliance
appxen login --endpoint https://appliance.example.com       # appliance with a domain (on the box too)
```

| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--profile` | `-p` | the global `appxen --profile`, then `APPXEN_PROFILE`, then `default` | Profile to save to |
| `--endpoint` | `-e` | `https://localhost` | Appliance origin — gateway and orchestrator paths are derived from it |
| `--insecure` | | `false` | Skip TLS verification, for the appliance's self-signed certificate (also `APPXEN_INSECURE=1`) |
| `--verify / --no-verify` | | `--verify` | Verify the key with the appliance before saving it; `--no-verify` saves it unchecked |

Exit status of `appxen login`: `0` the key was saved (verified first, unless `--no-verify`);
`1` the key was refused, could not be verified or was not given, and nothing was saved;
`2` usage: a malformed endpoint, or one that carries a user name or password (checked before
the key is read), an unknown option.

### `appxen status`

Check connectivity and the API key, show gateway info and RAG stats. The key is verified on an
authenticated route (`/health` answers anyone).

```bash
appxen status
appxen status --json              # machine-readable output
```

Exit status of `appxen status`: `0` connected, with a key the appliance accepted;
`1` the gateway is unreachable, or the key was refused (401, 403) or could not be verified (the
authenticated route failed another way);
`2` usage: a malformed endpoint or API key, an unknown option.

On `1` nothing is printed on stdout.

### `appxen ingest <path>`

Upload files to the RAG knowledge base. Accepts a single file or a directory.

```bash
appxen ingest ./report.pdf                    # single file
appxen ingest ./docs/                         # directory (recursive)
appxen ingest ./docs/ --glob "*.md"           # only markdown files
appxen ingest ./docs/ --no-recursive          # top-level only
appxen ingest ./docs/ --no-wait               # upload and exit (don't wait)
appxen ingest ./docs/ --json                  # JSON output: waits, then prints the final state
appxen ingest ./docs/ --json --no-wait        # JSON output of the accepted state (status: queued)
appxen ingest ./scans/ --timeout 1800         # wait up to 30 minutes for each file
```

| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--glob` | `-g` | `*` | Glob pattern for filtering files in a directory |
| `--recursive / --no-recursive` | | `--recursive` | Recurse into subdirectories |
| `--wait / --no-wait` | | `--wait` | Wait until every file is ingested, with `--json` too; `--no-wait` returns once the uploads are accepted (`--poll / --no-poll` are the same option) |
| `--timeout` | | `300` | Seconds to wait for EACH file before reporting it as not ready (the appliance goes on ingesting it) |
| `--json` | | `false` | Output raw JSON: one array, an entry per file |

With `--json`, stdout is the array and nothing else; the progress goes to stderr. An entry is
`{"file", "source_id", "filename", "status", "chunk_count"}`; a file that failed has `"error"`
with the reason. A file that is not ready when its `--timeout` passes is a failure, and so is one
whose source disappears during the wait (someone deleted it): the command goes on waiting for the
other files and its document, or its summary, holds every file with its source id. Only a refused
API key ends the command at once: no other file can succeed.

**Supported file types:**

| Category | Extensions |
|----------|------------|
| Documents | `.pdf` `.docx` `.html` `.htm` `.md` `.txt` `.csv` `.json` `.xml` `.yaml` `.yml` |
| Code | `.py` `.js` `.ts` `.jsx` `.tsx` `.css` `.sql` `.sh` `.rs` `.go` `.java` `.c` `.cpp` `.h` `.rb` `.php` |
| Images (OCR) | `.png` `.jpg` `.jpeg` `.tiff` `.bmp` |

Unsupported file types are silently skipped when scanning directories. The maximum file size is 100 MB.

Exit status of `appxen ingest`: `0` every file was ingested (with `--no-wait`: accepted);
`1` nothing could be ingested at all: no supported file, not logged in, the API key was refused;
`2` some or all files failed (upload refused, ingestion failed, not ready within `--timeout`, the
source disappeared during the wait): the other files are still waited for, and with `--json` the
document says which failed. Also a usage error (an unknown option, a `--timeout` that is not a
positive number, a malformed endpoint or API key), which prints no document;
`130` interrupted (Ctrl-C): stderr says how many files were uploaded and each one's last status,
and the appliance goes on ingesting what it accepted.

`2` for files that failed is the meaning it has had since 0.2.4 ("partial failure"). A script
tells it from a usage error by the output: with `--json`, files that failed come with the
document, each failed entry carrying `"error"`; a usage error prints nothing on stdout.

### `appxen search <query>`

Semantic search over your knowledge base.

```bash
appxen search "database schema design"
appxen search "error handling" --top-k 10
appxen search "deployment steps" --json
```

| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--top-k` | `-k` | `5` | Number of results to return |
| `--json` | | `false` | Output raw JSON |

### `appxen sources`

List and manage knowledge base sources.

```bash
appxen sources                            # list all sources
appxen sources --status ready             # filter by status
appxen sources --search "report"          # filter by filename
appxen sources --limit 50                 # more results
appxen sources --json                     # JSON output
```

| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--status` | `-s` | | Filter by status (`ready`, `processing`, `queued`, `failed`) |
| `--search` | `-q` | | Filter by filename |
| `--limit` | `-n` | `20` | Results per page |
| `--offset` | | `0` | Pagination offset |
| `--json` | | `false` | Output raw JSON |

```
                          Sources (47 total)
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ ID           ┃ Filename              ┃ Status ┃ Chunks ┃ Created             ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ a1b2c3d4-e5f │ architecture.pdf      │ ready  │     23 │ 2026-02-08T14:30:00 │
│ f6g7h8i9-j0k │ api-reference.md      │ ready  │      8 │ 2026-02-08T14:28:00 │
│ ...          │ ...                   │ ...    │    ... │ ...                 │
└──────────────┴───────────────────────┴────────┴────────┴─────────────────────┘
```

### `appxen sources delete <source_id>`

Delete a source and all its chunks.

```bash
appxen sources delete a1b2c3d4-e5f6-7890-abcd-1234567890ab
appxen sources delete a1b2c3d4-e5f6-7890-abcd-1234567890ab --yes   # skip confirmation
```

| Option | Short | Description |
|--------|-------|-------------|
| `--yes` | `-y` | Skip the confirmation prompt |

### `appxen stats`

Show knowledge base statistics.

```bash
appxen stats
appxen stats --json
```

```
╭──────────────────────── RAG Engine Stats ─────────────────────────╮
│ Sources:  47                                                      │
│ Chunks:   1,203                                                   │
│ Storage:  24.7 MB                                                 │
╰───────────────────────────────────────────────────────────────────╯
```

### `appxen workflow`

Workflows of the Agent Orchestrator: `list`, `compile <file>`, `create <file>`, `get <id>`,
`delete <id>`, `run <id>`, `status <execution>`, `output <execution>`, `stop <execution>`.

```bash
appxen workflow create ./digest.md --name digest
appxen workflow run wf_123 -i topic=security        # starts it and waits for the end
appxen workflow run wf_123 --no-poll --json         # starts it and returns the execution id
appxen workflow status exec_456 --poll --timeout 1800
appxen workflow output exec_456 --save result.md
appxen workflow run wf_123 --json --save result.md  # the document on stdout AND the output in the file
appxen workflow stop exec_456
```

`workflow run` (unless `--no-poll`) and `workflow status --poll` wait until the execution
ends: `completed`, `failed`, `rejected`, `timed_out` or `aborted`.

| Option | Default | Description |
|--------|---------|-------------|
| `--timeout` | `600` | Seconds to wait before giving up; the execution keeps running on the appliance |

Exit status of `appxen workflow run`: `0` the execution completed (with `--no-poll`: it was
started);
`1` it could not be started, it ended `failed`, `rejected`, `timed_out` or `aborted` (its error
is printed), or it was still running when `--timeout` passed;
`2` usage: `--save` together with `--no-poll`, an unknown option, a malformed endpoint or API key;
`130` interrupted (Ctrl-C) during the wait: stderr says what status the execution was last seen
in, and it keeps running on the appliance.

Exit status of `appxen workflow status`: `0` the status was read (with `--poll`: the execution
completed);
`1` the execution could not be read, or with `--poll` it ended `failed`, `rejected`, `timed_out`
or `aborted`, or it was still running when `--timeout` passed;
`2` usage: an unknown option, a `--timeout` that is not a positive number, a malformed endpoint
or API key;
`130` interrupted (Ctrl-C) during `--poll`: the execution keeps running on the appliance.

An execution held at an approval gate (`awaiting_approval`) is approved or rejected in the
console. With `--json`, stdout is one document, `{"execution": ..., "steps": [...]}`: the final
state, or at the deadline the state as last seen; nothing after an interrupt.

`--save FILE` (`workflow run`, `workflow output`) writes the output to a file, with `--json`
too (the "saved" line is then on stderr). It is refused with `--no-poll` (exit 2: there is no
output to save yet), and with no output to write it is an error, never an empty file.

`workflow stop` on an execution that had already ended changes nothing and says so
(`had already ended (status: completed); nothing was stopped`), exit 0.

## Configuration

### Config file

Stored at `~/.config/appxen/config.toml`:

```toml
[default]
api_key = "axgw_live_k1_..."
endpoint = "https://localhost"
insecure = true

[staging]
api_key = "axgw_test_k1_..."
endpoint = "https://staging-appliance.example.com"
insecure = false
```

Switch profiles with `--profile` or `APPXEN_PROFILE`:

```bash
appxen --profile staging sources
appxen -p staging search "test query"
APPXEN_PROFILE=staging appxen stats
appxen --profile staging login          # saves to [staging]
```

Which profile is used, nearest first: the command's own `--profile` (only `login` has one),
the global `appxen --profile`, `APPXEN_PROFILE`, then `default`. `insecure` skips TLS
verification only as the boolean `true`. The file is replaced atomically on every save.

### Environment variables

Environment variables override the config file:

| Variable | Description |
|----------|-------------|
| `APPXEN_API_KEY` | API key. When set, NO profile is read: the endpoint is `APPXEN_ENDPOINT` (default `https://localhost`) |
| `APPXEN_ENDPOINT` | Appliance origin, used with `APPXEN_API_KEY` |
| `APPXEN_INSECURE` | `1`, `true`, `yes` or `on` skips TLS verification, for the appliance's self-signed certificate; any other value (`0`, `false`, empty) keeps it on. Overrides the profile's `insecure` |
| `APPXEN_PROFILE` | Profile to use when `--profile` is not given (default: `default`) |

The endpoint is the appliance's origin (`https://<host>[:port]`). One that carries a user name
or password (`https://user:pass@host`) is refused. Plain `http://` to a host other than this
machine works, with a warning on stderr that the API key travels unencrypted.

```bash
# One-off command against the appliance box itself
APPXEN_API_KEY=axgw_test_k1_abc123 APPXEN_INSECURE=1 appxen status
```

## JSON Output

Commands that read from the appliance take `--json` for scripting and piping. Stdout is then
exactly one JSON document, written as it is (never wrapped to the terminal width, text outside
ASCII escaped); progress, warnings and errors go to stderr; a command that fails prints no
document, except a wait that ended (see `appxen workflow`) and `ingest` (the array says which
file failed).

```bash
# Get source IDs for all ready sources
appxen sources --json | jq '.sources[] | select(.status == "ready") | .source_id'

# Count chunks across all sources
appxen stats --json | jq '.chunk_count'

# Ingest and capture results
appxen ingest ./docs/ --json | jq '.[] | {file, status, source_id}'
```

## Exit status

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | The command failed: not logged in, the appliance is unreachable or refused the API key, an execution did not complete |
| `2` | Usage: an unknown option, a malformed endpoint (`https://host:abc`, `https://user:pass@host`), an API key that cannot be sent, `--save` with `--no-poll`. For `ingest` also: some or all files failed (see `appxen ingest`) |
| `130` | Ctrl-C during a wait (`ingest`, `workflow run`, `workflow status --poll`) |

Errors and warnings go to stderr, each message on one unwrapped line, and never contain the API
key. A certificate that cannot be verified, a refused key and a malformed endpoint each say
what to do next.

`appxen status` (without `--json`) and `appxen login` also look for a newer CLI at
`https://appxen.ai/releases/latest.json` (3 seconds at most; a failure is silent, a success is
remembered for a day).

## Examples

### Ingest a project's documentation

```bash
appxen ingest ./docs/ --glob "*.md" --recursive
```

### Ingest only PDFs from a folder

```bash
appxen ingest ./reports/ --glob "*.pdf"
```

### Search and get raw JSON for processing

```bash
appxen search "error handling patterns" --top-k 20 --json
```

### Delete all failed sources

```bash
appxen sources --status failed --json \
  | jq -r '.sources[].source_id' \
  | xargs -I{} appxen sources delete {} --yes
```

### Use in CI/CD

```bash
export APPXEN_API_KEY=${{ secrets.APPXEN_API_KEY }}
export APPXEN_ENDPOINT=https://your-appliance-host   # your appliance's origin

# Sync docs on every deploy
appxen ingest ./docs/ --glob "*.md"
```
