---
title: Build a small CLI with ScaiFlux
path: tutorials/build-a-cli
status: published
---

# Tutorial: build a small CLI with ScaiFlux

A 15-minute walkthrough that uses ScaiFlux to scaffold a real,
working Python CLI — from empty directory to passing tests — by
asking the agent to do the work instead of writing the code
yourself. The point isn't the CLI; it's seeing the agentic loop in
practice (model → tool call → result → next decision) so you have a
mental model for everything else you'll do with ScaiFlux later.

You should have ScaiFlux installed and configured. If not, do
[install](../install) and [quickstart](../quickstart) first.

---

## What you'll build

A tiny `wordcount` utility that takes a path (file or directory) and
prints lines / words / characters per file plus a total, with an
optional `--top N` flag to show only the largest files. Roughly 80
lines of Python plus tests.

Why this task? It's small enough to finish in one session, big
enough to exercise everything that makes ScaiFlux agentic:
file creation, running tests, iterating on a bug, refactoring on
request.

## Setup

```bash
mkdir ~/projects/wordcount && cd ~/projects/wordcount
scaiflux init       # scaffolds .scaiflux.json + SCAIFLUX.md + .gitignore
scaiflux            # drops into the REPL
```

Inside the REPL you'll see a banner with the resolved model, the
permission mode (`workspace-write` by default after `init`), and a
hint about `/help`.

---

## Turn 1 — scaffold the CLI

Type this and press Enter:

```text
▸ Write a small CLI tool in cli.py that takes a path argument
  (file or directory) and prints lines/words/chars per file plus
  totals. Use only the Python standard library. Also write tests
  in test_cli.py that I can run with `python -m unittest`.
```

You'll see ScaiFlux:

1. Stream a short plan in plain prose.
2. Invoke `write_file(path="cli.py", ...)` — you'll see a
   `● write_file(...)` row appear, then `✓` when it lands.
3. Invoke `write_file(path="test_cli.py", ...)` the same way.
4. Wrap up with a one-paragraph summary of what got written.

**No "here's the code, paste it into a file"** — the agent did the
filesystem operations itself. That's the point.

Verify:

```bash
ls
# cli.py  test_cli.py  .scaiflux.json  SCAIFLUX.md  .gitignore
python cli.py /etc/hostname
```

---

## Turn 2 — let the agent run the tests

This is the moment that makes ScaiFlux feel different. Type:

```text
▸ Run the tests and tell me if they pass. If anything's broken,
  fix it and re-run until everything's green.
```

You'll watch ScaiFlux:

1. Call `bash(command="python -m unittest test_cli.py -v")`.
2. Read the output. If a test failed, it'll diagnose, call
   `edit_file(...)` on the offending function, re-run, and repeat.
3. Stop when every test passes.

Tool calls roll past in the terminal in real time:

```text
● bash(python -m unittest test_cli.py -v)  1.1s ✗
  FAIL: test_handles_empty_file …
● edit_file(path=cli.py, old_string=…, new_string=…)  ✓ (42ms)
● bash(python -m unittest test_cli.py -v)  0.9s ✓
  OK
```

If the model's first attempt was perfect, you might see just one
`bash` call and a "tests pass" reply. Either way, **you didn't
copy/paste anything** — the iteration loop ran inside ScaiFlux.

---

## Turn 3 — add a feature

Once the baseline is green, ask for the `--top N` flag:

```text
▸ Add a --top N option that, when set, only prints the N largest
  files by word count. Update the tests to cover both the
  unflagged behaviour and the new flag. Run the tests after.
```

This turn exercises **reading existing code before editing it**.
ScaiFlux will:

1. Call `read_file(path="cli.py")` to see what's there.
2. Call `read_file(path="test_cli.py")` similarly.
3. Call `edit_file(...)` twice — one per file.
4. Call `bash(...)` to run the tests.

If the tests don't pass first try, repeat the loop from Turn 2.

---

## Turn 4 — interrupt mid-turn (optional)

Type:

```text
▸ Refactor cli.py to be more idiomatic — use pathlib, type
  hints, and split the file-walking logic into a helper function.
```

About a second in, hit **Esc**. The model stops mid-stream, you'll
see `— interrupted` in the footer, and you're back at a prompt. The
partial edits (if any) are visible — ScaiFlux doesn't undo work
that already landed; it just halts the turn cleanly.

This is the main "I changed my mind" affordance. The rest of the
session continues normally; you can immediately ask for something
different.

---

## Turn 5 — review and commit

```text
▸ Show me the diff vs. an empty workspace. Then write a
  one-line conventional-commit message describing what was built.
```

ScaiFlux will:

1. Call `bash(command="git init && git add -A && git diff --cached")`
   if there's no git repo yet — or just `git diff` if there is.
2. Read the diff and synthesise a commit message.
3. Print the message but **not** commit (action verbs without
   "run" are read-only by default — see [slash workflow docs](../reference/cli)
   for `/commit run` if you want the actual commit too).

---

## What just happened (the agentic loop)

Every turn followed the same shape:

```mermaid
sequenceDiagram
    autonumber
    participant U as you
    participant SF as ScaiFlux
    participant T as workspace tools
    U->>SF: prompt
    loop until done
        SF->>T: tool call (read / write / edit / bash …)
        T-->>SF: result
        Note over SF: decides next step from result
    end
    SF-->>U: text summary
```

The model isn't generating prose for you to act on; it's generating
**tool calls** that the harness executes against your filesystem and
shell. ScaiFlux is the glue — it serialises the calls, checks
permissions, runs them, feeds results back to the model, and
renders the loop in your terminal. The chat surface is the steering
wheel; the tool surface is where work actually happens.

## Where to go next

- **[CLI reference](../reference/cli)** — every slash command and
  flag, in one place.
- **[REPL walkthrough](../guides/repl)** — narrative tour of the
  interactive surface (session picker, context indicator, ESC,
  Poolnoodle mode, `/save`, …).
- **[Serve protocol](../reference/serve-protocol)** — when you want
  another program (ScaiForge, your own harness) to drive ScaiFlux
  the way you just did manually.
- **Try a harder task.** "Read this 500-line Python file and add
  type hints," "Convert this JSON schema to a Pydantic model,"
  "Find and fix the bug in `parser.py` — the tests fail on
  unicode." The shape stays the same; ScaiFlux scales up with
  permission modes and context-window management.

## Troubleshooting

- **Model asked clarifying questions instead of writing the code.**
  That's surprisingly persistent for some models; you can nudge
  with "just go ahead and write it, you can refactor later" or
  switch to a more action-oriented model via `/model <slug>`.
- **Permission prompt fired in the middle of a turn.** You're in
  `prompt` mode rather than `workspace-write`. `init` defaults to
  workspace-write, but you may have changed it via `/mode`. Run
  `/mode workspace-write` to relax.
- **No `git` for the commit step.** Either install git, or skip
  Turn 5 — it's optional.
- **Tests took multiple iterations to pass.** That's normal and
  shows the loop working. If it stalls (same error twice), the
  task may be too ambiguous for the model — break it into smaller
  sub-asks.
