# Integrate the SDK into your CLI

> Embed the LevelFour SDK as a library inside an existing internal CLI to add cost-optimization subcommands.

Source: https://docs.levelfour.ai/guides/integrate-sdk-into-your-cli


Embedding the SDK as a library adds LevelFour subcommands to a CLI your team already ships: `acme-cli costs summary`, `acme-cli recs list`. You get typed objects and language-native error handling, and the LevelFour calls sit in the same code as the rest of your commands.

For an alternative that adds no dependency and shells out to the official `l4` binary, see [Integrate `l4` into your CLI](/guides/integrate-l4-into-your-cli). [Embed or shell out](#embed-or-shell-out) compares the two.

## 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

The status code to error class table, for all three languages, is in [Error handling](/error-handling).

## Wire it into your CLI

**Python (Click)**

```bash
pip install levelfour click
```

```python title="cli.py"
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.costs.get_provider_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()
```

> **Note**
>
> The SDK's `SyncPager` fetches every page as you iterate, so `page_size` sizes each request and does not cap what comes back. You do not need a manual `for page in pages` loop. See [Pagination](/pagination).

**TypeScript (Commander)**

```bash
npm install levelfour commander
```

```typescript title="cli.ts"
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.costs.getProviderSummary({ 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)**

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

The Go sample builds `costs-summary` only. The Python and TypeScript tabs also show `recs list` and the `--provider` option.

```go title="main.go"
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](/pagination).

## Authentication

Your CLI does not need a credential layer of its own. If your team already injects an API key from `~/.acme/credentials` or a secret manager, export it as `LEVELFOUR_API_KEY` from that source and every SDK picks it up. [Authentication](/authentication) covers the key formats and the other ways to pass one.

> **Note**
>
> Export the key from that source before your CLI constructs the client.

## Embed or shell out

| Concern                               | Embed the SDK                          | Shell out to `l4`                          |
| ------------------------------------- | -------------------------------------- | ------------------------------------------ |
| Adds a dependency                     | Yes (PyPI / npm / go get)              | No (a single static binary)                |
| Typed objects                         | Yes (Pydantic / TS types / Go structs) | No (you parse JSON)                        |
| Control over filtering and pagination | Yes, the full SDK surface              | Yes, via `l4` flags and jq                 |
| Works in any language                 | No (Python, TypeScript and Go only)    | Yes (anything that can `exec`)             |
| Adds a binary install step in CI      | No (the package manager handles it)    | Yes (`brew install` or a release download) |
| Keeps PR diffs language-native        | Yes                                    | Mixes shell into your codebase             |
| Staying current                       | Lockfile-controlled                    | `l4` self-checks for updates               |

Embed the SDK when your CLI is in one of those three languages and you want first-class types. Shell out to `l4` when you are scripting in bash or Make, when your CLI is in some other language, or when you do not want the dependency.
