Parascope Docs

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 paraql

Or, to keep it in its own environment:

pipx install paraql

paraql 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.toml

Guided 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 init

It 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 = false

Select 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 show
setting        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 -i

Output Formats

FormatFlagUse Case
table--format tableHuman-readable Rich tables (default)
json--format jsonMachine-readable JSON array
csv--format csvSpreadsheet import, piping
value--format valueSingle 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 query

Flags:

FlagShortDescription
--file-fRead the query from a file
--format-oOutput format: table, json, csv, value
--limit-lAdds a LIMIT when the query has none
--offsetSkips rows before returning results, when the query has no OFFSET of its own. Paging is stable only when the query has an ORDER BY
--explainShow the query plan. SELECT queries only
--dry-runValidate 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-tAdd execution time to the footer
--no-headerSuppress column headers
--no-pagerDisable 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.pql

Schema 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 tiers

Ask (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 query

Your 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 TABLES

The 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 with DESCRIBE on 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. SHOW and DESCRIBE do not need a semicolon.
  • Persistent history in ~/.paraql/history, written mode 0600 because it is a verbatim transcript of your queries.

Backslash Commands

CommandDescription
\q, \quitExit the REPL
\format <fmt>Change output format for the session
\tablesRun SHOW TABLES
\describe <type>Run DESCRIBE for a CI type
\ask <question>Translate a plain-English question to ParaQL
\historyShow recent queries
\save <name>Save the last executed query
\clearClear the screen
\?, \helpShow 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-completion

paraql --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.