Skip to content

Repository files navigation

gqlcli — GraphQL Client CLI & Library

Go Version License Go Report Card

Two tools in one:

  1. gqlcli CLI — A GraphQL client for querying any GraphQL API. Discover fields, execute queries and mutations, explore schemas—all from the command line.

  2. gqlcli library — Build GraphQL-backed CLI applications in Go. Write CLIs where GraphQL is the interface language, not subcommands and flags. Perfect for AI agents that can introspect schemas and construct queries.


🚀 Quick Start — Using the CLI

Installation

# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/wricardo/gqlcli/main/install.sh | bash

# or with Go
go install github.com/wricardo/gqlcli/cmd/gqlcli@latest

See Installation for more options.

Basic Usage

# Discover what queries are available
gqlcli queries

# Find mutations related to "campaign"
gqlcli mutations --filter campaign

# Execute a query
gqlcli query --query "{ users { id name } }"

# Try against a different server
export GRAPHQL_URL=https://api.example.com/graphql
gqlcli queries --filter user

Real Examples

# List all Query fields with descriptions
gqlcli queries --desc

# Show mutation arguments and types
gqlcli mutations --args

# Explore all types
gqlcli types

# Inspect a specific type
gqlcli describe User --args

# Include related operations and schema fields too
gqlcli describe SmsCampaign --depth 1

# Remove reverse-reference caps (default is 5 of each)
gqlcli describe SmsCampaign --depth 1 --max-op-refs 0 --max-field-refs 0
# Truncated sections say "(showing X of N)"

# Describe exactly one operation instead of a substring match
gqlcli describe Query.smsCampaign --args --depth 1

# Execute a mutation with variables
gqlcli mutation \
  --mutation "mutation CreateUser(\$input: CreateUserInput!) { createUser(input: \$input) { id } }" \
  --input '{"name":"Alice","email":"alice@example.com"}'

# Use a query from a file
gqlcli query --query-file ./queries/getUser.graphql --variables '{"id":"123"}'

Different Output Formats

gqlcli queries --filter user -f json-pretty    # Pretty JSON
gqlcli queries --filter user -f table           # Aligned columns
gqlcli queries --filter user -f toon            # Token-optimized (default)
gqlcli queries --filter user -f llm             # Markdown for LLMs
gqlcli queries --filter user -f compact         # Minimal JSON

✨ CLI Features

🎯 Commands

  • query — Execute GraphQL queries with variables, multiple input methods, and built-in --jq filtering
  • mutation — Execute mutations with auto-wrapped input objects and built-in --jq filtering
  • subscribe — Stream GraphQL subscription events over WebSocket (graphql-transport-ws)
  • validate — Check a document against the schema without executing it; exits 1 on error
  • sdl — Print the endpoint's schema as a loadable SDL document (for offline validation)
  • batch — Execute multiple operations in one or sequentially chunked requests (NDJSON or JSON array) with jq filtering
  • script — Run JavaScript workflow scripts with async/await and gql.each() concurrency control
  • op — Save, list, show, and delete named operations in .gqlcli.json
  • types — List all schema types with filtering
  • describe — Print SDL definition of a named type
  • embed — Build a semantic index of the schema and search queries, mutations and types by meaning
  • queries — Discover available Query fields instantly
  • mutations — Discover available Mutation fields instantly

📊 Output Formats

  • json / json-pretty — Pretty or compact JSON
  • table — Aligned columns for terminal viewing
  • toon — Token-optimized format (40-60% smaller) — default
  • llm — Markdown-friendly for AI/LLM consumption
  • compact — Minimal JSON (strips nulls)

🔐 Configuration

  • Default endpoint: http://localhost:8080/graphql
  • Override via --url flag or GRAPHQL_URL environment variable
  • Per-directory config file: .gqlcli.json with named environments (local, prod, qa, …)
  • Switch environments at runtime with --env prod
  • Bearer token authentication support
  • Custom HTTP headers per environment
  • Debug mode for request/response logging
  • Per-request --header/-H overrides for one-off auth, tenant, trace, or preview headers
  • HTTP controls: opt-in --timeout, plus --retry, --retry-delay, --strict (default true), and --insecure
  • On-disk schema cache (10m default): repeated describe/queries/types/validate skip introspection; --refresh-schema to bypass, --schema-cache-ttl 0 to disable; gqlcli schema refresh|status|clear [--all] to manage it
  • Response metadata inspection with --include-headers, --dump-headers, and repeatable --metadata selectors

📝 Input Methods

  • Inline: --query "{ users { id } }"
  • From files: --query-file queries/getUser.graphql or --subscription-file subscriptions/events.graphql
  • As arguments: query "{ ... }" or subscribe "subscription { ... }"
  • Variables inline: --variables '{"id":"123"}'
  • Variables from files: --variables-file vars.json
  • Named operations in multi-operation files
  • Saved named operations in .gqlcli.json via gqlcli op

📚 Complete Usage Examples

Keep GraphQL Workflows Inside gqlcli

Use gqlcli's native features before building shell loops, xargs jobs, external jq pipelines, or raw curl requests:

Need Use
One GraphQL operation query or mutation
Filter, select, or aggregate a response built-in --jq
Many independent operations known up front batch, optionally with --batch-size
Loops, branches, or calls that depend on earlier results script with gql.each
A reusable workflow a named operation or saved script in .gqlcli.json

Shell orchestration is mainly useful when GraphQL work must coordinate with unrelated programs or operating-system tasks. Use curl when diagnosing raw HTTP behavior or when gqlcli does not support the required transport.

Discovering Operations

# List all queries (TOON format — token-efficient)
gqlcli queries

# List with descriptions
gqlcli queries --desc

# Show arguments and types
gqlcli queries --args

# Filter by name
gqlcli queries --filter user
gqlcli mutations --filter campaign

# Expand referenced arg/return types too (one command instead of queries + describe)
gqlcli queries --filter user --args --depth 1

# Different formats
gqlcli queries -f json-pretty
gqlcli mutations -f table

Executing Queries

# Simple query
gqlcli query --query "{ users { id name email } }"

# Query from file
gqlcli query --query-file ./queries/getUser.graphql

# With variables
gqlcli query \
  --query "query GetUser(\$id: ID!) { user(id: \$id) { id name } }" \
  --variables '{"id":"123"}'

# Variables from file
gqlcli query \
  --query-file ./queries/getUser.graphql \
  --variables-file ./variables.json

# Named operation (from multi-operation file)
gqlcli query \
  --query-file ./queries/operations.graphql \
  --operation "GetUser"

# Saved named operation (from .gqlcli.json)
gqlcli query --op get-user --variables '{"id":"123"}'

