# The kaab CLI

Run jobs, push and lint programs, review proposals and manage schedules and webhooks from a terminal.

`kaab` is a single binary for Linux, macOS and Windows, built on the Go SDK. It runs jobs, keeps
programs in files you can version and review, and answers parked jobs, so a terminal or a CI job
can do what the panel does.

## When to use it

Use the CLI from a shell, a CI pipeline or a cron job on your own machine: to run a program and
save its data, to lint and push program files from a repository, or to approve proposals in bulk.
From application code, use an [SDK](https://docs.kaab.ist/guides/sdks) instead; for building and testing a program
interactively, the [program builder](https://docs.kaab.ist/panel/builder) is quicker.

## Install

With Go 1.24 or later:

```bash
go install github.com/kaabist/kaab-go/cmd/kaab@latest
kaab version
```

Each release is also built as archives for Linux, macOS (`darwin`) and Windows, on `amd64` and
`arm64`: `kaab_<version>_<os>_<arch>.tar.gz` (`.zip` on Windows), each with the binary, its licence
and README, and a `checksums.txt` to verify them against. Unpack one and put `kaab` on your `PATH`.
There is no Homebrew or winget package yet.

Shell completion comes with the binary:

```bash
kaab completion zsh > "${fpath[1]}/_kaab"   # also bash, fish, powershell
```

## Sign in

```bash
kaab auth login            # paste an sk_live_ or sk_test_ key
kaab auth status
```

The key is checked against the API and kept in your operating system's keychain (or, where there
is none, in `~/.config/kaab/credentials.toml`, readable only by you). A profile holds one live and
one test key; `--test` uses the test one, `--profile` picks another profile. `KAAB_API_KEY` and
`KAAB_BASE_URL` override any profile, which is what CI should use.

## Run a job

### curl

```bash
response=$(curl -sS --fail-with-body "${KAAB_BASE_URL:-https://api.kaab.ist/v1}/jobs" \
  -H "Authorization: Bearer $KAAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: wait=30" \
  -d '{
    "url": "'"${SITE_URL:-https://shop.example.com}"'/p/1",
    "extract": {
      "title": "h1",
      "price": { "selector": ".price", "type": "money", "locale": "tr", "currency": "TRY" }
    }
  }')
echo "$response"
```

### TypeScript

```ts
// One call: create the job and hold the request up to 30 s for its result (`Prefer: wait`).
const job = await kaab.jobs.create({
  url: `${site}/p/1`,
  extract: {
    title: "h1",
    price: { selector: ".price", type: "money", locale: "tr", currency: "TRY" },
  },
  wait: 30,
});
if (job.status !== "succeeded") throw new Error(`job ${job.id} is ${job.status}`);
console.log(`${job.id}: ${JSON.stringify(job.data)}`);
```

### Python

```python
# One call: create the job and hold the request up to 30 s for its result (Prefer: wait).
job = kaab.jobs.create(
    url=f"{site}/p/1",
    extract={"title": "h1", "price": {"selector": ".price", "type": "money", "locale": "tr", "currency": "TRY"}},
    wait=30,
)
if job.status != "succeeded":
    raise SystemExit(f"job {job.id} is {job.status}")
print(f"{job.id}: {job.data.to_dict() if job.data else None}")
```

### Go

```go
// One call: create the job and hold the request up to 30 s for its result (Prefer: wait).
body, err := kaab.Body[client.CreateJobJSONBody](map[string]any{
	"url": site + "/p/1",
	"extract": map[string]any{
		"title": "h1",
		"price": map[string]any{"selector": ".price", "type": "money", "locale": "tr", "currency": "TRY"},
	},
})
if err != nil {
	log.Fatal(err)
}
job, err := c.Jobs.Create(ctx, body, kaab.WithWait(30))
if err != nil {
	log.Fatal(err)
}
if job.Status != "succeeded" {
	log.Fatalf("job %s is %s", job.ID, job.Status)
}
fmt.Printf("%s: %v\n", job.ID, *job.Data)
```

### CLI

```bash
data=$(kaab jobs run \
  --url "${SITE_URL:-https://shop.example.com}/p/1" \
  --extract '{"title": "h1", "price": {"selector": ".price", "type": "money", "locale": "tr", "currency": "TRY"}}' \
  --wait 30)
echo "$data"
```

More forms of the same command:

```bash
kaab jobs run --url https://shop.example.com/p/1 --extract @schema.json --wait 30
kaab jobs run --program acme-report@production --input report_id=42 --session acme --wait 60
```

A succeeded job prints its `data` as JSON on stdout and a one-line summary on stderr, so
`kaab jobs run … > data.json` does what it says. `--json` prints the whole envelope instead.

| Exit code | Meaning |
|---|---|
| 0 | succeeded, or still running when `--wait` ran out |
| 1 | the request failed (the API's code, message and request id are printed) |
| 2 | the job failed or was canceled |
| 3 | the job is parked: waiting for an input (`kaab jobs input`) or for attention (`kaab jobs resume`, `kaab open job`) |

## Programs, proposals, schedules, webhooks

```bash
kaab programs lint report.json          # exit 1 on errors
kaab programs push report.json --publish
kaab programs pull acme-report@production -o report.json
kaab programs diff acme-report 3 4
kaab proposals list && kaab proposals approve prp_… --promote
kaab schedules create --program acme-report@production --cron "0 * * * *" --change-detection --field price
kaab webhooks create --url https://example.com/hooks --events 'job.*'
kaab credits && kaab usage --group-by program
```

`kaab <command> --help` describes every flag, and [the CLI reference](https://docs.kaab.ist/reference/cli) lists every
command.

## Worked example

Run a shop's product page, `https://shop.example.com/p/1`, from a terminal and keep only the price:

```bash
export KAAB_API_KEY=sk_test_…
cat > schema.json <<'JSON'
{ "title": "h1", "price": { "selector": ".price", "type": "money", "locale": "tr", "currency": "TRY" } }
JSON
kaab jobs run --url https://shop.example.com/p/1 --extract @schema.json --wait 30 > data.json
echo $?   # 0
```

`data.json` holds the job's `data`: the title `Kulaklık Klasik 101` and the price
`{ "amount": 1337.4, "currency": "TRY" }`.
In a CI step, branch on the exit code: `2` means the job failed, `3` that it parked and somebody has
to answer it.

## Pitfalls

- **A live key in CI by accident.** `KAAB_API_KEY` wins over every profile; give CI an `sk_test_`
  key for checks and a narrowly scoped live key only where it does real work.
- **Treating exit code 0 as "done".** It also means the job was still running when `--wait` ran
  out. Use `--json` and read `status` when the difference matters.
- **Forgetting `--publish`.** `kaab programs push` without it saves a draft version that no alias
  runs.
- **Shell globbing in `--events 'job.*'`.** Quote event patterns.

## Reference

- [`kaab`](https://docs.kaab.ist/reference/cli/kaab), [`kaab jobs run`](https://docs.kaab.ist/reference/cli/kaab-jobs-run),
  [`kaab programs push`](https://docs.kaab.ist/reference/cli/kaab-programs-push), [`kaab auth login`](https://docs.kaab.ist/reference/cli/kaab-auth-login)
- [`kaab proposals approve`](https://docs.kaab.ist/reference/cli/kaab-proposals-approve),
  [`kaab schedules create`](https://docs.kaab.ist/reference/cli/kaab-schedules-create),
  [`kaab webhooks create`](https://docs.kaab.ist/reference/cli/kaab-webhooks-create)
