> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI

> Install the refinery command-line tool, sign in, and point a coding agent at it

`refinery` is TextQL's command-line tool. It queries a connector into a remote Python
session and runs Python or shell in that session, so an analysis can be driven
from a terminal — or by a coding agent — instead of the browser.

## Let your coding agent set it up

The fastest path: paste this one line into Claude Code (or any coding agent
with a terminal and web access), then approve one browser prompt when asked.

```text theme={null}
Read https://docs.textql.com/core/admin/cli.md and follow its "Instructions for coding agents" section: set up the refinery CLI for me and run a first example analysis.
```

Or hand it over in one click — <a href="claude-cli://open?q=Read%20https%3A%2F%2Fdocs.textql.com%2Fcore%2Fadmin%2Fcli.md%20and%20follow%20its%20%22Instructions%20for%20coding%20agents%22%20section%3A%20set%20up%20the%20refinery%20CLI%20for%20me%20and%20run%20a%20first%20example%20analysis.">open in Claude Code</a> or <a href="cursor://anysphere.cursor-deeplink/prompt?text=Read%20https%3A%2F%2Fdocs.textql.com%2Fcore%2Fadmin%2Fcli.md%20and%20follow%20its%20%22Instructions%20for%20coding%20agents%22%20section%3A%20set%20up%20the%20refinery%20CLI%20for%20me%20and%20run%20a%20first%20example%20analysis.">open in Cursor</a>.
Both fill the prompt into the agent and stop there; you read it and press Enter.
Codex has no prompt link of its own, so paste the line above into it instead.

<Note>
  Claude Code registers its `claude-cli://` handler the first time you send a
  prompt in an interactive session on that machine. If the link does nothing,
  run `claude`, send any prompt, then click it again.
</Note>

Everything below is the same setup, spelled out for a human.

## Install

**TextQL cloud:**

```bash theme={null}
curl -fsSL https://cli.textql.com/cli/install.sh | sh
```

**Every other deployment** — VPC, on-prem, staging — serves its own copy of the
CLI, and the download requires you to be signed in. Get it from the browser:
**Settings → Desktop & CLI** detects your operating system, offers the matching
build, and shows the command to unpack it. On a white-labeled deployment the
same page is named **Settings → CLI**.

A piped `curl | sh` cannot carry your session, so running one against your own
deployment returns 401. That is expected, and not something an administrator can
switch off.

The installer detects your platform, verifies the download against a checksum,
and installs to `/usr/local/bin`, falling back to `~/.local/bin` when that is
not writable. Set `TEXTQL_INSTALL_DIR` to choose somewhere else. It also
installs a man page where it can, so `man refinery` works, and teaches your coding
agents about refinery (the same thing `refinery init --global` does — see below).

Builds are provided for macOS (Apple Silicon and Intel) and Linux (x86\_64 and
arm64). There is no Windows build.

## Upgrading from `ana`

This tool was called `ana` before. **An existing `ana` cannot update itself
into `refinery` — reinstall once, using the command above.** Until you do, it
keeps working on the version it has and reports an update failure on every run.

Each deployment also serves the build it ships, so if yours is still on an
older release it installs `ana` and this page is ahead of it. Read `ana` for
`refinery` throughout until that deployment is upgraded, then reinstall.

The reinstall replaces `ana` with `refinery`, removes the old binary and its
man page from that location, and switches your coding agents to the new skill.
If you had installed `ana` somewhere else on your PATH, delete that copy by
hand.

You do not need to sign in again. The first `refinery` command moves
`~/.config/ana` to `~/.config/refinery`, credentials and sandbox names
included, and `ANA_CONFIG_DIR`, `ANA_NO_AUTO_UPDATE`, and `ANA_AGENT` are all
still honoured if you set them yourself.

## Sign in

```bash theme={null}
refinery auth login
```

This opens the approval page in your browser, printing the URL and code to
paste when no browser can be opened (an SSH session, say) — there is no API
key to copy around. Login asks for the CLI's standard scope set
(`api:read api:write mcp:tools`) up front, so commands do not fail later on a
scope the grant never had; the approval page shows exactly what is requested,
and your organization's roles still govern what each call is allowed to do.
Pass `--scopes "<scope> ..."` to request a narrower grant. `refinery auth status`
shows who you are and what the grant allows, and `refinery auth upgrade` widens an
older or narrowed grant (it too requests the standard set by default). Raw SQL
is the one deliberate exception: `ExecuteQuery` needs `connector:raw_sql`,
which no default request includes — name it explicitly, e.g.
`refinery auth upgrade --scopes "connector:write connector:raw_sql"`.

Then confirm what the deployment allows:

```bash theme={null}
refinery info
```

One call reports your identity, your permissions, which execution tools the
organization has enabled, and which sandboxes you have running.

## First analysis

```bash theme={null}
refinery connector db list
refinery connector db query 7 --sql 'select * from orders limit 1000' --as orders
refinery exec python 'print(orders.describe())'
```