# Built-in jq filtering — skipped on error so failures stay visible
gqlcli query "{ users { id name } }" --jq '.data.users[].name'

Mutations

# Basic mutation
gqlcli mutation \
  --mutation "mutation { createUser(name: \"Alice\") { id } }"

# With auto-wrapped input
gqlcli mutation \
  --mutation "mutation CreateUser(\$input: CreateUserInput!) { createUser(input: \$input) { id } }" \
  --input '{"name":"Alice","email":"alice@example.com"}'

# Alternative: explicit variables
gqlcli mutation \
  --mutation-file ./mutations/createUser.graphql \
  --variables '{"input":{"name":"Alice"}}'

# Saved named mutation (from .gqlcli.json)
gqlcli mutation --op create-user --input '{"name":"Alice","email":"alice@example.com"}'

Built-in jq Filtering

Use --jq when the filtered value is the desired command output. It avoids an external jq pipeline, does not require switching to JSON output first, and leaves GraphQL or transport errors visible instead of filtering them as if they were successful data.

gqlcli query '{ users { id name active } }' \
  --jq '.data.users[] | select(.active) | {id, name}'

gqlcli query '{ users { id } }' --jq '[.data.users[].id]'
gqlcli mutation 'mutation { refreshCache { updated } }' --jq '.data.refreshCache.updated'

Use external jq when gqlcli output must feed a non-gqlcli program, when processing unrelated files, or when an expression needs a jq feature not supported by the built-in engine.

JavaScript Scripting

Use script when you need loops, conditions, dependent GraphQL calls, or controlled concurrency. It replaces shell loops that repeatedly invoke gqlcli and re-encode intermediate JSON.

# Run a script file (calls run(gql, input))
gqlcli script --file ./scripts/disableUsers.js

# Pass structured input
gqlcli script --file ./scripts/job.js --arg '{"tenantId":"acme"}'
gqlcli script --file ./scripts/job.js --arg-file ./input.json

# Short inline workflow
gqlcli script --source 'async function run(gql) { return gql.query("query { viewer { id } }") }'

# Run saved inline script from .gqlcli.json scripts
gqlcli script --op disable-inactive-users

Example script:

async function run(gql) {
  const res = await gql.query("query { users { id active } }")
  const inactive = res.data.users.filter((u) => !u.active)

  return await gql.each(
    inactive,
    async (user) => {
      await gql.mutation(
        "mutation Disable($id: ID!) { disableUser(id: $id) { ok } }",
        { id: user.id }
      )
    },
    { concurrency: 5, stopOnError: false }
  )
}

Available helpers inside scripts:

  • gql.query(query, variables?, operationName?)
  • gql.mutation(mutation, variables?, operationName?)
  • gql.request({ type, query|mutation, variables, operationName })
  • gql.each(items, worker, { concurrency?, stopOnError?, onError? })

run can be synchronous or async (async function run(gql, input) { ... }).

gql.each() returns a summary object:

  • total — number of items
  • success — successful worker calls
  • failed — failed worker calls
  • errors — array of { index, error }

Use gql.each instead of xargs or background shell jobs. Set concurrency: 1 for sequential calls. JavaScript variables, arrays, loops, and async/await work normally.

For multiline source without a temporary file, use a quoted heredoc on Unix-like systems:

gqlcli script --file /dev/stdin --arg-file ./input.json <<'EOF'
async function run(gql, input) {
  let results = []
  for (const id of input.scorecardIds) {
    const response = await gql.query(
      `query Scorecard($id: ID!) { scorecard(id: $id) { id name } }`,
      { id }
    )
    results.push(response.data.scorecard)
  }
  return results
}
EOF

The CLI has no overall script deadline. GraphQL HTTP calls have no timeout by default; --timeout 60 gives each individual request a 60-second deadline.

Save reusable inline scripts in .gqlcli.json:

gqlcli script save --name disable-inactive-users --source-file ./scripts/disableUsers.js \
  --defaults '{"concurrency":5}' --description 'Disable inactive users'

gqlcli script list
gqlcli script show --name disable-inactive-users
gqlcli script --op disable-inactive-users --arg '{"concurrency":10}'

HTTP Controls

Use transport flags when commands need one-off request customization or CI-friendly failure behavior. These flags work on HTTP-backed commands such as query, mutation, subscribe, batch, script, queries, mutations, types, and describe.

# Per-request headers override headers from the selected .gqlcli.json environment
gqlcli query '{ viewer { id } }' \
  --env prod \
  -H 'Authorization=Bearer temporary-token' \
  -H 'X-Tenant=acme'

# Opt into a request deadline and retry transient failures
gqlcli query '{ health }' --timeout 10 --retry 3 --retry-delay 500ms

# Exits non-zero by default when the GraphQL response includes an errors array (CI-friendly)
gqlcli query --query-file ./checks/schema.graphql

# Opt out of that (e.g. to inspect partial data even when errors are present)
gqlcli query --query-file ./checks/schema.graphql --strict=false

# Internal/self-signed TLS endpoints
gqlcli queries --url https://localhost:8443/graphql --insecure

Response metadata flags are opt-in so normal JSON output stays parseable by default:

# Include status line + headers before the body, like curl -i
gqlcli query '{ viewer { id } }' --include-headers

# Dump headers without changing stdout; filter the response with built-in jq
gqlcli query '{ viewer { id } }' --dump-headers headers.txt --jq '.data'

# Print selected metadata after the response
gqlcli query '{ viewer { id } }' --metadata status-code --metadata header:X-Request-Id

Subscriptions

# Stream subscription events as NDJSON envelopes
gqlcli subscribe 'subscription { messageAdded { id text } }'

# From a file, with variables
gqlcli subscribe \
  --subscription-file ./subscriptions/messages.graphql \
  --variables-file ./variables.json

# Named operation in a multi-operation document
gqlcli subscribe \
  --subscription 'subscription WatchRoom($room: ID!) { messageAdded(room: $room) { id text } }' \
  --variables '{"room":"general"}' \
  --operation WatchRoom

subscribe writes one JSON object per line to stdout:

{"type":"next","payload":{"data":{"messageAdded":{"id":"1","text":"hello"}}}}
{"type":"error","payload":[{"message":"..."}]}
{"type":"complete"}

Transport coverage: subscriptions use the standard GraphQL over WebSocket protocol (graphql-transport-ws). HTTP and HTTPS endpoint URLs are automatically mapped to ws:// and wss://; explicit ws:// or wss:// URLs are also accepted. Server-Sent Events (SSE) is another common subscription transport, but is not implemented yet; it should be added as an explicit --transport sse mode if needed.

Press Ctrl-C to cancel; gqlcli sends a WebSocket complete message and closes the connection cleanly.

