qqq <noun> <verb> for table (list, describe, query, count, get, create, update, delete, export), process (list, describe, run) and job (list, get, logs, cancel, retry), with the 0.1 flat commands kept as hidden aliases. Global --output json|table|text|csv|yaml, --json, --query (JMESPath), --fields, --yes and --trace work before or after the subcommand. Filters take --where, --group (AND/OR with parentheses), --or, --order, --filter-json, --print-filter and --count; queries page with --limit, --skip and --all. qqq api <operationId> calls any operation from the generated index (--path, --param, --body-json, --form, --bytes, --out). Exit codes: 2 usage and bad filters, 3 auth, 4 unreachable or rate limited, 5 not found, 6 validation, 70 server error. The realtime stream can carry a session cookie for tail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qqq-cli
The reference command-line client for QQQ applications, and the kit for writing your own CLI, TUI or native client. Everything here talks to a QQQ server only through the UI API (/qqq/v1), the API the web UI uses too.
| Package | What it is |
|---|---|
@qrun-io/qqq-cli |
qqq, the reference CLI: device-code sign-in, apps, tables, filtered queries, records, processes, jobs, and a live event stream |
@qrun-io/qqq-ui-api-client |
A typed TypeScript client, generated from the merged UI API document (core plus extensions). It also does OAuth2 device-flow sign-in, token refresh, and reads the realtime (SSE) stream |
@qrun-io/qqq-ui-api-conformance |
A black-box conformance suite you can run against any server, plus qqq-dev-idp, a local OIDC provider for testing native clients |
The packages are published to GitHub Packages:
echo "@qrun-io:registry=https://npm.pkg.github.com" >> ~/.npmrc
npm install -g @qrun-io/qqq-cli@rc
Requires Node.js 20 or later and a QQQ server with UI API 1.6.0 or later (QQQ 4.1.0-RC.15 or later).
The CLI
$ qqq login --server https://admin.example.com
To sign in to https://admin.example.com, open:
https://auth.example.com/device?code=ED84-CC03
and confirm the code ED84-CC03 (it expires in 10 minutes).
Waiting for approval...
Signed in to https://admin.example.com as darin@example.com.
$ qqq tables --app peopleApp
name label insert edit delete
------- -------- ------ ---- ------
person Person yes yes yes
pet Pet yes yes yes
$ qqq query person --where "firstName ~ a" --where "id < 100" --order -id --limit 5
Id First Name Last Name Annual Salary ...
5 Morgan Sample $1,500,000.00
3 Casey Sample $300,000.00
(2 records)
$ qqq get person 1
$ qqq run greetInteractive --records 1,2 --value greetingPrefix=Hi
$ qqq jobs --all
$ qqq tail --channel table:person
| Command | UI API operations |
|---|---|
login |
getAuthenticationMetaData, then the provider's device-authorization grant with values.nativeClientId |
whoami |
getVersion, getMetaData |
apps, tables [--app] |
getMetaData |
query <table> [--where "field OP value"]... [--or] [--order [-]field] [--limit n] [--fields a,b] |
getTableMetaData, queryTable |
get <table> <pk> |
getTableMetaData, getRecord |
run <process> [--value k=v]... [--records ids] |
getProcessMetaData, initProcess, stepProcess, getProcessStatus |
jobs [--all] |
listJobs |
tail [--channel ...] |
openRealtimeStream, updateRealtimeSubscriptions (user:me, plus your channels) |
Add --json for machine-readable output. Errors print the server's message, code and traceId.
Where things are kept:
- Tokens go in
~/.config/qqq-cli/credentials.json(mode 0600), one entry per server. The CLI refreshes the access token at the provider before it expires, and once more on a 401. QQQ_TOKENoverrides the stored token with any bearer token, such as a QQQ access token exchanged for a JWT, or a session UUID.QQQ_SERVERsets the default server.
Every request carries X-QQQ-Client: cli/qqq-cli <version>, so the server's audits attribute it to the CLI.
Writing a client
import { createUiApiClient, unwrap, getNativeSignInConfig, deviceSignIn } from '@qrun-io/qqq-ui-api-client'
const config = await getNativeSignInConfig('https://admin.example.com')
const tokens = await deviceSignIn(config, (p) => console.log(`Open ${p.verificationUriComplete}`))
const api = createUiApiClient({ baseUrl: 'https://admin.example.com', token: () => tokens.accessToken, clientChannel: 'tui/my-tui 1.0' })
const page = await unwrap(api.POST('/qqq/v1/table/{tableName}/query', {
params: { path: { tableName: 'person' } },
body: { filter: { criteria: [{ fieldName: 'lastName', operator: 'STARTS_WITH', values: ['K'] }], limit: 20 } },
}))
Paths, parameters, bodies and responses are typed from the document. Read the QQQ docs before you write a client:
UI-API-CLIENTS.mdcovers authentication, errors, widgets, processes, capabilities and validation.UI-API-CHANGELOG.mdcovers versioning.
Mock server
npm run mock serves the merged document with Prism on http://127.0.0.1:4010, so you can build a client without a server. Responses come from the document's examples and schemas.
Conformance suite
qqq-ui-api-conformance --server https://admin.example.com # public checks only
qqq-ui-api-conformance --server https://admin.example.com --token "$T" # all read-only checks
The suite only reads. It never writes. It checks that:
- the version and the served document agree;
- every operation has a unique operationId and every
$refresolves; - every extension the server lists has operations;
- requests without credentials or with forged tokens (
alg: none, garbage) get a 401 with the error schema; - metadata, table metadata (with
clientCapabilities), query, count, record, jobs and realtime responses validate against the document; - a 404 has the error schema, and
X-Request-Idcomes back astraceId.
Testing a client locally
qqq-dev-idp is a small OIDC provider for local development only: device grant, refresh and RS256 JWTs, with one issuer per client, as Authentik does it. Run it with the QQQ sample app:
npx qqq-dev-idp --port 9400 --client sample-app --client sample-cli --user you@example.com --groups admins --auto-approve
# in a qqq checkout (QQQ 4.1.0-RC.15 or later)
export OAUTH2_BASE_URL=http://localhost:9400/application/o/sample-app/ OAUTH2_CLIENT_ID=sample-app OAUTH2_CLIENT_SECRET=dev \
OAUTH2_SCOPES="openid profile email offline_access" \
OAUTH2_NATIVE_CLIENT_ID=sample-cli OAUTH2_NATIVE_CLIENT_BASE_URL=http://localhost:9400/application/o/sample-cli/
./mvnw -f qqq-sample-project/pom.xml compile exec:java -Dexec.mainClass=com.kingsrook.sampleapp.SampleJavalinServer -Dqqq.sample.port=8077
qqq login --server http://localhost:8077
Development
npm ci
npm run spec:merge -- <core openapi.yaml> <extension yaml>... # or a server's /qqq/v1/openapi.json
npm run generate # regenerate packages/ui-api-client/src/schema.ts
npm run build && npm test
Releases: push a tag v<client version>. The publish workflow publishes the three packages to GitHub Packages.
License
Apache-2.0