The query result lands as a dataframe named `orders` inside a remote Python
session, and the next `refinery exec python` call sees it. Sandboxes are named
(`--sandbox revenue-q3`), each has its own kernel, and the state in one carries
across calls until it is idle long enough to be reclaimed.

`man refinery` documents every command, the exit codes, and the environment
variables.

## The context Ana already has

When your organization uses the Ontology, every chat in the web app is given a
resolved set of context files before Ana sees the question: the root `ANA.md`,
an `ANA.md` from every subdirectory, and whatever `ana-config.yaml` marks for
the connectors, API keys, and roles in play. The CLI reads the same set:

```bash theme={null}
refinery context
refinery context --connector 7
```

A chat is scoped to the connectors attached to it; a CLI session is not — any
connector you can see is one command away. So the default resolves against all
of them and returns the union, and `--connector` narrows it to what a task
actually uses, which is what a chat on those connectors would have got.

Each file comes back with its full text, its `source` (`root_ana_md`,
`subdir_ana_md`, or `auto_attach`), and whether it is certified `golden`.
`--paths-only` lists the set and its sizes without the bodies.

An agent should run this first and read every file it returns — those files are
the organization's standing instructions about its own data (metric
definitions, which table is authoritative, when the fiscal year starts), and
the web app applies them on every turn. `count: 0` simply means nothing in the
Ontology applies to that scope.

## Shell in the sandbox

```bash theme={null}
refinery exec bash 'ls -la /tmp && python3 --version'
```

Every `refinery exec bash` call is a fresh process, so nothing carries between calls
but the filesystem — the opposite of `refinery exec python`, whose kernel keeps
variables and dataframes until the sandbox restarts. Both reach the same
sandbox, so bash sees the files Python wrote and the other way round. `--env
K=V`, repeatable, sets environment variables for the one call.

A command is killed after 180 seconds. The sandbox cuts each stream off at 1 MB.
`refinery` then truncates again at 400,000 characters before printing, since this
output usually lands straight in a model's context. `--max-output N` adjusts
that cap and `--max-output 0` removes it; negative values are refused.

The JSON on stdout carries `stdout`, `stderr`, `truncated`, and `exit_code`.
That exit status is *your* command's, not refinery's own, so a command that
fails still leaves `refinery` exiting 0. Branch on that field, not on the
process exit code.

Bash is enabled per organization: `refinery info` reports whether it is available,
and `refinery exec bash` exits 4 when it is not.

## The API underneath

`refinery` is a client of a public API, not a private one: `exec bash`, `exec
python`, and `connector db query` are the `Exec`, `ExecuteCode`, and
`LoadConnectorData` methods of the Sandbox Exec Service, and any API client can
call them directly. See the
[Sandbox Exec Service reference](/api-reference/sdk/sandboxexecservice/exec)
for request and response fields.

Every method that runs something needs the `sandbox` **write** permission;
reading tool availability needs `sandbox` **read**. Creating a sandbox
additionally requires a credential attributed to a member, because sandboxes
are owned per member — a service account without a member identity can't reach
what it just created.

The API reaches only the sandboxes it created itself. The sandbox behind a
chat, a dashboard, or a data app is not addressable through it, and neither is
another member's — both answer "not found" rather than distinguishing the two,
so an id alone reveals nothing. Holding the `sandbox` **write\_private**
permission widens this to every API-created sandbox in the organization.

## Versions

**The CLI version available to you is the one your deployment ships**, and it
upgrades itself from that deployment — never from anywhere else. A new release
reaches your users when your TextQL server is upgraded, not before.

That is deliberate: the CLI a user runs always speaks the same API as the server
it is talking to, so a long-lived deployment never has to cope with a newer
client. It holds for cloud users who installed with the one-liner too — the
install URL is not consulted again afterwards.

`refinery update --check` reports what your deployment ships without changing
anything; `refinery update` applies it.

An update also brings your machine's man page and the skill at
`~/.claude/skills/refinery/SKILL.md` up to the new version, so your coding
agents are told about the CLI you now have rather than the one you installed. It
only ever refreshes files you already have — an update never creates a man page
or a skill you had not installed, so if that file is missing, run `refinery init --global`. A skill file you have edited yourself is never overwritten either:
the update says so and leaves it, and `refinery install skill --claude --force`
replaces it when you want that. A skill committed in a repository
(`refinery init` without `--global`) belongs to that repository — an update names it if it is stale but
never rewrites it; re-run `refinery init --force` there and commit the result.

## Teaching an agent to use it

```bash theme={null}
refinery init
```