Batch Operations

Use batch for independent operations known up front instead of invoking gqlcli in a shell loop. It supports two wire formats:

  • NDJSON (default) — one JSON object per line, Content-Type: application/x-ndjson
  • JSON array — standard batch format, Content-Type: application/json

For NDJSON, put one operation on each line in operations.ndjson:

{"query":"query User($id: ID!) { user(id: $id) { id name } }","variables":{"id":"u_101"}}
{"query":"query User($id: ID!) { user(id: $id) { id name } }","variables":{"id":"u_102"}}
{"query":"query User($id: ID!) { user(id: $id) { id name } }","variables":{"id":"u_103"}}
# Send all operations in one HTTP request
gqlcli batch --file ./operations.ndjson

# Send sequential requests of at most ten operations, preserving output order
gqlcli batch --file ./operations.ndjson --batch-size 10

# Apply one client-side jq expression to every response
gqlcli batch --file ./operations.ndjson --jq '.data'

--batch-size 1 sends one operation per request without a shell loop. --batch-size 0 is the default and sends every operation in one request. Requests have no timeout by default; use --timeout SECONDS when a deadline is required.

Each operation can include a "jq" field for server-side filtering before the response is returned. The expression receives the full GraphQL envelope, so paths usually start at .data.

jq examples for the "jq" field:

Expression Effect
.data Strip the GraphQL envelope
.data.users[].name Extract a field from each array element
.data.users | length Count results
.data.users[] | select(.active) Filter array items
.data.logs[] | select(.message | test("error")) Regex match
.data | {count: (.users | length), first: .users[0]} Transform/reshape

Use script when later calls depend on IDs or other values returned by earlier calls. That keeps the full workflow inside gqlcli instead of assembling a query-to-jq-to-batch shell pipeline.

Schema Exploration

# List all types
gqlcli types

# Filter types by name
gqlcli types --filter User

# Filter by kind
gqlcli types --kind OBJECT
gqlcli types --kind ENUM
gqlcli types --kind INPUT_OBJECT

# Include field argument signatures and doc strings
gqlcli types --filter User --args --desc

# Compact output (good for piping)
gqlcli types -f compact

Environment Configuration

Option 1 — Environment variable

export GRAPHQL_URL="http://staging-api.example.com/graphql"
gqlcli queries

Option 2 — .gqlcli.json (per-directory config)

Create .gqlcli.json in your project directory to define named environments:

{
  "default": "local",
  "environments": {
    "local": {
      "url": "http://localhost:8080/graphql",
      "headers": {
        "Authorization": "Bearer dev-token"
      }
    },
    "staging": {
      "url": "http://staging-api.example.com/graphql",
      "headers": {
        "Authorization": "Bearer staging-token",
        "X-Tenant": "acme"
      }
    },
    "prod": {
      "url": "https://api.example.com/graphql",
      "headers": {
        "Authorization": "Bearer prod-token"
      }
    }
  }
}
# Uses "local" (the default)
gqlcli queries

# Switch to prod
gqlcli queries --env prod
gqlcli query --query "{ users { id } }" --env prod

# Override URL on top of a named env
gqlcli queries --env staging --url http://other-host/graphql

Priority (lowest → highest): hardcoded default → .gqlcli.json env → GRAPHQL_URL → --url flag

Saved Named Operations

Save frequently used queries and mutations in .gqlcli.json, then execute them by name with --op.

# Save a query with default variables
gqlcli op save \
  --name get-user \
  --query 'query GetUser($id: ID!) { user(id: $id) { id name email } }' \
  --defaults '{"id":"123"}'

# Run it; explicit variables override saved defaults
gqlcli query --op get-user --variables '{"id":"456"}'

# Save and run a mutation
gqlcli op save \
  --name create-user \
  --mutation 'mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } }'

gqlcli mutation --op create-user --input '{"name":"Alice","email":"alice@example.com"}'

# Save and run a subscription
gqlcli op save \
  --name watch-messages \
  --subscription 'subscription { messageAdded { id text } }'

gqlcli subscribe --op watch-messages

# Manage saved operations
gqlcli op list
gqlcli op show --name get-user
gqlcli op delete --name get-user

Saved operations are stored under the operations key:

{
  "operations": {
    "get-user": {
      "type": "query",
      "query": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
      "defaults": { "id": "123" }
    }
  }
}

Subscriptions are stored with "type": "subscription", reusing the query field for the operation text (there is no separate subscription field).

Use --operation when selecting a GraphQL operation from a multi-operation document. Use --op when running a saved operation from .gqlcli.json.

Saved Inline Scripts

Store reusable JavaScript workflows under the scripts key in .gqlcli.json. Unlike operations, scripts are stored inline (not as file paths), so they travel with project config.

{
  "scripts": {
    "disable-inactive-users": {
      "lang": "javascript",
      "function": "run",
      "source": "async function run(gql, input) { /* ... */ }",
      "defaults": { "concurrency": 5 },
      "description": "Disable inactive users"
    }
  }
}

Run with:

gqlcli script --op disable-inactive-users
gqlcli script --op disable-inactive-users --arg '{"concurrency":10}'

Advanced: Save Results to File

# Query result to file
gqlcli query --query "{ users { id } }" --output results.json

# Types list to file
gqlcli types --output types.json

🔧 Command Reference

Global Flags

