Metadata-Version: 2.4
Name: askfaro-cli
Version: 0.6.1
Summary: CLI for Faro: the tools your AI agent can't run on its own
License: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: askfaro-progressive-context>=0.3.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: jsonschema>=4.25.1
Requires-Dist: pyyaml>=6.0.2
Requires-Dist: rich>=15.0.0
Requires-Dist: typer>=0.23.2
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.22.0; extra == 'dev'
Description-Content-Type: text/markdown

# askfaro-cli

Command-line interface for [Faro](https://askfaro.com): the tools your AI agent can't run on its own.

Two surfaces in one tool:

- **Buyers** — search for skills by natural language and run them from the terminal or any agent that can run shell commands.
- **Publishers** — create namespaces, edit listings declaratively in `faro.yaml`, and submit for review without leaving the shell.

**Agent-readable reference:** the canonical, always-current CLI docs live at <https://askfaro.com/llms/cli.md>. The full discovery index is at <https://askfaro.com/llms.txt>. You can also fetch any of these from the terminal:

```bash
askfaro docs              # full inlined docs (llms-full.txt)
askfaro docs cli          # this CLI reference
askfaro docs skill        # the buyer agent recipe
askfaro docs --list       # all available topics
```

## Install

```bash
pip install askfaro-cli
```

## Quick start

```bash
# Save your API key
askfaro auth login

# Search for a skill
askfaro search "generate an image"

# Run it (the skill agent picks the tools, runs them, and bills your account)
askfaro run image "a red bicycle"
```

## Commands

### `askfaro auth login`
Save credentials to `~/.config/faro/credentials` (mode 0600). Three modes:

```bash
# Email + password — logs in, mints a new API key, saves it. JWT is discarded.
askfaro auth login --email you@example.com

# Paste an existing key
askfaro auth login                          # interactive prompt
askfaro auth login --api-key faro_yourkey   # non-interactive
```

The `--email` flow is the recommended bootstrap for new machines. API keys cannot mint other keys (a leaked key must not be able to bootstrap replacements), so all key creation goes through this flow.

### `askfaro tokens list` / `askfaro tokens revoke <id-or-name>`
List or revoke your API keys. Works with a saved API key (no password re-prompt) so emergency revocation is one command away. To mint a new key, use `askfaro auth login --email`.

```bash
askfaro tokens list
askfaro tokens revoke cli-mylaptop-20260101
```

### `askfaro auth whoami`
Show the currently authenticated user.

### `askfaro auth logout`
Remove the saved API key.

### `askfaro search <query>`
Semantic search over the Faro catalog. Each hit carries enough to act on it without a second lookup (id, schema, pricing); a skill hit's id is what you hand to `askfaro run`.

```bash
askfaro search "transcribe audio"
askfaro search "weather forecast" --top 5 --category data
```

### `askfaro describe <namespace>/<tool>`
Show the full schema and pricing for a single tool.

```bash
askfaro describe acme/send-email
```

### `askfaro run <skill> [prompt]`
Run a skill end-to-end. The hosted skill agent selects the underlying tools, runs
them, enforces your budget, and bills your account; you get back the normalized
envelope. The intent is a prompt string or a JSON object (string, `--intent`,
`--intent-file`, or piped stdin).

```bash
# Prompt shorthand
askfaro run image "a red bicycle"

# JSON intent
askfaro run image --intent '{"prompt": "a red bicycle", "size": "1024x1024"}'

# Stdin
echo '{"prompt": "a red bicycle"}' | askfaro run image

# Budget ceilings: hard cap, and a soft cap that returns a quote instead of spending
askfaro run image "a red bicycle" --max-credits 200 --confirm-above 50
```

A run that would cross `--confirm-above` comes back as a quote
(`status: needs_input`) with a continuation token; re-run with
`--continuation <token>` to proceed.

### `askfaro credits balance` / `askfaro credits purchases`
Check your prepaid credit balance and purchase history. Agents should call `askfaro credits balance` before running expensive skills.

```bash
askfaro credits balance
```

### `askfaro docs [topic]`
Fetch the agent-readable docs from askfaro.com. Renders markdown on a TTY; pipes raw markdown otherwise. Topics: `quickstart`, `skill`, `search`, `invoke`, `cli`, `publish`, or a `<namespace>` / `<namespace>/<tool>` for live listing docs.

```bash
askfaro docs                  # full llms-full.txt
askfaro docs skill            # buyer agent recipe
askfaro docs grok/web_search  # tool docs (markdown mirror of the listing)
askfaro docs --url cli        # print the URL only
```

### `askfaro doctor`
One-shot health check: API key found, authenticated, publisher registered, namespace count.

```bash
askfaro doctor
```

---

## Publishing a listing

The fastest path is the declarative `faro.yaml` workflow:

```bash
# 1. Become a publisher (one time)
askfaro publisher register --display-name 'Acme Corp'

# 2. Scaffold a namespace from your MCP server
askfaro ns quick-setup --namespace acme --mcp-url https://mcp.acme.dev

# 3. Pull a manifest you can edit
askfaro init acme            # writes ./faro.yaml + ./README.md

# 4. Edit faro.yaml — descriptions, tags, per-tool example_prompts.
#    Add `icon_file: ./logo.png` to upload + link the icon on push.
$EDITOR faro.yaml

# 5. Validate, preview, push, and submit
askfaro ns check acme        # local readiness check (mirrors server)
askfaro diff                 # preview manifest vs. server
askfaro push --publish       # save listing draft + submit for review
```

For one-off updates without the manifest, every API endpoint has a CLI counterpart:

| Action | Command |
|---|---|
| List namespaces | `askfaro ns list` |
| Inspect a namespace | `askfaro ns get <slug>` |
| Edit description / tags | `askfaro ns edit-listing <slug> --description '…' --tags 'a,b'` |
| Set per-tool description / prompts | `askfaro ns edit-listing <slug> --tools-file tools.json` |
| Toggle a tool's visibility | `askfaro ns tools set-visibility <slug> <tool_id> --visibility public` |
| Add a REST tool by hand | `askfaro ns tools add-rest <slug> --name search --method GET --path /search` |
| Set group pricing | `askfaro ns groups set-pricing <slug> <group_id> --mode fixed_per_request --fixed-cost 10` |
| Store upstream API key | `askfaro ns creds set <slug> --api-key '…'` |
| Submit for review | `askfaro publish <slug>` |
| Earnings & sales | `askfaro publisher earnings`, `askfaro publisher sales --period last_month` |

Every namespace argument accepts a slug **or** a UUID. Run `askfaro <command> --help` for examples on any individual command.

### `faro.yaml` shape

```yaml
namespace: acme
namespace_id: 5f6f1234-…
listing_name: Acme MCP
description: |
  Multi-line description rendered as `|` block scalars on round-trip.
category: developer-tools
tags: [email, comms]
icon_file: ./logo.png       # uploaded on `askfaro push` → URL stored back here
readme_file: README.md       # contents become the listing's readme_content
tools:
  - id: 1d…                  # UUID is required to update an existing tool
    name: send-email         # informational
    visibility: public
    short_description: Send an email
    display_order: 0
    example_prompts:
      - Send Bob a status update
      - Email everyone in marketing
```

For active namespaces, edits land on a server-side draft overlay until you publish.

---

## Global options

| Option | Env var | Description |
|--------|---------|-------------|
| `--api-key` | `FARO_API_KEY` | Faro API key |
| `--api-url` | `FARO_API_URL` | Override API base URL (default: `https://api.askfaro.com`) |
| `--skill-url` | `FARO_SKILL_URL` | Override the skill-agent base URL used by `run` (default: `https://skill.askfaro.com`) |
| `--pretty` / `--json` | — | Force pretty or JSON output (auto-detected by TTY) |
| `-v` / `--verbose` | — | Log HTTP requests to stderr |

## Authentication precedence

1. `--api-key` flag
2. `FARO_API_KEY` environment variable
3. `~/.config/faro/credentials`

## Exit codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Auth error (invalid or missing key) |
| 3 | Not found |
| 4 | Validation error (bad parameters) |
| 5 | Quota / billing limit reached |
| 6 | Upstream tool error |
| 7 | Network / timeout error (retriable) |

When stderr is a pipe (agent / script), errors are printed as JSON:
```json
{"error": {"code": "quota_exceeded", "message": "...", "retriable": false}}
```
On a TTY they render as `✗ <message>` so the JSON noise doesn't dominate.

---

## Use Faro from your agent

Copy this system-prompt snippet to give any LLM shell access to Faro:

```
You have access to Faro (the tools your agent can't run on its own) via the `askfaro` CLI.

To find a capability:
  askfaro search "<describe what you want to do>"
  Returns a JSON list. Each item includes its id, `short_description`, and `pricing`.

To run it:
  askfaro run <skill> "<intent>"
  The skill agent picks the underlying tools, runs them, enforces your budget, and
  bills your account. Returns the canonical envelope with `result` and `status`.

Rules:
- Always search before running unless you already know the skill id.
- Express the task as a plain-language intent; pass JSON only when the skill needs it
  (askfaro run <skill> --intent '<json>').
- Cap spend with --max-credits; --confirm-above returns a quote (status=needs_input)
  with a continuation token to resume.
- If exit code is 7, retry once. If exit code is 5, tell the user they need more credits.
- Output JSON only; parse `result` for the actual response.

Examples:
  askfaro search "send a slack message"
  askfaro run messaging "post 'deploy is done' to #alerts on Slack"
```

### Worked example — Claude API

```python
import anthropic
import subprocess
import json

client = anthropic.Anthropic()

system = """
You have access to Faro (the tools your agent can't run on its own) via the `askfaro` CLI.
[paste the snippet above]
"""

def run_faro(command: str) -> str:
    result = subprocess.run(
        ["bash", "-c", f"askfaro {command}"],
        capture_output=True, text=True
    )
    if result.returncode not in (0,):
        return result.stderr  # error envelope JSON
    return result.stdout

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    system=system,
    messages=[{"role": "user", "content": "Send a Slack message to #alerts saying the deploy is done."}],
)

# The model will emit askfaro commands; run them and feed results back
```

### Worked example — environment variable auth

```bash
export FARO_API_KEY=faro_yourkey

# In your agent's subprocess call:
result=$(askfaro search "translate text to Spanish" --json)
skill=$(echo $result | jq -r '.[0].id')
askfaro run "$skill" "translate 'Hello world' to Spanish"
```

---

## Browse-first discovery (low-context / on-device agents)

For agents with small context windows, Faro exposes a **progressive-context (pcx)
manifest** — a compact, navigable index of the whole catalog. Instead of loading
every schema, the agent sees short descriptors and drills in on demand.

### Three endpoints

| Verb | Endpoint | What it does |
|------|----------|--------------|
| `index` | `GET /pcx/manifest?budget=4k` | Compact catalog tree (776 tokens at 4k). Inject this into the LLM. |
| `open` | `GET /pcx/open/<node_id>` | Branch: returns child descriptors. Skill leaf: returns full capability + `skill_id`. |
| `look` | `GET /pcx/look?ids=id1,id2` | Full `what`/`when`/`keywords` for specific nodes — without committing to open them. |

Available budgets: `4k` (default) and `32k`. Pass `?budget=32k` to get the fuller variant.

### Caching the manifest

The manifest changes only when the catalog is rebuilt, so it is safe to cache. The
`/pcx/*` endpoints return an `ETag` and honor `If-None-Match` with a `304`, so a
warm cache costs one cheap revalidation instead of a full re-download. `askfaro
browse` does this automatically (an on-disk cache under `~/.cache/askfaro/pcx`,
revalidated by ETag), as does the `askfaro` Python SDK. If you call `/pcx/manifest`
directly, send the last `ETag` back as `If-None-Match` and reuse your copy on a
`304`.

### Node IDs and skill IDs

Every node in the manifest has an `id` like `skill-image` or `cat-media`. Skill leaf
nodes also carry a `skill_id` field — that is the id you hand to `run`:

```
node id:   skill-image          (what you see while navigating)
skill_id:  image                (what you pass to run)
```

Never strip the `skill-` prefix yourself — always read `skill_id` from the `open`
response. Some skill ids contain dots (e.g. `calc.evaluate`) that would not survive
a naive prefix strip.

### Canonical agent integration — expose 3 tools

The recommended pattern is to wire the three endpoints as agent tools so the LLM
drives navigation without writing its own HTTP calls:

```python
import httpx

BASE = "https://api.askfaro.com"
HEADERS = {"Authorization": f"Bearer {FARO_API_KEY}"}

def catalog_index() -> dict:
    """Call first. Returns the compact catalog tree to inject into the LLM."""
    return httpx.get(f"{BASE}/pcx/manifest", params={"budget": "4k"}, headers=HEADERS).json()

def catalog_open(node_id: str) -> dict:
    """Expand a node. Branch -> child descriptors. Skill leaf -> capability + skill_id."""
    return httpx.get(f"{BASE}/pcx/open/{node_id}", headers=HEADERS).json()

def skill_run(skill_id: str, intent: str) -> dict:
    """Run a skill by the skill_id returned from catalog_open."""
    return httpx.post(f"{BASE}/skills/{skill_id}/run",
                      json={"intent": intent}, headers=HEADERS).json()
```

Expose these as tool definitions:

```python
tools = [
    {
        "name": "catalog_index",
        "description": (
            "Fetch the compact Faro catalog index. Call this first whenever you need "
            "a Faro capability. Returns category and skill descriptors (what/when) "
            "you can navigate."
        ),
        "input_schema": {"type": "object", "properties": {}, "required": []},
    },
    {
        "name": "catalog_open",
        "description": (
            "Expand a catalog node. For a branch (category) it returns child "
            "descriptors; for a skill leaf it returns the full capability description "
            "and the `skill_id` to use with skill_run. Use `look` via the API if you "
            "need full descriptors for several nodes without opening them."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"node_id": {"type": "string", "description": "The node id from the manifest"}},
            "required": ["node_id"],
        },
    },
    {
        "name": "skill_run",
        "description": (
            "Run a Faro skill end-to-end. Pass the `skill_id` from catalog_open and "
            "a natural-language intent. Returns the normalized result envelope."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "skill_id": {"type": "string"},
                "intent": {"type": "string"},
            },
            "required": ["skill_id", "intent"],
        },
    },
]
```

### End-to-end flow

```
1. catalog_index()
   → {nodes: {cat-media: {what: "Images, video, audio", ...}, ...}}

2. catalog_open("cat-media")
   → {type: "branch", children: [{id: "skill-image", what: "Generate or find images", ...}]}

3. catalog_open("skill-image")
   → {type: "leaf", skill_id: "image", content: "# Image\n...", ...}

4. skill_run("image", "a red bicycle on a white background")
   → {status: "success", result: {url: "https://..."}}
```

The agent only pays for the tokens it opens — not the whole catalog. At `4k` budget
the initial index is ~776 tokens; a typical two-hop navigation reaches a skill in
under 200 additional tokens.

### System-prompt snippet (browse variant)

```
You have access to the Faro skill catalog via three tools:

catalog_index()         — fetch the compact catalog tree. Call this first.
catalog_open(node_id)   — expand a node; skill leaves return `skill_id` + full description.
skill_run(skill_id, intent) — run the skill. Use the skill_id from catalog_open.

Workflow:
  1. Call catalog_index() to see available categories and skills.
  2. Call catalog_open() on the node whose `when` matches your goal.
     If a category branch, open its children to reach a skill leaf.
  3. Call skill_run() with the returned skill_id and a plain-language intent.

Rules:
  - Always navigate before running unless you already know the skill_id.
  - If you cannot decide from a node's brief descriptor, call the API's
    /pcx/look?ids=<id1>,<id2> endpoint to get full what/when without opening.
  - Cap spend with max_credits / confirm_above on skill_run if needed.
```

---

## MCP vs CLI

| | MCP | CLI |
|--|-----|-----|
| Setup | Config block in your host app | `pip install askfaro-cli` |
| Best for | Claude Desktop, Cursor, MCP-native hosts | Custom agents, Claude API, any shell |
| Discovery | Dynamic at connect time | `askfaro search` |
| Auth | API key in MCP config | `FARO_API_KEY` or `askfaro auth login` |
| Debuggable | Harder | Run the exact same command yourself |

Both use the same API keys and the same tool catalog.
