ParaQL CLI
Command-line interface for querying Parascope infrastructure data with ParaQL
The ParaQL CLI (paraql) is a terminal client for running ParaQL queries against your
Parascope tenant. It prints results as a Rich table, JSON, CSV, or a bare value you can pipe
into the next command, and it ships an interactive REPL with schema-aware completion and saved
queries.
For the query language itself, see the ParaQL reference.
Installation
pip install paraqlOr, to keep it in its own environment:
pipx install paraqlparaql supports Python 3.11 and later. The PyPI package named parascope is unrelated to
this project and is not published by us; the client is paraql.
Connecting
paraql talks to your tenant, so it needs two things: the tenant endpoint
(https://<slug>.parascope.io) and an API key. There is no built-in default endpoint. When no
layer supplies one, the CLI says so and exits rather than guessing:
no endpoint configured; pass --endpoint https://<slug>.parascope.io, set PARAQL_ENDPOINT, or add endpoint to ~/.paraql/config.tomlGuided setup
config init prompts for the endpoint and key and writes ~/.paraql/config.toml with mode
0600, inside a 0700 directory. The key prompt is hidden, so it stays out of your scrollback:
paraql config initIt replaces the file rather than merging into it, so it confirms before overwriting an existing config, defaulting to No.
Environment variables
Handy in CI, where a config file is awkward:
export PARAQL_ENDPOINT="https://acme.parascope.io"
export PARAQL_API_KEY="ps_ro_..."
paraql "SHOW TABLES"PARAQL_TIMEOUT sets the HTTP timeout in seconds.
Config file
~/.paraql/config.toml holds one section per profile, plus a shared [nl] section:
[default]
endpoint = "https://acme.parascope.io"
api_key = "ps_ro_..."
[lab]
endpoint = "https://lab.parascope.io"
api_key = "ps_ro_..."
output_format = "json"
timeout = 120
[nl]
confirm = falseSelect a profile with --profile:
paraql --profile lab "SHOW TABLES"A profile name you pass that the file does not define produces a warning rather than a silent fallback, and a config file readable by other users produces a permissions warning.
Resolution order
Command-line flags win over environment variables, which win over the config file, which wins over the built-in defaults. To see what resolved and which layer supplied it, with the key masked:
paraql config showsetting value source
endpoint https://acme.parascope.io file
api_key ps_ro_…3f9c env
output_format table default
timeout 30 default
profile default default
config file /home/you/.paraql/config.toml -config show runs even when nothing is configured, so it is the first thing to reach for when
the CLI refuses to connect.
The --api-key flag
--api-key works, but prefer PARAQL_API_KEY or the config file: a key on the command line
lands in your shell history and in the process list, where any other user on the box can read
it. Reserve the flag for one-off use on a machine you control.
Transport security
The endpoint must be https. Plain http is accepted without argument for loopback addresses
(localhost, 127.0.0.1, ::1), where the traffic does not leave the machine. Anywhere else,
plain http is refused unless you pass --insecure, which sends the API key in the clear:
paraql --insecure --endpoint http://staging.internal:8000 "SHOW TABLES"A profile can carry insecure = true in the config file, which waives the check for every
command run under that profile. It deserves more caution than the flag, not less: the flag is
visible in the command you typed, whereas the config key is a standing waiver you will not see
again. Prefer the flag, and keep the key for a lab profile you would not point at production.
Flag placement
--endpoint, --api-key, --profile, --timeout, and --insecure are group-level flags, so
they go before the subcommand:
paraql --timeout 120 query "SELECT name FROM kubernetes.pod"
paraql --profile lab show tables--endpoint, --api-key, and --profile are also accepted after the subcommand, where they
take precedence over the group-level copy. --timeout and --insecure are group-level only.
Write short group flags separately rather than bundled: paraql -i -p lab routes correctly,
paraql -ip lab does not.
Quick Start
# Run a query directly
paraql "SHOW TABLES"
# Select specific fields
paraql "SELECT name, ci_type FROM kubernetes.pod LIMIT 10"
# JSON output for scripting
paraql --format json "SELECT name FROM openstack.instance"
# Start interactive REPL
paraql -iOutput Formats
| Format | Flag | Use Case |
|---|---|---|
table | --format table | Human-readable Rich tables (default) |
json | --format json | Machine-readable JSON array |
csv | --format csv | Spreadsheet import, piping |
value | --format value | Single scalar for shell substitution |
The format is validated: an unsupported value is rejected during argument parsing, before the
CLI resolves any configuration or sends any request. An unsupported output_format in the
config file is warned about and ignored, falling through to the default.
Only the table format carries a footer (row count, plus execution time under --time). The
machine formats emit the body and nothing else, so a redirect captures exactly the data.
Examples:
# Rich table (default)
paraql "SELECT name, ci_type FROM kubernetes.pod LIMIT 5"
# JSON for jq processing
paraql -o json "SELECT name FROM openstack.instance" | jq '.[].name'
# CSV for spreadsheets
paraql -o csv "SELECT name, ci_type FROM *" > export.csv
# Single value for scripts
COUNT=$(paraql -o value "SELECT COUNT(*) FROM kubernetes.pod")
echo "There are $COUNT pods"A CSV export can hit the server's row cap. When it does, the rows written to export.csv are
the truncated set and a warning naming the cap is written to stderr, so watch for it before
treating the file as the full picture. See Truncated results.
Cells that open with a spreadsheet formula trigger (=, +, -, @, or a tab) are prefixed
with an apostrophe in CSV output, so a CI named =cmd() imports as text. Typed numbers pass
through unchanged.
Truncated results
The server caps every query. A query with no LIMIT gets the default cap; a query with its own
LIMIT is capped by it, and by a higher hard ceiling above it. So SELECT ... LIMIT 5 against
six or more matching rows is a truncated result too. When a result is cut at whichever cap
applied, paraql writes a warning to stderr naming it:
warning: result truncated at 1000 rows; use --offset to page (paging requires ORDER BY for stable results)The number in the message is the cap the server applied to that query, not a fixed constant of
the CLI. The rows on stdout are still valid and the exit code is still 0, so a pipeline keeps
working; the warning is advisory. To page through the rest, add an ORDER BY and step
--offset. See
Ordering and Pagination for the clauses
themselves.
Command Reference
Query Execution
# Direct query
paraql query "SELECT name FROM kubernetes.pod"
# Shorthand (no 'query' subcommand needed)
paraql "SELECT name FROM kubernetes.pod"
# From file
paraql query --file report.pql
# From stdin
echo "SHOW TABLES" | paraql queryFlags:
| Flag | Short | Description |
|---|---|---|
--file | -f | Read the query from a file |
--format | -o | Output format: table, json, csv, value |
--limit | -l | Adds a LIMIT when the query has none |
--offset | Skips rows before returning results, when the query has no OFFSET of its own. Paging is stable only when the query has an ORDER BY | |
--explain | Show the query plan. SELECT queries only | |
--dry-run | Validate the query server-side without returning rows, then print OK. SELECT queries only, and a UNION ALL query is rejected by the server. Takes precedence over --explain | |
--time | -t | Add execution time to the footer |
--no-header | Suppress column headers | |
--no-pager | Disable the auto-pager on a TTY |
--timeout and --insecure apply to queries too, but they are group-level: put them before
the subcommand, as described under Flag placement.
--limit and --offset append a clause only when the query does not already have one, so a
query that spells out its own LIMIT keeps it.
Validating without running
--dry-run is implemented with EXPLAIN, which the grammar accepts in front of SELECT only.
SHOW, DESCRIBE, and DIFF are therefore rejected locally, with a message saying so and
exit code 2, before anything reaches the server. A UNION ALL query gets past that local check
(it opens with SELECT) and is rejected by the server instead, for the same reason. A valid
query prints OK on stdout; a broken one prints the server's parse or semantic error on stderr
and exits 1, which makes this usable as a lint step in CI:
paraql query --dry-run --file report.pqlSchema Inspection
# List the CI types
paraql show tables
# Filter by source
paraql show tables --source kubernetes
# Describe a CI type's fields
paraql describe openstack.instance
# List relationship types
paraql show relationships
# Relationships for a specific CI type
paraql show relationships --for kubernetes.pod
# List data sources
paraql show sources
# List infrastructure tiers
paraql show tiersAsk (natural language)
paraql ask translates a plain-English question into ParaQL and offers to run it:
paraql ask "how many pods are running per namespace?"The explanation, any assumptions the model made, and the generated query go to stderr. The prompt then defaults to No, so pressing Enter on a query you have not read does nothing:
Counts pods grouped by namespace, filtered to the Running phase.
Assumption: "running" means config.phase = 'Running'
SELECT namespace, COUNT(*) AS pods FROM kubernetes.pod WHERE config.phase = 'Running' GROUP BY namespace
Execute? [y/N]:--no-confirm skips the prompt. Setting confirm = false under [nl] in the config file has
the same effect for the paraql ask command. It does not reach the REPL: \ask confirms
regardless of that setting.
Under --dry-run the generated query becomes the command's stdout payload and nothing is
executed, so ask composes as a pipeline stage:
paraql ask --dry-run "pods in the prod namespace" | paraql queryYour question is sent to a third-party LLM provider, along with schema and grounding context
drawn from your tenant. paraql ask sends the question on its own; the REPL's \ask also
sends the last query of the session as surrounding context. paraql says so once per profile
before the first request:
note: ask sends your question and recent query context to a third-party LLM provider (see docs: /docs/paraql-natural-language)Having shown it, the CLI records ask_notice_shown in the config file so it does not repeat.
For what the feature does, what leaves your instance, and where it falls short, see
Natural Language Queries.
The REPL exposes the same feature as \ask.
Saved Queries
# List saved queries (yours plus those shared with you)
paraql saved list
# List only your own
paraql saved list --no-shared
# Run a saved query
paraql saved run <query-id>
# Save a new query
paraql saved create --name "Pod count" --query "SELECT COUNT(*) FROM kubernetes.pod"
# With description and sharing
paraql saved create \
--name "Critical CIs" \
--query "SELECT name, criticality_score FROM * WHERE criticality_score > 80" \
--description "High-criticality infrastructure items" \
--shared
# Update a query you own
paraql saved update <query-id> --name "Critical CIs (prod)" --description "Prod only"
# Stop sharing it
paraql saved update <query-id> --no-shared
# Delete a saved query
paraql saved delete <query-id>saved update sends only the options you pass, so any field you omit keeps its stored value,
sharing included. An update with no options is a usage error rather than a round trip.
saved run echoes the stored query text to stderr before executing it. An ID says nothing
about what it does, and a shared query may have been edited by its owner since you last looked.
Interactive REPL
Start the REPL with paraql -i or paraql --interactive. Running paraql with no arguments
in a terminal does the same.
$ paraql -i
ParaQL CLI v0.1.0
Connected to: https://acme.parascope.io
CI types: 214, Sources: 9
Type "\q" to quit, "\?" for help.
paraql> SHOW TABLESThe version line reflects the release you installed, and the counts are whatever your tenant reports; the numbers above are illustrative. If the connection fails, the banner says so and the REPL still starts, so you can fix the endpoint from inside it.
Features
- Tab completion for keywords, CI types, fields, and relationship types. The CI types,
sources, and relationship types come from your tenant at startup, and
config./metrics./raw.fields are fetched withDESCRIBEon first use and cached for the session, so the suggestions match the schema the server will actually accept. - Syntax highlighting for keywords, strings, numbers, CI type references, and traversal arrows.
- Multiline editing. A line that does not finish the query switches to the
->prompt and prints(multiline; end with ; to run, \q to quit)once.SHOWandDESCRIBEdo not need a semicolon. - Persistent history in
~/.paraql/history, written mode 0600 because it is a verbatim transcript of your queries.
Backslash Commands
| Command | Description |
|---|---|
\q, \quit | Exit the REPL |
\format <fmt> | Change output format for the session |
\tables | Run SHOW TABLES |
\describe <type> | Run DESCRIBE for a CI type |
\ask <question> | Translate a plain-English question to ParaQL |
\history | Show recent queries |
\save <name> | Save the last executed query |
\clear | Clear the screen |
\?, \help | Show available commands |
At the primary prompt, bare help and ? also show the help, and bare exit and quit also
leave. Mid-query those are query text, so they are passed through.
\q and \? are dispatched at the continuation prompt too: a half-typed multiline query is
not a trap. \q abandons the buffer and quits; \? prints the help and returns you to the
continuation prompt with the buffer intact.
\ask shows the same one-time data-egress notice as paraql ask, and confirms before running
the generated query with the same No default.
Shell completion
Typer's completion installer registers paraql with your shell:
paraql --install-completionparaql --show-completion prints the script instead of installing it, which is what you want
when your shell config is managed elsewhere.
Automation Examples
# Daily pod count in cron
0 8 * * * paraql -o value "SELECT COUNT(*) FROM kubernetes.pod" >> ~/reports/pod-count.log
# Export instances to CSV
paraql -o csv --no-header "SELECT name, status FROM openstack.instance" > servers.csv
# Check whether critical CIs exist
if [ "$(paraql -o value 'SELECT COUNT(*) FROM * WHERE criticality_score > 90')" -gt 0 ]; then
echo "Critical CIs found"
fi
# Pipe to jq for processing
paraql -o json "SELECT name, ci_type FROM kubernetes.pod" | \
jq -r '.[] | "\(.name) (\(.ci_type))"'Warnings, prompts, and errors go to stderr, and query results go to stdout, so a redirect
captures the data and leaves the diagnostics on your terminal. Exit code 0 means the command
did what you asked. Exit code 1 covers the failures: a rejected query, an unresolvable
configuration, a saved update with no options to update, a declined config init overwrite.
Exit code 2 is reserved for arguments the parser rejects, such as an unsupported --format or
--dry-run on a statement it cannot validate.
paraql --help lists the subcommands, and paraql <subcommand> --help covers each one.