-u, --url VALUE       GraphQL endpoint (default: http://localhost:8080/graphql, env: GRAPHQL_URL)
--env VALUE           Environment to use from .gqlcli.json (e.g. local, prod)
-f, --format VALUE    Output format: json, json-pretty, table, compact, toon, llm (default: toon)
-p, --pretty          Pretty print JSON output
-h, --help            Show help

query Command

-q, --query STRING           GraphQL query
--query-file PATH            Read query from file
-v, --variables JSON         Query variables as JSON
--variables-file PATH        Read variables from file
-o, --operation STRING       Named operation to execute from a multi-operation document
--op NAME                    Saved operation from .gqlcli.json
-f, --format FORMAT          Output format
--output FILE                Write to file
-H, --header KEY=VALUE       Per-request HTTP header (repeatable; overrides env headers)
--include-headers, -i        Include response status line and headers before body
--dump-headers FILE          Write response status line and headers to file
--metadata SELECTOR          Print selected metadata (status, status-code, headers, header:Name)
--timeout SECONDS            Request timeout; 0 disables the timeout (default)
--retry N                    Retry transient failures
--retry-delay DURATION       Delay between retries (e.g. 500ms, 2s)
--strict     Exit non-zero when response.errors is present (default: true; use --strict=false to disable)
--insecure                   Skip TLS certificate verification
--schema-cache-ttl DURATION  Reuse the on-disk introspection cache this long (default 10m; 0 disables; env: GQLCLI_SCHEMA_CACHE_TTL)
--refresh-schema             Ignore the cached schema and introspect again
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable HTTP debug logging

mutation Command

-m, --mutation STRING        GraphQL mutation
--mutation-file PATH         Read mutation from file
--input JSON                 Input object (auto-wrapped as {"input":{...}})
-v, --variables JSON         Variables as JSON
--variables-file PATH        Read variables from file
-o, --operation STRING       Named operation to execute from a multi-operation document
--op NAME                    Saved operation from .gqlcli.json
-f, --format FORMAT          Output format
--output FILE                Write to file
-H, --header KEY=VALUE       Per-request HTTP header (repeatable; overrides env headers)
--include-headers, -i        Include response status line and headers before body
--dump-headers FILE          Write response status line and headers to file
--metadata SELECTOR          Print selected metadata (status, status-code, headers, header:Name)
--timeout SECONDS            Request timeout; 0 disables the timeout (default)
--retry N                    Retry transient failures
--retry-delay DURATION       Delay between retries (e.g. 500ms, 2s)
--strict     Exit non-zero when response.errors is present (default: true; use --strict=false to disable)
--insecure                   Skip TLS certificate verification
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable HTTP debug logging

subscribe Command

-s, --subscription STRING    GraphQL subscription
--subscription-file PATH     Read subscription from file
-v, --variables JSON         Variables as JSON
--variables-file PATH        Read variables from file
-o, --operation STRING       Named operation to execute from a multi-operation document
--op NAME                    Saved subscription from .gqlcli.json (type: "subscription")
-H, --header KEY=VALUE       Per-request HTTP/WebSocket header (repeatable)
--timeout SECONDS            Connection/read timeout; 0 disables the timeout (default)
--insecure                   Skip TLS certificate verification for wss:// endpoints
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL); http(s) maps to ws(s)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable debug logging

Output is NDJSON subscription envelopes: next, error, and complete.

validate Command

-q, --query STRING           Document to validate
--query-file PATH            Read the document from a file
-m, --mutation STRING        Mutation document to validate
--mutation-file PATH         Read the mutation from a file
--schema-file PATH           Validate against SDL on disk instead of introspecting
-f, --format STRING          Render the verdict as json/toon/table/compact/llm instead of text
--jq EXPR                    Filter the verdict shape

Checks a document against the schema and reports each problem as line:column: message, followed by compact SDL for the type the message refers to. Machine-readable output puts that hint at errors[].extensions.schemaHint — the same path the executed path uses — so one jq expression works against both. The hint is rendered from the already-parsed schema, so it costs no extra requests and works with --schema-file and in inline mode. The operation is never sent: only the schema is fetched, so no resolver runs and no data changes. Exits 0 when the document is valid and 1 when it is not, so it can gate a build. query, mutation and subscribe accept --validate-only for the same check.

Variable values are not part of the document and are not checked — a document declaring required variables validates on its own.

gqlcli validate '{ users { id name } }'
gqlcli query --validate-only '{ users { nope } }'

# validate offline, with no network and no credentials
gqlcli sdl > schema.graphql
gqlcli validate --schema-file schema.graphql '{ users { id } }'

sdl Command

-o, --output PATH            Write the SDL to a file instead of stdout

Introspects the endpoint and prints its whole schema as SDL that any GraphQL parser can load back. Builtin scalars and directives are omitted, since every parser supplies them. Use describe instead to read a few types in a compact form.

op Command

gqlcli op save --name NAME (--query QUERY | --mutation MUTATION | --subscription SUBSCRIPTION) [--defaults JSON]
gqlcli op list
gqlcli op show --name NAME
gqlcli op delete --name NAME

Saved operations live in .gqlcli.json and run with gqlcli query --op NAME, gqlcli mutation --op NAME, or gqlcli subscribe --op NAME.

script Command

gqlcli script --file PATH [--function NAME] [--arg JSON | --arg-file PATH]
gqlcli script --source 'async function run(gql){...}'
gqlcli script --op NAME

gqlcli script save --name NAME (--source JS | --source-file PATH) [--function NAME] [--defaults JSON] [--description TEXT]
gqlcli script list
gqlcli script show --name NAME
gqlcli script delete --name NAME

Saved scripts live inline under .gqlcli.json scripts and run with gqlcli script --op NAME.

batch Command

--ndjson                     Use NDJSON transport (default)
--array                      Use JSON array batch transport
--batch-size N               Maximum operations per HTTP request; 0 sends all operations in one request (default)
--file PATH                  Read operations from file instead of stdin
--jq EXPR                    Apply jq expression to each response (client-side)
-H, --header KEY=VALUE       Per-request HTTP header (repeatable)
--timeout SECONDS            Request timeout; 0 disables the timeout (default)
--retry N                    Retry transient failures
--retry-delay DURATION       Delay between retries
--strict     Exit non-zero when any response.errors is present (default: true; use --strict=false to disable)
--insecure                   Skip TLS certificate verification
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable HTTP debug logging

queries Command

--desc                       Include field descriptions
--args                       Include field arguments with types
--filter PATTERN             Filter by name (case-insensitive)
-f, --format FORMAT          Output format (default: toon)
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable debug logging

mutations Command

--desc                       Include field descriptions
--args                       Include field arguments with types
--filter PATTERN             Filter by name (case-insensitive)
-f, --format FORMAT          Output format (default: toon)
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable debug logging

describe Command

TYPE_NAME | TYPE_NAME.FIELD_NAME   Name of the type to describe, or Type.field
                              (e.g. Query.smsCampaign) to describe exactly one
                              operation instead of the whole Query/Mutation type
--args, -a                   Expand field argument signatures
--desc                        Include field/type descriptions
--depth N                    Recursively include referenced non-scalar types; when N >= 1 also append top-level Query/Mutation fields and non-root schema fields that reference the requested type within that depth
--max-op-refs N              Max top-level operation references to append (default: 5, 0 = unlimited)
--max-field-refs N           Max referencing schema types to append in the fields section (default: 5, 0 = unlimited)
                              Truncated reverse-reference headers show "(showing X of N)"
                              Top-level Query/Mutation refs are ranked: arg matches first, then shallower return matches
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable debug logging

types Command

--filter PATTERN             Filter by name (substring match)
--kind KIND                  Filter by kind (OBJECT, ENUM, INPUT_OBJECT, SCALAR, INTERFACE, UNION)
-f, --format FORMAT          Output format (default: compact)
-u, --url URL                GraphQL endpoint (env: GRAPHQL_URL)
--env VALUE                  Environment from .gqlcli.json
-d, --debug                  Enable debug logging