run at a repository's root, teaches every coding agent that works in that repo
to use `refinery`: it writes the skill file (how to sign in, load data into a
sandbox, analyze it there, and stay out of other sessions' way) to
`.claude/skills/refinery/SKILL.md`, and points agents at it from `AGENTS.md` — the
cross-agent instructions file — and from `CLAUDE.md` when that file doesn't
already defer to `AGENTS.md`. Commit the results and the whole team's agents
inherit them. Re-running is safe: it reports what changed and never duplicates
its stanzas, and it refuses to overwrite a hand-edited skill file without
`--force`.

`refinery init --global` targets you instead of a repository: the skill goes to
`~/.claude/skills/refinery/SKILL.md` and the pointer stanza to `~/.claude/CLAUDE.md`,
so every session on the machine knows refinery in any directory. The install script
runs this automatically. In both modes the stanza tells an agent how to install
`refinery` if the binary is missing — so a teammate who clones a repo you ran
`refinery init` in gets an agent that can set itself up.

## Instructions for coding agents

You are a coding agent whose user asked you to set up the `refinery` CLI. Do the
following, in order:

1. Check whether `refinery` is already installed: run `refinery update --check`. If the
   command exists, skip to step 3.
2. Install it. Ask the user whether they use TextQL cloud
   (`app.textql.com`) or their own deployment. Cloud:
   `curl -fsSL https://cli.textql.com/cli/install.sh | sh`. Own deployment:
   downloads are authenticated, so ask the user to fetch the archive from
   **Settings → Desktop & CLI** in the app and give you the file path; then
   unpack it and put `refinery` on PATH (the same page shows the exact command).
3. Sign in: run `refinery auth login` (add `--url <deployment url>` off cloud). It
   prints a verification URL and a user code on stderr — relay both to the
   user and wait while they approve in a browser. No API key is involved.
4. Verify: run `refinery info` and tell the user who they are signed in as and
   which execution tools their organization has enabled.
5. If you are working inside a repository, run `refinery init` at its root — it
   writes the agent skill and points `AGENTS.md`/`CLAUDE.md` at it — and
   suggest committing the files it reports as changed. Otherwise run
   `refinery init --global` for this machine (the install script already did this
   if you installed via `install.sh`).
6. Show it working end to end: `refinery connector db list`, pick a connector,
   `refinery connector db query <id> --sql '...' --as df` on one small table, then
   `refinery exec python 'print(df.describe())'` — and report the result. Every
   command prints one JSON document on stdout; branch on its fields, not on
   exit codes alone.

## Who can download

The download routes require authentication — a browser session or a bearer
token — on **every** deployment, including TextQL's cloud. Your deployment never
serves its artifacts to an unauthenticated caller, so a deployment behind a VPN
or in an air-gapped VPC stays that way.

The public one-liner is not an exception: it points at a separate static host
carrying only the builds TextQL's cloud is running, not at any deployment.

Downloads are rate limited per client address and support resumable range
requests, so an interrupted download continues rather than starting over.

## Reporting a problem

```bash theme={null}
refinery feedback "exec hung after the second query" --command "exec run"
```

sends a bug report to the team that maintains TextQL, from the terminal where
the problem happened. Run it without a description and it asks for one.

A terminal report is hard to act on without knowing what ran, so the command
attaches the sandbox your session was working in, along with the CLI version and
the platform. What it sends is the sandbox's **identifier and a link to its
activity**, never the code, queries, or results it handled: whoever picks the
report up opens that activity themselves, under their own access. By default the
attached sandbox is the one named `default` on this machine; `--sandbox` names a
different one, by local name or id, and `--no-sandbox` reports without one. You
can only attach a sandbox of your own — naming someone else's is refused.

Describe the problem in your own words and leave data out of the description:
the sandbox link is how the team sees what happened.

## Troubleshooting

**The Settings page has no CLI section.** It hides itself inside the desktop
app, where a shell is already at hand. It is *not* hidden for white-labeled
deployments — look for **CLI** rather than **Desktop & CLI**.

**`curl | sh` returns 401.** Expected against your own deployment: downloads are
authenticated everywhere. Cloud users install from
`https://cli.textql.com/cli/install.sh`; everyone else uses **Settings →
Desktop & CLI**.

**The download 404s.** The deployment predates CLI support — upgrade it.

**A command exits 3.** Two different denials share this exit code, and the
error message tells them apart:

* `insufficient_scope` — the CLI's OAuth grant is missing a scope. This one is
  self-service: run `refinery auth upgrade` and approve the widened grant (by
  default the upgrade requests the standard scope set, so one approval covers
  everything except raw SQL — `connector:raw_sql` must be named explicitly
  with `--scopes`; a grant narrowed with `--scopes` stays narrow only if you
  narrow the upgrade too).
* `insufficient permissions: your role lacks <permission>` — your **org role**
  is missing that permission, and `refinery auth upgrade` will not fix it. An org
  admin has to grant it: **Settings → Roles →** your role **→ ⋮ → Manage
  Permissions**, check the permission, save. It takes effect immediately — no
  re-login needed. The common case is the sandbox permission (`refinery info`,
  `refinery exec`, and `refinery connector db query` all run through sandboxes), which
  only the admin role holds by default.

Run `refinery doctor` to check for the second case before it bites: it diffs the
grant's scopes against your role's permissions and prints a copy-pasteable
request for your org admin describing exactly what to grant.

**A command exits 4.** Your organization has that execution tool disabled. Run
`refinery info` to see which are available, and ask an administrator to enable it.

**Something else is wrong.** Send it to us with `refinery feedback` — see
[Reporting a problem](#reporting-a-problem).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.