Guides
Building a CLI
Overview
This guide shows how to build a CLI tool that wraps the LevelFour SDK. The code below is extracted from production CLI implementations that have been tested against the live API.
The CLI covers:
- Authentication (
whoami) - Cost summaries and breakdowns (
costs summary,costs breakdown) - Recommendations (
recommendations list,recommendations view,recommendations by-provider) - Bulk export (
export-recommendations) - Error handling and JSON output
Python with Click
Dependencies
pip install levelfour clickFull CLI
from __future__ import annotations
import json
import sys
import click
from levelfour import LevelFour
from levelfour.exceptions import (
LevelFourError,
NotFoundError,
)
def make_client(token, api):
kwargs = {}
if token:
kwargs["api_key"] = token
if api:
kwargs["base_url"] = api
return LevelFour(**kwargs)
def print_json(data):
click.echo(json.dumps(data, indent=2, default=str))
@click.group()
@click.option("-t", "--token", envvar="LEVELFOUR_TOKEN", help="API token")
@click.option("--api", envvar="LEVELFOUR_API", help="API base URL")
@click.option("--json-output", "as_json", is_flag=True, help="Output as JSON")
@click.pass_context
def cli(ctx, token, api, as_json):
ctx.ensure_object(dict)
ctx.obj["token"] = token
ctx.obj["api"] = api
ctx.obj["json"] = as_json
@cli.command()
@click.pass_context
def whoami(ctx):
client = make_client(ctx.obj["token"], ctx.obj["api"])
try:
resp = client.auth.get_whoami()
data = resp.data if hasattr(resp, "data") else resp
if ctx.obj["json"]:
print_json(data.__dict__ if hasattr(data, "__dict__") else data)
else:
click.echo(f"User: {getattr(data, 'email', 'N/A')}")
click.echo(f"Organization: {getattr(data, 'organization_name', 'N/A')}")
except LevelFourError as e:
click.echo(f"Error: {e.status_code} {e.code}: {e.message}", err=True)
sys.exit(1)
@cli.group()
def costs():
pass
@costs.command("summary")
@click.pass_context
def costs_summary(ctx):
client = make_client(ctx.obj["token"], ctx.obj["api"])
try:
spending = client.costs.get_summary()
savings = client.recommendations.audit.get_summary()
if ctx.obj["json"]:
print_json({
"spending": spending.__dict__ if hasattr(spending, "__dict__") else spending,
"savings": savings.__dict__ if hasattr(savings, "__dict__") else savings,
})
else:
click.echo("=== Spending Summary ===")
click.echo(f" {spending}")
click.echo()
click.echo("=== Savings Summary ===")
click.echo(f" {savings}")
except LevelFourError as e:
click.echo(f"Error: {e.status_code} {e.code}: {e.message}", err=True)
sys.exit(1)
@recommendations.command("list")
@click.option("--page", default=1, type=int)
@click.option("--page-size", default=20, type=int)
@click.pass_context
def recommendations_list(ctx, page, page_size):
client = make_client(ctx.obj["token"], ctx.obj["api"])
try:
resp = client.recommendations.list(page=page, page_size=page_size)
if ctx.obj["json"]:
print_json(resp.__dict__ if hasattr(resp, "__dict__") else resp)
else:
data = resp.data if hasattr(resp, "data") else resp
items = data.items if hasattr(data, "items") else []
click.echo(f"Recommendations (page {page}, {len(items)} items):")
for item in items:
name = getattr(item, "title", getattr(item, "name", str(item)))
savings_val = getattr(item, "monthly_savings", "N/A")
click.echo(f" - {name}: ${savings_val}/mo")
except LevelFourError as e:
click.echo(f"Error: {e.status_code} {e.code}: {e.message}", err=True)
sys.exit(1)
@cli.group()
def recommendations():
pass
@recommendations.command("view")
@click.argument("recommendation_id")
@click.pass_context
def recommendations_view(ctx, recommendation_id):
client = make_client(ctx.obj["token"], ctx.obj["api"])
try:
resp = client.recommendations.get(recommendation_id=recommendation_id)
if ctx.obj["json"]:
print_json(resp.__dict__ if hasattr(resp, "__dict__") else resp)
else:
data = resp.data if hasattr(resp, "data") else resp
click.echo(f"Recommendation: {getattr(data, 'title', 'N/A')}")
click.echo(f" Status: {getattr(data, 'status', 'N/A')}")
click.echo(f" Monthly Savings: ${getattr(data, 'monthly_savings', 'N/A')}")
click.echo(f" Provider: {getattr(data, 'provider', 'N/A')}")
except NotFoundError:
click.echo(f"Recommendation {recommendation_id} not found", err=True)
sys.exit(1)
except LevelFourError as e:
click.echo(f"Error: {e.status_code} {e.code}: {e.message}", err=True)
sys.exit(1)
@cli.command("export-recommendations")
@click.option("--format", "fmt", default="json", type=click.Choice(["json", "csv"]))
@click.pass_context
def export_recommendations(ctx, fmt):
client = make_client(ctx.obj["token"], ctx.obj["api"])
try:
all_items = []
page = 1
page_size = 100
while True:
resp = client.recommendations.list(page=page, page_size=page_size)
data = resp.data if hasattr(resp, "data") else resp
items = data.items if hasattr(data, "items") else []
all_items.extend(items)
pagination = data.pagination if hasattr(data, "pagination") else None
if not pagination or not getattr(pagination, "has_next", False):
break
page += 1
click.echo(f"Exported {len(all_items)} recommendations", err=True)
if fmt == "json":
print_json([item.__dict__ if hasattr(item, "__dict__") else item for item in all_items])
except LevelFourError as e:
click.echo(f"Error: {e.status_code} {e.code}: {e.message}", err=True)
sys.exit(1)
if __name__ == "__main__":
cli()Usage
export LEVELFOUR_TOKEN="l4_live_..."
python cli.py whoami
python cli.py costs summary
python cli.py costs summary --json-output
python cli.py recommendations list --page-size 5
python cli.py recommendations view rec_123
python cli.py export-recommendations --format jsonKey Patterns
make_client()readsLEVELFOUR_TOKENandLEVELFOUR_APIenvironment variables via Click'senvvarparameterclient.costs.get_summary()returns the overall spending overviewclient.recommendations.list(page=page, page_size=page_size)returns paginated resultsclient.recommendations.get(recommendation_id=id)fetches a single recommendationNotFoundErroris caught before the genericLevelFourErrorfor specific error messages--json-outputflag toggles between human-readable and JSON output
TypeScript with Commander.js
Dependencies
npm install levelfour commanderFull CLI
import { Command } from "commander";
import {
LevelFourClient,
NotFoundError,
LevelFourError,
} from "levelfour";
const program = new Command();
function makeClient(opts: { token?: string; api?: string }): LevelFourClient {
return new LevelFourClient({
apiKey: opts.token ?? process.env.LEVELFOUR_TOKEN,
baseUrl: opts.api ?? process.env.LEVELFOUR_API,
});
}
function printJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
}
program
.name("l4")
.option("-t, --token <key>", "API token")
.option("--api <url>", "API base URL")
.option("--json", "Output as JSON");
program
.command("whoami")
.description("Show current user identity")
.action(async () => {
const opts = program.opts();
const client = makeClient(opts);
try {
const resp = await client.auth.getWhoami();
if (opts.json) {
printJson(resp);
} else {
const data = (resp as any).data ?? resp;
console.log(`User: ${data.email ?? "N/A"}`);
console.log(`Organization: ${data.organization_name ?? "N/A"}`);
}
} catch (err) {
if (err instanceof LevelFourError) {
console.error(`Error: ${err.statusCode} ${err.message}`);
process.exit(1);
}
throw err;
}
});
const costsCmd = program.command("costs").description("Cost analysis");
costsCmd
.command("summary")
.description("Spending and savings overview")
.action(async () => {
const opts = program.opts();
const client = makeClient(opts);
try {
const spending = await client.costs.getSummary();
const savings = await client.recommendations.audit.getSummary();
if (opts.json) {
printJson({ spending, savings });
} else {
console.log("=== Spending Summary ===");
console.log(` ${JSON.stringify(spending)}`);
console.log();
console.log("=== Savings Summary ===");
console.log(` ${JSON.stringify(savings)}`);
}
} catch (err) {
if (err instanceof LevelFourError) {
console.error(`Error: ${err.statusCode} ${err.message}`);
process.exit(1);
}
throw err;
}
});
const recommendationsCmd = program.command("recommendations").description("Recommendations");
recommendationsCmd
.command("list")
.description("List recommendations")
.option("--page <n>", "Page number", "1")
.option("--page-size <n>", "Items per page", "20")
.action(async (cmdOpts) => {
const opts = program.opts();
const client = makeClient(opts);
try {
const resp = await client.recommendations.list({
page: parseInt(cmdOpts.page),
pageSize: parseInt(cmdOpts.pageSize),
});
if (opts.json) {
printJson(resp);
} else {
const items = (resp as any).data ?? [];
console.log(`Recommendations (page ${cmdOpts.page}, ${items.length} items):`);
for (const item of items) {
const name = (item as any).title ?? (item as any).name ?? JSON.stringify(item);
const monthlySavings = (item as any).monthly_savings ?? "N/A";
console.log(` - ${name}: $${monthlySavings}/mo`);
}
}
} catch (err) {
if (err instanceof LevelFourError) {
console.error(`Error: ${err.statusCode} ${err.message}`);
process.exit(1);
}
throw err;
}
});
recommendationsCmd
.command("view <id>")
.description("View a recommendation")
.action(async (id: string) => {
const opts = program.opts();
const client = makeClient(opts);
try {
const resp = await client.recommendations.get(id);
if (opts.json) {
printJson(resp);
} else {
const data = (resp as any).data ?? resp;
console.log(`Recommendation: ${data.title ?? "N/A"}`);
console.log(` Status: ${data.status ?? "N/A"}`);
console.log(` Monthly Savings: $${data.monthly_savings ?? "N/A"}`);
console.log(` Provider: ${data.provider ?? "N/A"}`);
}
} catch (err) {
if (err instanceof NotFoundError) {
console.error(`Recommendation ${id} not found`);
process.exit(1);
}
if (err instanceof LevelFourError) {
console.error(`Error: ${err.statusCode} ${err.message}`);
process.exit(1);
}
throw err;
}
});
program.parseAsync(process.argv);Usage
export LEVELFOUR_TOKEN="l4_live_..."
npx tsx cli.ts whoami
npx tsx cli.ts costs summary
npx tsx cli.ts costs summary --json
npx tsx cli.ts recommendations list --page-size 5
npx tsx cli.ts recommendations view rec_123Key Patterns
makeClient()readsLEVELFOUR_TOKENandLEVELFOUR_APIfromprocess.envclient.costs.getSummary()returns the spending overviewclient.recommendations.list()returns aPageobject where.datais the items arrayclient.recommendations.get(id)takes the ID as a positional argument (not in a request object)NotFoundErrorandLevelFourErrorare caught withinstanceof, then unknown errors are re-thrown--jsonflag toggles between human-readable and JSON output
Go with Cobra
Dependencies
go get github.com/LevelFourAI/levelfour-go
go get github.com/spf13/cobraSDK Client Wrapper
The Go CLI wraps the SDK in a client struct that also holds a raw HTTP client for endpoints not covered by the SDK:
package api
import (
"net/http"
"github.com/LevelFourAI/levelfour-go/levelfour"
)
type SDKClient struct {
sdk *levelfour.Client
BaseURL string
}
func NewSDKClient(baseURL, token string) (*SDKClient, error) {
sdk, err := levelfour.NewClient(token,
levelfour.WithBaseURL(baseURL),
levelfour.WithHTTPClient(&http.Client{}),
)
if err != nil {
return nil, err
}
return &SDKClient{sdk: sdk, BaseURL: baseURL}, nil
}
func (c *SDKClient) SDK() *levelfour.Client {
return c.sdk
}Token Resolution
The Go CLI resolves tokens from three sources in priority order:
func resolveToken(flagToken string) (string, error) {
if flagToken != "" {
return flagToken, nil
}
if key := os.Getenv("LEVELFOUR_TOKEN"); key != "" {
return key, nil
}
return "", fmt.Errorf("not authenticated - set LEVELFOUR_TOKEN or use --token")
}Recommendations List Command
var recommendationsListCmd = &cobra.Command{
Use: "list",
Short: "List recommendations",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := newSDKClient()
if err != nil {
return err
}
page, _ := cmd.Flags().GetInt("page")
pageSize, _ := cmd.Flags().GetInt("page-size")
resp, err := client.SDK().Recommendations.List(
context.Background(),
&levelfour.ListRecommendationsRequest{
Page: levelfour.Int(page),
PageSize: levelfour.Int(pageSize),
},
)
if err != nil {
return err
}
for _, item := range resp.Results {
fmt.Printf("%-12s %-20s $%.2f/mo\n",
item.RecommendationID,
item.Service,
item.MonthlySavings,
)
}
return nil
},
}Key Patterns
levelfour.NewClient(token, levelfour.WithBaseURL(url))creates the SDK clientclient.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{...})returns aPagewith.Results- Optional fields use helper functions:
levelfour.Int(n),levelfour.String(s) - Errors are returned directly (Cobra prints them)
- The Go CLI also supports
l4 estimateandl4 difffor Terraform cost analysis (see CI/CD Integration)