🔎 Semantic Search (embed)

describe and types --filter need the name. embed finds things by meaning, for when you know what you want but not what it is called.

Matches come back in three categories, each ranked and returned separately:

Category What it holds
queries Fields of the Query root — the schema's read entry points
mutations Fields of the Mutation root — the write entry points
types Every other type (OBJECT, INPUT_OBJECT, ENUM, INTERFACE, UNION, SCALAR)

The split matters: "pause an sms conversation" should surface the pauseConversation mutation, not just types that mention conversations. Asking for an operation and asking for a data shape are different questions, so they get separate candidate pools.

# Build the index once (one embedding call per entry, then cached by content hash)
gqlcli embed index

# Ask in plain language — 5 queries, 5 mutations and 5 types by default
gqlcli embed search 'pause an sms conversation for a lead'

# One category at a time
gqlcli embed search --category mutations 'create a campaign'
gqlcli embed search -c queries --no-sdl 'scorecard results for an agent'
gqlcli embed search -c operations --top 10 'send a one off sms'   # queries + mutations

# Narrow the types category by kind, keep only strong matches
gqlcli embed search -c types --kind INPUT_OBJECT --min-score 0.5 'campaign settings'

Each hit prints its cosine similarity and its SDL — a type definition, or an operation's call signature (pauseConversation(subscriptionId: ID!, reason: String): ConversationPauseResult!) — ready to paste into a query or a prompt. --top N applies per category. --format json|table|compact|toon switches output; --no-sdl prints names, kinds and scores only.

The index file. embed index writes .gqlcli-embeddings.json, or .gqlcli-embeddings.<env>.json when an environment is selected — one file per environment, since vectors from one schema say nothing about another. Override the path with -o/-i, or pin it per environment with an "embeddings" key in .gqlcli.json:

{
  "default": "local",
  "environments": {
    "prod": { "url": "https://api.example.com/graphql", "embeddings": "schema/prod-embeddings.json" }
  }
}

Re-running embed index re-embeds only entries whose text changed (compared by SHA-256 hash), so keeping the index in git is cheap. --force re-embeds everything; --no-queries / --no-mutations skip a category entirely.

Embedding provider. Embeddings come from the Venu API (POST {base}/embeddings, X-API-Key). Set VENU_API_KEY; VENU_URL and VENU_EMBEDDING_MODEL (or --embedding-url, --embedding-key, --embedding-model) change host and model. The default is nomic-embed-text-v1.5, for which the required search_document: / search_query: task prefixes are applied automatically.

Only names, descriptions and SDL are embedded — entries with no descriptions and generic names have little for the model to work with, so expect weaker matches there.

From Go

embedder, err := gqlcli.NewVenuEmbedder(gqlcli.WithEmbeddingAPIKey(os.Getenv("VENU_API_KEY")))
if err != nil {
	log.Fatal(err)
}

// Build and persist an index.
ix, err := gqlcli.BuildEmbeddingIndexFromClient(ctx, client, embedder, gqlcli.EmbeddingIndexOptions{
	Kinds:    []string{"OBJECT", "INPUT_OBJECT"},   // applies to the types category only
	Exclude:  []string{"*Connection", "*Edge"},
	ShowArgs: true,
})
if err != nil {
	log.Fatal(err)
}
if err := ix.Save(gqlcli.EmbeddingIndexPath("prod")); err != nil {
	log.Fatal(err)
}

// Later: load and find the top 5 per category for a description.
ix, err = gqlcli.LoadEmbeddingIndex(gqlcli.EmbeddingIndexPath("prod"))
if err != nil {
	log.Fatal(err)
}
ix.SetEmbedder(embedder)

results, err := ix.Search(ctx, "pause an sms conversation", 5)
if err != nil {
	log.Fatal(err)
}
for _, m := range results.Mutations {   // also results.Queries, results.Types
	fmt.Printf("%-30s %-24s %.4f\n%s\n", m.Name, m.ReturnType, m.Score, m.SDL)
}
API Purpose
Embedder Embed(ctx, text) ([]float32, error) + Model() string — implement for another provider
QueryEmbedder Optional EmbedQuery for models that embed queries differently from documents
NewVenuEmbedder(opts...) Venu-backed embedder; options for URL, key, model, task prefixes, HTTP client
BuildEmbeddingIndex / BuildEmbeddingIndexFromClient Build an index from an introspection response or a Client
EmbeddingIndex.Search(ctx, text, topN) Embed once, return *SearchResults with topN per category
EmbeddingIndex.SearchVector(vec, topN) Rank against a vector you already have — no network call
SearchResults Queries / Mutations ([]OperationMatch) and Types ([]TypeMatch)
LoadEmbeddingIndex / Save / EmbeddingIndexPath Persistence and the per-environment default path

Search refuses to run when the index's model differs from the embedder's — scores across vector spaces are meaningless. Both CLI modes (HTTP and inline) expose the same embed index and embed search commands.


📚 Using as a Library

The gqlcli package provides two ways to build CLI tools:

Mode Use Case
HTTP Mode Build a CLI that queries external GraphQL APIs over HTTP
Inline Mode Build a GraphQL-backed CLI with inline execution (using gqlgen) — perfect for AI agents and schema-driven CLIs

See sections below for detailed examples of each mode.


📦 Installation

Download a Binary (no Go required)

macOS / Linux — one-liner (auto-detects OS and architecture):

curl -fsSL https://raw.githubusercontent.com/wricardo/gqlcli/main/install.sh | bash

Installs to /usr/local/bin/gqlcli. Override with INSTALL_DIR:

INSTALL_DIR=~/.local/bin curl -fsSL https://raw.githubusercontent.com/wricardo/gqlcli/main/install.sh | bash

Windows: Download gqlcli_windows_amd64.zip from the Releases page and extract gqlcli.exe to a directory in your PATH.

Install with Go

go install github.com/wricardo/gqlcli/cmd/gqlcli@latest

Build from Source

git clone https://github.com/wricardo/gqlcli.git
cd gqlcli
make install
gqlcli --help

As a Go Library

go get github.com/wricardo/gqlcli

HTTP Mode — Query External GraphQL APIs

Build a CLI that connects to external GraphQL servers over HTTP. Useful for API testing, schema exploration, and CI/CD pipelines:

package main

import (
	"os"
	"log"
	"github.com/urfave/cli/v2"
	"github.com/wricardo/gqlcli/pkg"
)

