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 click

Full 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 json

Key Patterns

  • make_client() reads LEVELFOUR_TOKEN and LEVELFOUR_API environment variables via Click's envvar parameter
  • client.costs.get_summary() returns the overall spending overview
  • client.recommendations.list(page=page, page_size=page_size) returns paginated results
  • client.recommendations.get(recommendation_id=id) fetches a single recommendation
  • NotFoundError is caught before the generic LevelFourError for specific error messages
  • --json-output flag toggles between human-readable and JSON output

TypeScript with Commander.js

Dependencies

npm install levelfour commander

Full 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_123

Key Patterns

  • makeClient() reads LEVELFOUR_TOKEN and LEVELFOUR_API from process.env
  • client.costs.getSummary() returns the spending overview
  • client.recommendations.list() returns a Page object where .data is the items array
  • client.recommendations.get(id) takes the ID as a positional argument (not in a request object)
  • NotFoundError and LevelFourError are caught with instanceof, then unknown errors are re-thrown
  • --json flag toggles between human-readable and JSON output

Go with Cobra

Dependencies

go get github.com/LevelFourAI/levelfour-go
go get github.com/spf13/cobra

SDK 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 client
  • client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{...}) returns a Page with .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 estimate and l4 diff for Terraform cost analysis (see CI/CD Integration)