Guides

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

  1. Install the SDK matching your host CLI's language
  2. Construct a client once, share via your CLI's context object
  3. Map SDK responses into your CLI's existing output formatter
  4. Map SDK typed errors to your CLI's exit codes
  5. Use the SDK's iterators for paginated endpoints

Python (Click)

pip install levelfour click
from __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 commander
import { 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/cobra
package 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, &notFoundErr):
        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?

ConcernEmbed the SDKShell out to l4
Adds a dependencyYes (PyPI / npm / go get)No — single static binary
Typed objectsYes (Pydantic / TS types / Go structs)No (you parse JSON)
Granular control over filtering, paginationYes — full SDK surfaceYes via l4 flags + jq
Works in any languageNo — Python / TS / Go onlyYes — anything that can exec
Adds binary install step in CINo (package manager handles it)Yes (brew install or release download)
Keeps PR diffs language-nativeYesMixes shell into your codebase
Stays current automaticallyLockfile-controlledl4 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.