func main() {
	cfg := &gqlcli.Config{
		URL:     "http://localhost:8080/graphql",
		Format:  "toon",
	}

	builder := gqlcli.NewCLIBuilder(cfg)
	app := &cli.App{
		Name: "gql",
		Usage: "GraphQL CLI",
	}

	builder.RegisterCommands(app)
	// Use gqlcli.RunApp, not app.Run directly — it reorders flags placed
	// after a command's positional argument so they aren't dropped.
	if err := gqlcli.RunApp(app, os.Args); err != nil {
		log.Fatal(err)
	}
}

Embedding ScriptRunner — running JavaScript inside a host program

ScriptRunner executes the same JavaScript workflows as the script command, but from inside your own Go program. This matters when the script text is not written by a human — for example when an AI agent authors it and your program runs it.

client := gqlcli.NewHTTPClient(&gqlcli.Config{URL: endpoint})

var logs bytes.Buffer
runner := gqlcli.NewScriptRunner(client,
	gqlcli.WithStdout(&logs),            // keep console.log out of your stdout
	gqlcli.WithStderr(&logs),
	gqlcli.WithTimeout(30*time.Second),  // a script that never returns cannot hang you
	gqlcli.WithReadOnly(true),           // reject every mutation
	gqlcli.WithMaxOperations(200),       // cap a runaway gql.each
	gqlcli.WithOnRequest(func(info gqlcli.RequestInfo) {
		log.Printf("%s %s", info.Type, info.OperationName)
	}),
)

result, err := runner.RunSource(ctx, "agent.js", source, "run", input)
if errors.Is(err, gqlcli.ErrScriptInterrupted) {
	// cancelled or timed out, as opposed to a script bug
}

NewScriptRunner(client) with no options behaves exactly as the CLI does: console output goes to the process stdio and no policy is applied.

Option Purpose
WithStdout(w) / WithStderr(w) Redirect console.log / console.error. Required if your program reserves its own stdout for structured output.
WithTimeout(d) Bound a single run. Also interrupts the JavaScript VM, so an unbounded loop cannot hang the caller.
WithMaxOperations(n) Cap how many GraphQL operations one run may attempt. Blocked operations count against the budget.
WithReadOnly(true) Reject every mutation, including those issued via gql.request.
WithApprover(fn) Gate each operation individually; returning an error aborts just that one.
WithOnRequest(fn) Observe every attempted operation, before any policy check.
WithOnResponse(fn) Observe each operation's outcome — result or error.

Cancellation. The context passed to RunSource / RunFile interrupts the JavaScript runtime itself, not just in-flight HTTP calls, so while (true) {} is recoverable. Errors caused by cancellation wrap ErrScriptInterrupted, which distinguishes them from a script that threw.

Policy. WithReadOnly and WithApprover are enforced per dispatched operation, and the operation's kind comes from parsing the document — not from which helper the script called, and not from scanning the text for the word mutation. So all of these are blocked under WithReadOnly:

gql.mutation("mutation { deleteUser(id: 1) { ok } }")
gql.request({ type: "mutation", mutation: "mutation { ... }" })
gql.query("mutation Evil { deleteEverything { ok } }")        // the helper lies; the document does not

…while a query merely mentioning the word is not blocked:

gql.query('query { search(term: "run the mutation now") { id } }')

A document that cannot be parsed is refused rather than sent whenever a policy is in force, since an unclassifiable operation is the one thing a read-only policy must not wave through. With no policy configured, the server stays the authority, as before.

RequireOperationKind(document, kind) and DocumentOperationKind(document) are exported so a host can apply the same check to documents it dispatches itself.

Rejections surface in JavaScript as catchable throws, so a script can handle a declined mutation instead of dying:

try { gql.mutation("mutation { deleteUser(id: 1) { ok } }") }
catch (e) { console.log("declined:", String(e)) }

Callbacks receive a copy of RequestInfo.Variables, so a hook cannot alter what is sent, and they run on the single goroutine driving the JavaScript runtime — gql.each interleaves its workers inside that one runtime, so callbacks need no locking of their own.

Your own transport. NewScriptRunner takes OperationExecutor, which is just Execute and ExecuteMutation — the two methods it actually calls. Adapting an in-house HTTP client does not require stubbing out schema introspection or response metadata:

type myExecutor struct{ c *house.Client }

func (m myExecutor) Execute(ctx context.Context, _ gqlcli.ExecutionMode, o gqlcli.QueryOptions) (map[string]any, error) {
	return m.c.Do(ctx, o.Query, o.Variables)
}
func (m myExecutor) ExecuteMutation(ctx context.Context, _ gqlcli.ExecutionMode, o gqlcli.MutationOptions) (map[string]any, error) {
	return m.c.Do(ctx, o.Mutation, o.Variables)
}

Reusing saved scripts. ProjectConfig.ResolveScript(name) looks up a script from the scripts section of .gqlcli.json and (*NamedScript).MergeInput layers caller input over its defaults, so an embedder does not have to reimplement that:

cfg, _ := gqlcli.LoadProjectConfig()
script, err := cfg.ResolveScript("disable-inactive-users")
result, err := runner.RunSource(ctx, "disable-inactive-users", script.Source, script.Function,
	script.MergeInput(map[string]interface{}{"concurrency": 10}))

Schema discovery from your own transport

Describer renders a type as compact SDL, caches each introspection result, and can filter fields or expand referenced types. It is the piece to reach for when a model has to read a schema before writing a query against it.

NewDescriberFromExecFunc builds one from any function that can run an operation, so an embedder on its own HTTP stack gets the caching and filtering rather than reimplementing introspection around FormatTypeSDL:

d := gqlcli.NewDescriberFromExecFunc(house.DoRaw) // func(ctx, query, vars) (json.RawMessage, error)

// The operations matching a keyword, with their call signatures.
sdl, err := d.DescribeWithOptions(ctx, "Query", gqlcli.DescribeOptions{
	FieldFilter: "campaign",
	ShowArgs:    true,
})

// What an input type requires.
sdl, err = d.DescribeWithOptions(ctx, "CreateSmsProviderInput", gqlcli.DescribeOptions{})

The exec func must return the full GraphQL response envelope (the object with the data key), not just the data payload.

DescribeOptions.FieldFilter matches across fields, input fields and enum values, so it works on object types, input objects and enums alike, and returns "" when nothing matches — which lets a caller tell "no such field" apart from "type has none". Depth follows only the surviving fields.

DescribeWithFieldFilter is a different, narrower thing: it backs schema-hint error enrichment, so it fixes its own formatting, prepends a # Closest matches header and looks at fields only. Use DescribeWithOptions for general filtering.

Running scripts in-process

NewInlineClient adapts an InlineExecutor to Client, so a gqlgen app can run the same scripts against its own schema with no HTTP hop and no server:

exec := gqlcli.NewInlineExecutor(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))
client := gqlcli.NewInlineClient(exec)

