pscli is a small Go CLI for querying Perfectscale's public API with a UI-generated service token.
It is optimized for fast terminal exploration and agent-friendly output, with:
- stored local auth profiles
- sensible production defaults
- table, JSON, and JSONL output
- workload filtering, sorting, aggregation, and CSV export
- GitHub Actions builds for macOS, Windows, and Linux
- Homebrew install via
brew install doitintl/tap/pscli - Scoop install on Windows via
scoop bucket add pscli ... && scoop install pscli .deb/.rpmpackages for direct install on Linuxpscli skill <agent>to install the Perfectscale coding-agent skill
This CLI is intentionally public-API only. Command groups: auth,
clusters, namespaces, workloads, nodegroups, unevictable,
automation, open, plus update, skill, and commands — see their dedicated sections
below for subcommands and flags.
The CLI uses Perfectscale public API service tokens from the Perfectscale UI.
To generate one:
- Open
app.perfectscale.io - Click the user circle in the bottom left
- Open
Org Settings - Open
API Tokens - Click
Generate Token - Assign a
Read Onlyrole - Copy the
client_idandclient_secret
If you run pscli auth without saved credentials, the CLI prints the same setup guide plus a ready-to-copy login example.
Then log in:
pscli auth loginOr pass the credentials directly:
pscli auth login --client-id 'YOUR_CLIENT_ID' --client-secret 'YOUR_CLIENT_SECRET'The CLI validates the token by calling the public auth endpoint and saves the profile locally.
The CLI is designed so common usage does not require flags.
Default profile:
default
Default output:
table
Default public API base URL:
https://api.app.perfectscale.io/public/v1
If you do not export any environment variables and do not pass any global flags, the CLI will talk to production.
Global flags can be set through environment variables:
PERFECTSCALE_PROFILEPERFECTSCALE_OUTPUTPERFECTSCALE_DEBUGPERFECTSCALE_PUBLIC_API_URL
Examples:
export PERFECTSCALE_PROFILE='dev-public'
export PERFECTSCALE_PUBLIC_API_URL='https://api.dev.perfectscale.click/public/v1'
export PERFECTSCALE_OUTPUT='jsonl'-o json/-o jsonl (or PERFECTSCALE_OUTPUT=json/jsonl) are explicit,
deliberate choices — there's no auto-detection of "an agent is driving this."
Agents and humans alike get exactly what they ask for: pass -o (or set
PERFECTSCALE_OUTPUT) explicitly for machine-readable output, and leave it
unset for the default table.
-o json is a single, pretty-printed document — meant to be read.
-o jsonl is a compact stream, one object per line — meant to be parsed
(jq -c per line, jq -s to collect). A command whose result isn't
list-shaped falls back to a single compact JSON document under -o jsonl,
matching jsonl's own convention.
On failure, the error is printed as a JSON object on stderr instead of the usual plain-text message, following the same pretty-vs-compact split:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "cluster \"prod-a\" not found",
"retryable": false,
"hint": "check the name/ID, or run the resource's `list` command to see what's available.",
"request_id": "6a993ed529ef3441362e89911d67f328"
}
}code is a stable machine-readable string; retryable says whether
retrying the same request might succeed (true for rate limits and 5xx
responses); hint is a short human-readable suggestion for what to do next.
request_id is the API's X-Request-Id for that call, present only for
errors that came from an actual HTTP response (a client-side "not found"
derived from a list you already have, for example, has none) — worth
including if you need to escalate to Perfectscale support. The process exit
code is also mapped to the error category — 2 for a usage/flag mistake
(including an unrecognized command or subcommand), 10/11 for an
authentication/authorization failure, 20 for "not found", 21 for a
conflict, 30 for a rejected request, 40/41/50 for server/network/
rate-limit failures, 1 otherwise — always, not just when output is json/
jsonl, so a shell script can branch on $? without parsing any output at
all. In table mode, hint and request_id (when present) print as extra
lines under the error message instead of JSON fields.
Profiles are stored under the OS config directory.
On macOS, the default profile path is usually:
~/Library/Application Support/perfectscale-cli/profiles/default.json
Storage behavior:
- profile directory permissions:
0700 - profile file permissions:
0600 auth logoutdeletes the selected local profile
brew install doitintl/tap/pscliTo upgrade to the latest release:
brew upgrade pscliscoop bucket add pscli https://github.com/doitintl/perfectscale-cli
scoop install pscliTo upgrade to the latest release:
scoop update pscliDownload the package for your architecture from Releases and install it directly:
sudo dpkg -i pscli_<version>_linux_amd64.deb # Debian/Ubuntu
sudo rpm -Uvh pscli_<version>_linux_amd64.rpm # Fedora/RHELThis is a direct package install, not a hosted apt/yum repository — there's no
apt install psclioradd-apt-repositorystep.
Download the archive for your platform from
Releases and
extract the pscli binary onto your PATH.
Requirements:
- Go 1.24+
Run directly:
go run . clusters listBuild locally:
make build
./dist/pscli clusters listOr build the binary directly:
go build -o pscli .
./pscli clusters listRegenerate the local public API client:
make openapiRun tests:
go test ./...--profile/-p, --output/-o, --debug/-d, and --public-api-url/-u
work at the top level and on leaf commands. Output modes: table (default,
human-friendly), json (one document), jsonl (one object per line, for
list commands and automation):
pscli -o jsonl workloads list -c prod-a -s waste -r desc -T 10Short options are consistent across commands:
-pprofile,-ooutput,-upublic API URL,-ddebug-ccluster,-wperiod window,-nnamespace-mworkload name,-tworkload type-ssort,-rorder,-Ttop,-Bbottom-Cmin-cost,-Wmin-waste,-Vworkload view-iid or client-id,-kclient-secret or label key (depends on command)-fexport format,-Fexport file-Smin-severity,-vlabel value,-gnode group name
pscli auth login
pscli auth status
pscli clusters listSee the dedicated sections below for each command group's full flags and examples.
workloads list supports client-side filtering and ranking with:
--cluster,-c--period,-w--namespace,-n--name,-m--type,-t--min-cost,-C--min-waste,-W--sort,-s--order,-r--top,-T--bottom,-B--view,-V
Important:
- the public workloads API is fixed to
30dtoday - the CLI enforces
--period 30d - non-cluster filters are applied client-side after the workload list is fetched
Examples:
pscli workloads list -c prod-a -n kube-system -s waste -r desc
pscli workloads list -c prod-a -m api -t Deployment -C 25 -W 10
pscli workloads list -c prod-a -s cost -r desc -T 20workloads list supports view presets through --view or -V.
Available views:
default- cost, waste, namespace, type, and max-indicator overview
capacity- replica counts and current vs recommended request totals
usage- summed container usage percentiles
policy- optimization policy, resilience, and mute state
risk- risk severity, risk counts, and waste counts
all- the broadest enriched workload view
Special behavior:
- if
--view allis used without explicitly setting--output, the CLI switches tojsonl - this makes the full enriched workload objects easier to consume in pipelines and by agents
Examples:
pscli workloads list -c prod-a -V default
pscli workloads list -c prod-a -V capacity
pscli workloads list -c prod-a -V usage
pscli workloads list -c prod-a -V policy
pscli workloads list -c prod-a -V risk
pscli workloads list -c prod-a -V all
pscli -o json workloads list -c prod-a -V allCluster summary:
pscli workloads summary -c prod-aGroup by namespace:
pscli workloads group-by namespace -c prod-a -s waste -r desc -T 10Group by workload type:
pscli workloads group-by type -c prod-a -s workloads -r descGroup by optimization policy:
pscli workloads group-by optimization-policy -c prod-a -s waste -r descGroup by risk severity:
pscli workloads group-by risk-severity -c prod-a -s workloads -r descGroup by label value:
pscli workloads group-by label -c prod-a -k team -s waste -r descShow one workload:
pscli workloads show -c prod-a -i workload-123
pscli workloads show -c prod-a -m api -n backendExport CSV:
pscli workloads export -c prod-a -F workloads.csv
pscli workloads export -c prod-a -n kube-system -s waste -r desc -T 25List risky workloads:
pscli workloads risky -c prod-a -S 2 -s severity -r desc -T 10Explore workload labels:
pscli workloads labels -c prod-a
pscli workloads labels -c prod-a -k app -s waste -r desc -T 20
pscli workloads labels -c prod-a -v productionList muted workloads:
pscli workloads muted -c prod-a -s expires -r ascList clusters:
pscli clusters listGet cluster details:
pscli clusters get -c prod-aShow carbon emission metrics:
pscli clusters emission -c prod-a -s value -r descNamespaces are derived from workloads.
Examples:
pscli namespaces list -c prod-a
pscli namespaces list -c prod-a -s workloads -r desc
pscli namespaces list -c prod-a -n kube -T 5nodegroups list and nodegroups get wrap the public InfraFit node-groups
endpoint: node count/pod count, cost and idle breakdown, CPU/memory/GPU
utilization percentiles, and instance-type or Karpenter recommendations per
node group.
Examples:
pscli nodegroups list -c prod-a
pscli nodegroups list -c prod-a --autoscaler-type karpenter --has-recommendations
pscli nodegroups list -c prod-a --all -o jsonl
pscli nodegroups list -c prod-a -V gpu
pscli nodegroups get -c prod-a -g clickhouseNotes:
-V/--view:default(cost, CPU/memory, recommendation summary) orgpu(GPU architecture/utilization;-in GPU columns for non-GPU groups).-o json/-o jsonlalways include the full payload regardless of view.--autoscaler-type,--has-recommendations,--include-mutedare server-side filters.recommendationsis a discriminated union (standardorkarpenter).nodegroups listtable output shows a summary (type, count, top instance type — the latter only populated forstandard);nodegroups gettable output additionally lists each recommended change (Karpenter: recommended value + rationale; standard: instance type + cost/savings). Use-o json/-o jsonlfor the full payload, including the current value and Karpenter's rawcurrent_config/recommended_configdiff.-o jsonwraps the list as{"node_groups": [...], "pagination": {...}};-o jsonlemits one node group per line with no cursor.--page-sizeis 1–500 (default 50).--page-tokenconsumes the cursor frompagination.next(wire field:meta.pagination.next).--allauto-paginates until no next cursor remains, capped by--page-cap(default 50), always requesting page size 500 regardless of--page-sizesince the backend recomputes the full set each request.
unevictable list, report, show, and muted wrap the public
unevictable-pods endpoints: pods that autoscalers can't evict, why, and what
it's costing. All four are served from the latest pre-computed snapshot for
the cluster — there's no request-time recompute.
Examples:
pscli unevictable list -c prod-a
pscli unevictable list -c prod-a -n payments --reason pod_disruption_budget
pscli unevictable report -c prod-a -C 5 -s blockedCostHourly -r desc
pscli unevictable show -c prod-a -i a1b2c3d4
pscli unevictable muted -c prod-aNotes:
list/reportshow one row per pod with reason codes concatenated (e.g.pod_disruption_budget,node_selector);reportalso carriesnodeandpriority.showreturns full single-pod detail — remediation (fix summary, risk, confidence, current/recommended spec, unified diff) and sibling pod names — not populated bylist/report.-n,--reason,-g,-C/--min-blocked-costare server-side filters (AND-combined).--reasononly works onlist—report's filter schema doesn't accept it.--mutecontrols muted-finding visibility:exclude(default),include, oronly.-s/--sortonly acceptsblockedCostHourly, the only server-side sort key today.mutedis read-only — mute/dismissal rules can only be managed via the web app or the user API.- A 202 (snapshot processing) or 422 (processing failed) response surfaces as a distinct error, not a generic HTTP failure.
- Pagination flags (
--page-size,--page-token,--all,--page-cap) matchnodegroups list. Snapshot metadata (time, algorithm version, summary counts) appears in the table footer and top-level-o jsonfields, but not in--allmode (it spans multiple snapshot reads).
automation audit-logs lists the actions Perfectscale's automation took in your
clusters. The endpoint is cursor-paginated and returns events from the last 30
days.
Examples:
pscli automation audit-logs
pscli automation audit-logs -c prod-a -c prod-b
pscli automation audit-logs -c prod-a -n kube-system -n default
pscli automation audit-logs --from 2026-04-01T00:00:00Z --to 2026-04-15T00:00:00Z
pscli automation audit-logs --since 24h
pscli automation audit-logs --all -o jsonl
pscli automation audit-logs --page-size 200 --after BASE64CURSORNotes:
--cluster(-c) and--namespace(-n) are repeatable. Cluster values may be UID or name.--fromand--toaccept RFC3339 (UTC).--sinceaccepts a relative duration (24h,7d,30m) and is shorthand for--from now-since.--page-sizeis 1–5000 (default 1000).--after/--beforeconsume cursor tokens from a previous response'spagination.next/pagination.prev.--allauto-paginates forward until the server reportshas_next=false, capped by--page-cap(default 50) as a safety net.--executionfilters client-side to one ofregular-eviction,inplace-resize, orcleanup.
open cluster|workload|nodegroup|alerts|automation opens the matching page
on app.perfectscale.io in your default browser — a terminal shortcut to the
same pages you'd reach by clicking around the UI.
Examples:
pscli open cluster -c prod-a
pscli open workload -c prod-a -i workload-123
pscli open workload -c prod-a -m my-deployment -n my-namespace
pscli open nodegroup -c prod-a -g clickhouse
pscli open alerts -c prod-a
pscli open automation
pscli open automation -c prod-a -n my-namespace -m my-deployment -t Deployment --container exporterNotes:
cluster,workload,nodegroup, andalertsresolve--cluster(-c) by name or UID via the public API, same as the other command groups.workloadadditionally resolves the workload by--id/-ior--name/-m(+--namespace/-nto disambiguate), same rules asworkloads show.automation's filters (-c,-n,-m,-t,--container) are not resolved via the API — they're passed straight through as the audit log page's own filter query params, and all are optional.--period(-w, default30d) only sets the UI time window shown on the opened page. Forworkload, the lookup itself always uses the public API's fixed 30-day window regardless of--period.-o json/-o jsonlprint{"url": "..."}instead of opening a browser — useful for scripts/agents that have no browser to open.
pscli commands -o json
pscli commands -o jsonl
pscli commandsWalks the live command tree (no API call) and prints every visible command
with its flags and short-flag aliases. -o json emits one document
{version, cli_version, commands}; -o jsonl emits one command object per
line; table (default) prints PATH and USAGE.
Each command has path (for example ["workloads","list"]), usage,
description, runnable, and flags. Each flag has name, aliases,
type, usage, default, env_vars, required, and runtime (true
for the global profile/output/debug/public-api-url flags copied onto every
command).
pscli updateChecks the latest GitHub release against the running version and prints how
to update, based on the detected install method: brew upgrade pscli,
scoop update pscli, a download+install one-liner for dpkg/rpm, or a link to
the releases page otherwise. Doesn't install anything itself.
-o json/-o jsonl print {"current", "latest", "update_available", "instruction"} instead of the plain-text message.
pscli --version/-v prints the same plain-text status too (always plain
text, regardless of -o), skipped silently on a lookup failure.
pscli skill cursor
pscli skill --allCopies the skill compiled into the binary into that agent's user-level
skills directory (claude/cursor/gemini/kiro →
~/.<agent>/skills/perfectscale/, opencode →
~/.config/opencode/skills/perfectscale/, codex →
~/.agents/skills/perfectscale/). --all covers every agent whose config
dir already exists; --force overwrites local edits after backing them up.
pscli completion bash
pscli completion zsh
pscli completion fish
pscli completion powershellPrints a completion script for the given shell to stdout. bash/zsh/
powershell complete dynamically by shelling back into pscli; fish is
generated from the full command tree.
Install:
# bash
echo 'eval "$(pscli completion bash)"' >> ~/.bashrc
# zsh
echo 'eval "$(pscli completion zsh)"' >> ~/.zshrc
# fish
pscli completion fish > ~/.config/fish/completions/pscli.fish# powershell — add to $PROFILE
Invoke-Expression (& pscli completion powershell | Out-String)CI (build.yml) runs tests on every
push/pull_request — it never creates releases.
Releases are manual: Actions tab → "Release" → "Run workflow", with a bump
input (patch/minor/major, default patch).
release.yml cuts the version tag, then
goreleaser (.goreleaser.yaml)
cross-builds macOS/Windows/Linux (amd64/arm64) and publishes:
- GitHub Release assets: archives, checksums,
.deb/.rpmpackages - Homebrew formula →
doitintl/homebrew-tap(needs theHOMEBREW_TAP_APP_CLIENT_ID/HOMEBREW_TAP_APP_PRIVATE_KEYsecrets; skipped gracefully until they're set) - Scoop manifest → this repo's own
bucket/
Current asset names:
pscli-darwin-arm64.tar.gzpscli-darwin-amd64.tar.gzpscli-windows-amd64.zippscli-windows-arm64.zippscli-linux-amd64.tar.gzpscli-linux-arm64.tar.gzpscli_<version>_linux_amd64.deb/.rpmpscli_<version>_linux_arm64.deb/.rpm
Each release archive contains a pscli binary, or pscli.exe on Windows.
checksums.txt is published alongside them. The goreleaser config lives in
.goreleaser.yaml. The Perfectscale coding-agent skill
is compiled into the binary; install it with pscli skill <agent>.
The local spec is public-api.yaml; it generates
internal/publicapi/client.gen.go via
make openapi. Don't hand-edit the generated client — update the spec and
regenerate. internal/api/client.go is the
handwritten adapter on top: auth headers, response validation, and mapping
into CLI types.
- only service-token auth is supported
- only the public API is supported
- workloads are fixed to a
30dperiod because the public endpoint is fixed-window - namespace and many workload filters are client-side
- CSV is the only export format in v1