Integrate the SDK into your CLI
If your team already has an internal CLI (e.g. acme-cli) and you want to surface LevelFour data inside it — acme-cli costs summary, acme-cli recs list — embed the SDK as a library. This gives you typed objects, language-native error handling, and the ability to mix LevelFour calls with the rest of your CLI's logic.
For a zero-dependency alternative that just shells out to the official l4 binary, see Integrate l4 into your CLI. The two approaches are compared at the bottom of this page.
Pattern
- Install the SDK matching your host CLI's language
- Construct a client once, share via your CLI's context object
- Map SDK responses into your CLI's existing output formatter
- Map SDK typed errors to your CLI's exit codes
- Use the SDK's iterators for paginated endpoints
Python (Click)
pip install levelfour clickfrom __future__ import annotations
import sys
import click
from levelfour import LevelFour
from levelfour.exceptions import (
LevelFourError,
AuthenticationError,
NotFoundError,
RateLimitError,
)
def make_client(ctx):
if "l4" not in ctx.obj:
ctx.obj["l4"] = LevelFour()
return ctx.obj["l4"]
@click.group()
@click.pass_context
def cli(ctx):
ctx.ensure_object(dict)
@cli.group()
def costs():
pass
@costs.command("summary")
@click.option("--provider", help="aws | gcp | azure | k8s")
@click.pass_context
def costs_summary(ctx, provider):
try:
l4 = make_client(ctx)
if provider:
data = l4.providers.get_costs_summary(provider)
else:
data = l4.costs.get_summary()
click.echo(f"Monthly spend: ${data.monthly_spending:,.2f}")
click.echo(f"Forecast: ${data.forecasted_monthly_costs:,.2f}")
click.echo(f"Potential savings: ${data.potential_savings:,.2f}")
except AuthenticationError:
click.echo("Auth required. Set LEVELFOUR_API_KEY.", err=True)
sys.exit(4)
except RateLimitError as e:
click.echo(f"Rate limited; retry after {e.retry_after}s", err=True)
sys.exit(2)
except NotFoundError as e:
click.echo(f"Not found: {e.message}", err=True)
sys.exit(1)
except LevelFourError as e:
click.echo(f"LevelFour error {e.status_code}: {e.message}", err=True)
sys.exit(1)
@cli.group()
def recs():
pass
@recs.command("list")
@click.option("--status", multiple=True)
@click.option("--service", multiple=True)
@click.pass_context
def recs_list(ctx, status, service):
l4 = make_client(ctx)
for rec in l4.recommendations.list(
page_size=100,
display_status=list(status) or None,
service=list(service) or None,
):
click.echo(f"{rec.recommendation_id}\t${rec.monthly_savings:.2f}/mo\t{rec.service}")
if __name__ == "__main__":
cli()The SDK's SyncPager auto-fetches every page when iterated; no manual for page in pages loop required.
TypeScript (Commander)
npm install levelfour commanderimport { Command } from "commander";
import {
LevelFourClient,
AuthenticationError,
RateLimitError,
NotFoundError,
LevelFourError,
} from "levelfour";
const program = new Command();
let client: LevelFourClient | null = null;
function getClient() {
if (!client) client = new LevelFourClient();
return client;
}
function exitOnError(err: unknown): never {
if (err instanceof AuthenticationError) {
console.error("Auth required. Set LEVELFOUR_API_KEY.");
process.exit(4);
}
if (err instanceof RateLimitError) {
console.error(`Rate limited; retry after ${err.retryAfter}s`);
process.exit(2);
}
if (err instanceof NotFoundError) {
console.error(`Not found: ${err.message}`);
process.exit(1);
}
if (err instanceof LevelFourError) {
console.error(`LevelFour error ${err.statusCode}: ${err.message}`);
process.exit(1);
}
throw err;
}
const costs = program.command("costs");
costs
.command("summary")
.option("--provider <id>", "aws | gcp | azure | k8s")
.action(async (opts) => {
try {
const l4 = getClient();
const data = opts.provider
? await l4.providers.getCostsSummary({ provider_id: opts.provider })
: await l4.costs.getSummary();
console.log(`Monthly spend: $${data.monthly_spending.toLocaleString()}`);
console.log(`Forecast: $${data.forecasted_monthly_costs.toLocaleString()}`);
console.log(`Potential savings: $${data.potential_savings.toLocaleString()}`);
} catch (e) {
exitOnError(e);
}
});
const recs = program.command("recs");
recs
.command("list")
.option("--status <s...>")
.option("--service <s...>")
.action(async (opts) => {
try {
const l4 = getClient();
for await (const rec of await l4.recommendations.list({
page_size: 100,
display_status: opts.status,
service: opts.service,
})) {
console.log(`${rec.recommendation_id}\t$${rec.monthly_savings.toFixed(2)}/mo\t${rec.service}`);
}
} catch (e) {
exitOnError(e);
}
});
await program.parseAsync();Go (Cobra)
go get github.com/LevelFourAI/levelfour-go github.com/spf13/cobrapackage main
import (
"context"
"errors"
"fmt"
"os"
"github.com/LevelFourAI/levelfour-go/levelfour"
"github.com/spf13/cobra"
)
var l4 *levelfour.Client
func makeClient() (*levelfour.Client, error) {
if l4 != nil {
return l4, nil
}
c, err := levelfour.NewClient("")
if err != nil {
return nil, err
}
l4 = c
return l4, nil
}
func exitOnError(err error) {
var authErr *levelfour.UnauthorizedError
var rateErr *levelfour.TooManyRequestsError
var notFoundErr *levelfour.NotFoundError
switch {
case errors.As(err, &authErr):
fmt.Fprintln(os.Stderr, "Auth required. Set LEVELFOUR_API_KEY.")
os.Exit(4)
case errors.As(err, &rateErr):
fmt.Fprintln(os.Stderr, "Rate limited")
os.Exit(2)
case errors.As(err, ¬FoundErr):
fmt.Fprintln(os.Stderr, "Not found")
os.Exit(1)
default:
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func main() {
root := &cobra.Command{Use: "acme"}
costsSummary := &cobra.Command{
Use: "costs-summary",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := makeClient()
if err != nil {
exitOnError(err)
}
data, err := client.Costs.GetSummary(context.Background())
if err != nil {
exitOnError(err)
}
fmt.Printf("Monthly spend: $%.2f\n", data.MonthlySpending)
return nil
},
}
root.AddCommand(costsSummary)
if err := root.Execute(); err != nil {
os.Exit(1)
}
}levelfour.PageIterator and levelfour.CollectAll handle pagination — see Pagination.
Authentication: re-use what's already there
The SDK's LEVELFOUR_API_KEY env-var auto-detection means you don't have to add a new credential layer to your CLI. If your team already injects an API key via ~/.acme/credentials or a secret manager, just export LEVELFOUR_API_KEY from that source before constructing the client.
SDK-embed vs l4 shell-out — which to choose?
| Concern | Embed the SDK | Shell out to l4 |
|---|---|---|
| Adds a dependency | Yes (PyPI / npm / go get) | No — single static binary |
| Typed objects | Yes (Pydantic / TS types / Go structs) | No (you parse JSON) |
| Granular control over filtering, pagination | Yes — full SDK surface | Yes via l4 flags + jq |
| Works in any language | No — Python / TS / Go only | Yes — anything that can exec |
| Adds binary install step in CI | No (package manager handles it) | Yes (brew install or release download) |
| Keeps PR diffs language-native | Yes | Mixes shell into your codebase |
| Stays current automatically | Lockfile-controlled | l4 self-checks for updates |
Rule of thumb: if your CLI is in Python, TypeScript, or Go and you want first-class types, embed the SDK. If you're scripting in bash, Make, or anything else — or you want to avoid adding a dependency — shell out to l4.