runner := gqlcli.NewScriptRunner(client, gqlcli.WithReadOnly(true))
result, err := runner.RunSource(ctx, "agent.js", source, "run", input)

d := client.Describer() // schema discovery against the same in-process schema

The ExecutionMode argument is ignored by InlineClient — it selects between transports, and an inline client is already one. LastResponseMetadata returns nil, since an in-process call has no HTTP response.

Validating documents without executing them

SchemaValidator checks a document against a schema and never executes it. Building one costs a single introspection round trip, so build it once and reuse it; it holds no mutable state and is safe to share between goroutines.

// All you have is the endpoint's URL:
v, err := gqlcli.NewSchemaValidatorFromURL(ctx, "https://api.example.com/graphql")

// ...or the endpoint needs auth, a timeout, custom headers:
v, err := gqlcli.NewSchemaValidatorFromConfig(ctx, &gqlcli.Config{
    URL:   "https://api.example.com/graphql",
    Token: os.Getenv("API_TOKEN"),
})

result := v.Validate(`{ users { nope } }`)
if !result.Valid {
    for _, e := range result.Errors {
        fmt.Printf("%d:%d: %s (%s)\n", e.Line, e.Column, e.Message, e.Rule)
        if e.SchemaHint != "" {
            fmt.Println(e.SchemaHint) // compact SDL for the type the message names
        }
    }
}

SchemaHint is rendered from the parsed schema the validator already holds, so it costs no extra requests — unlike the executed path's hint, which introspects the type per error. result.Map() places it at errors[].extensions.schemaHint, matching the executed path's shape.

Other constructors, for when you already have the schema in some form:

Constructor Source
NewSchemaValidator(*ast.Schema) An already-parsed schema — pass InlineExecutor.Schema() to skip introspection entirely
NewSchemaValidatorFromSDL(sdl) SDL text: a .graphql file, or a saved SDL() result
NewSchemaValidatorFromIntrospection(map) An introspection result you fetched yourself
NewSchemaValidatorFromClient(ctx, Client) Any Client — works over HTTP and inline alike

SDL() returns the SDL the validator was built from, so a program can introspect once, persist the result, and rebuild later with no network access:

online, _ := gqlcli.NewSchemaValidatorFromURL(ctx, endpoint)
os.WriteFile("schema.graphql", []byte(online.SDL()), 0644)

// later, offline:
sdl, _ := os.ReadFile("schema.graphql")
offline, _ := gqlcli.NewSchemaValidatorFromSDL(string(sdl))

The introspection-to-SDL conversion is available on its own as SchemaSDL(introspection) and SchemaFromIntrospection(introspection).

Variable values are not part of the document and are not checked: a document declaring required variables validates on its own.

Bounding captured output

LimitWriter caps what a script can write, which is what actually bounds memory — trimming the buffer afterwards happens only once the whole run's output already exists:

logs := &bytes.Buffer{}
capped := gqlcli.LimitWriter(logs, 4096)
runner := gqlcli.NewScriptRunner(client, gqlcli.WithStdout(capped), gqlcli.WithStderr(capped))
// ...
if capped.Truncated() { /* note it in the trace */ }

Concurrency

HTTPClient, InlineClient and ScriptRunner are safe to share across goroutines, and each RunSource gets its own JavaScript runtime. Two caveats:

  • LastResponseMetadata() reports the most recent response across all callers, so it only carries a well-defined meaning when operations are issued serially.
  • The single-goroutine guarantee for WithOnRequest/WithOnResponse/WithApprover is per run. Concurrent RunSource calls on one runner invoke your callbacks from several goroutines, so a callback accumulating shared state across runs needs its own lock.

Inline Mode — GraphQL-Backed CLI Applications

Build GraphQL-native CLI applications where GraphQL is the interface language, not subcommands and flags. This is especially powerful for AI agents that can introspect schemas and construct queries dynamically.

Why GraphQL for CLIs:

Traditional CLI GraphQL-Native CLI
myapp --user-type=active --limit 10 --format json myapp query '{ users(type: "active", limit: 10) { id name } }'
Multiple commands for different operations One unified query language
AI must learn your CLI's custom flags AI naturally understands GraphQL
Hard to combine operations Execute multiple queries in parallel
Schema is implicit Schema is explicit and queryable

If you have a gqlgen schema, you can run operations in-process without an HTTP server. This is useful for building a CLI that ships alongside your application binary.

package main

import (
	"log"
	"os"

	"github.com/urfave/cli/v2"
	gqlcli "github.com/wricardo/gqlcli/pkg"

	"github.com/myorg/myapp/graph" // your gqlgen package
)

func main() {
	// 1. Create your gqlgen ExecutableSchema.
	r := graph.NewResolver()
	execSchema := graph.NewExecutableSchema(graph.Config{Resolvers: r})

	// 2. Inline executor — runs operations directly in-process.
	//    WithSchemaHints attaches compact type SDL to validation errors.
	exec := gqlcli.NewInlineExecutor(execSchema,
		gqlcli.WithSchemaHints(),
	)

	// 3. Command set — adds query, mutation, describe, types commands.
	commands := gqlcli.NewInlineCommandSet(exec)

	// 4. Mount onto any urfave/cli app.
	app := &cli.App{Name: "myapp", Usage: "CLI for my GraphQL API"}
	commands.Mount(app)

	// Use gqlcli.RunApp, not app.Run directly — it reorders flags placed
	// after a command's positional argument so they aren't dropped.
	if err := gqlcli.RunApp(app, os.Args); err != nil {
		log.Fatal(err)
	}
}

This adds the following subcommands:

Command Description
query Execute a query (TOON format by default)
mutation Execute a mutation (JSON format by default)
batch Execute multiple operations from stdin (NDJSON) with jq filtering
op Manage saved named operations in .gqlcli.json
describe TYPE Print SDL definition of a type
types List all types in the schema

Schema hints — when WithSchemaHints() is enabled, validation errors include a compact SDL description of the referenced type:

Error: Cannot query field "titl" on type "Book".
Schema hint:
type Book {
  id: ID!
  title: String!
  author: Author!
}

describe Command (Inline-Only)

Available only in inline execution mode. Print the SDL definition of a type:

# Describe a type
./myapp describe Query
./myapp describe Book
./myapp describe AddBookInput

# Output shows field signatures and relationships
type Book {
  id: ID!
  title: String!
  author: Author!
}

Useful for AI agents to discover schema structure before constructing queries.


Complete Example

See example/README.md for a complete working example of a GraphQL-native CLI — no subcommands, no flags, just GraphQL queries and mutations. The example demonstrates:

  • GraphQL as the interface — Execute queries like ./myapp query '{ books { id title author { name } } }'
  • Schema introspection — AI agents can discover capabilities with ./myapp describe Book
  • Parallel execution — Multiple top-level queries in one command
  • Inline execution — No HTTP server needed, runs in-process against a gqlgen schema
  • File-based persistence — Data stored in store.json
  • Forced resolvers — Using @goField(forceResolver: true) for lazy-loading
  • Split schema files — Organized with follow-schema layout

This is the ideal paradigm for:

  • AI agents — Introspect schema, construct queries, explore data
  • CLI automation — Write complex queries instead of chaining commands
  • Consistent interfaces — GraphQL works everywhere, agents already understand it

See example/README.md for detailed setup and usage.


🏗️ Architecture

Core Components

Component Purpose
Config Configuration holder (URL, format, timeout)
CLIBuilder HTTP-based CLI command generator
InlineExecutor In-process executor for gqlgen schemas
InlineCommandSet CLI commands backed by an InlineExecutor
TokenStore JWT persistence at ~/.{appName}/token
Describer Introspects a schema and returns SDL for a type
Formatter Output format converter (JSON, table, TOON, etc.)
FormatterRegistry Manages available formatters

Package Structure

pkg/
├── cli.go              # HTTP-based CLI command builders (CLIBuilder)
├── client.go           # HTTP GraphQL client
├── batch.go            # Batch/NDJSON execution + jq filtering
├── inline.go           # InlineExecutor — in-process execution
├── inline_commands.go  # InlineCommandSet — query/mutation/describe/login commands
├── projectconfig.go    # .gqlcli.json loader and environment resolution
├── token.go            # TokenStore — JWT persistence and parsing
├── describe.go         # Describer — schema introspection and SDL formatting
├── formatter.go        # Output formatters
└── types.go            # Type definitions and interfaces

🔌 Extending the Library

Add a Custom Formatter

package main

import "github.com/wricardo/gqlcli/pkg"

type CSVFormatter struct{}

func (f *CSVFormatter) Format(data map[string]interface{}) (string, error) {
	// Your CSV formatting logic
	return csvOutput, nil
}

func (f *CSVFormatter) Name() string {
	return "csv"
}

// Usage:
registry := gqlcli.NewFormatterRegistry()
registry.Register("csv", &CSVFormatter{})

Custom Client Implementation

type CachedClient struct {
	cache map[string]interface{}
}

func (c *CachedClient) Execute(ctx context.Context, mode gqlcli.ExecutionMode, opts gqlcli.QueryOptions) (map[string]interface{}, error) {
	// Check cache first
	// Fall back to HTTP if not found
	return result, nil
}

📊 Use Cases

API Development & Testing

# Discover available operations
gqlcli queries
gqlcli mutations

# Test a mutation
gqlcli mutation \
  --mutation-file ./test/mutations/createUser.graphql \
  --variables-file ./test/variables.json

Schema Documentation

# Export all types as JSON
gqlcli types --format json-pretty > types.json

# Describe specific types
gqlcli describe User --args --desc
gqlcli describe CreateUserInput --args

CI/CD Pipelines

# Capture types for schema drift detection
gqlcli types --format json > current-types.json
git diff previous-types.json current-types.json

AI/LLM Integration

# Discover operations for LLM context
gqlcli queries --desc --format toon
gqlcli mutations --desc --args --format toon

# Inspect a type before writing a query
gqlcli describe User --args
gqlcli types --kind INPUT_OBJECT

🧪 Testing

# Run all tests
make test

# Test with coverage
make test-coverage

# Run linter
make lint

# Format code
make fmt

⚙️ Development

# Build
make build

# Build and test
make dev

# Install locally
make install

# Clean artifacts
make clean

# View all available commands
make help

🔒 Error Handling

Rich error messages with context:

🚨 GraphQL Validation/Execution Errors:

  ❌ 1. Cannot query field "unknown" on type "Query"
     📂 Path: unknown
     🏷️  Code: GRAPHQL_VALIDATION_FAILED
     📍 Position: Line 1, Column 3

📝 Query that caused the error:
   1 | { unknown }

🌟 Why gqlcli?

  • Zero Dependencies — Single binary, no runtime dependencies
  • Production-Ready — Extensively tested and battle-hardened
  • Token-Efficient — TOON format reduces tokens by 40-60%
  • Extensible — Clean interfaces for custom formatters and clients
  • Flexible Input — Multiple ways to specify queries and variables
  • DevOps Friendly — Perfect for scripts, CI/CD, and automation
  • Open Source — MIT licensed, community-driven

🤝 Contributing

We welcome contributions! Whether it's bug fixes, features, documentation, or examples.

Getting Started

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make your changes
  4. Run tests: make test
  5. Run linter: make lint
  6. Commit: git commit -m 'Add amazing feature'
  7. Push: git push origin feature/amazing-feature
  8. Open a Pull Request

Guidelines

  • Keep commits focused and descriptive
  • Add tests for new functionality
  • Update documentation as needed
  • Follow Go conventions and style
  • Run make fmt before committing

📝 License

MIT License — see LICENSE file for details.


📞 Support & Community


🙏 Acknowledgments

Built with:


📈 Project Status

Active Development — Maintained and open to contributions.

Latest features:

  • ✅ GraphQL subscriptions — subscribe streams graphql-transport-ws events as NDJSON
  • ✅ Curl-style HTTP controls — per-request --header/-H, timeout/retry/fail behavior, --insecure, and response metadata flags
  • ✅ Batch operations — execute multiple queries/mutations in one request (NDJSON + JSON array)
  • ✅ Server-side jq filtering — per-operation "jq" field for response transformation
  • ✅ Client-side jq — --jq flag applies jq to all batch responses
  • ✅ --jq on query/mutation — built-in jq filtering on a single operation's response, skipped on error so failures stay visible
  • ✅ --depth on queries/mutations — expand a filtered operation's referenced arg/return types in one command
  • ✅ Auto re-login on expired JWT — login --save-creds persists credentials so an expired token auto-refreshes before the next request (see [[Authentication]])
  • ✅ .gqlcli.json project config — named environments with URL and custom headers, --env flag
  • ✅ Inline execution — run operations in-process against a gqlgen schema (no HTTP server)
  • ✅ Schema hints — attach type SDL to GraphQL validation errors
  • ✅ Token store — JWT persistence and parsing for login/logout/whoami
  • ✅ Query and Mutation operation discovery (queries, mutations commands)
  • ✅ Token-optimized TOON format (default)
  • ✅ Environment variable support (GRAPHQL_URL)
  • ✅ Multiple output formats
  • ✅ Extensible architecture

Made with ❤️ for the GraphQL community

About

Graphql command-line client

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages