clawdforge/clients/bash/README.md
Kayos cb1d8c2c54 clients/bash: v0.2 multi-turn session subcommands
- cf session new / turn / close / list / get
- --json flag mirrors v0.1 convention
- close is idempotent (exit 0 on already-closed)
- Bearer hygiene preserved (regression guard test)
- tests/test_session.sh: ~18 tests, 44 assertions
- README "Sessions (v0.2)" section

v0.1 subcommands unchanged.

Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
2026-04-29 07:00:40 -07:00

6.6 KiB

cf — bash CLI for clawdforge

Single-file shell client for clawdforge. Wraps curl so you can drive clawdforge from cron jobs, deploy scripts, one-off pipes, or any shell where pulling in a Python/Go runtime is overkill.

Install

sudo install -m 755 cf /usr/local/bin/cf

Or just symlink into ~/bin:

ln -s "$(pwd)/cf" ~/bin/cf

No dependencies beyond curl + standard POSIX tools. jq is optional — used for prettier error output if available, never required.

Configuration

Reads from env or ~/.config/clawdforge/cf.env (env wins on conflict):

Variable Default Purpose
CLAWDFORGE_URL http://192.168.0.5:8800 clawdforge base URL
CLAWDFORGE_TOKEN per-app bearer; required for /run, /files
CLAWDFORGE_ADMIN_TOKEN bootstrap admin bearer; required for cf admin *

Example cf.env:

CLAWDFORGE_URL=http://192.168.0.5:8800
CLAWDFORGE_TOKEN=cf_AbCd...
CLAWDFORGE_ADMIN_TOKEN=...

Commands

cf healthz
cf run "<prompt>"  [--model sonnet] [--system "..."] [--timeout 60] [--files token1,token2]
cf run -                                     # read prompt from stdin (any size)
cf upload <path>   [--ttl 3600]
cf admin token-mint <name>  [--ip-cidrs cidr1,cidr2]
cf admin token-list
cf admin token-revoke <name>

# Sessions (v0.2 — multi-turn)
cf session new                               [--agent claude] [--meta '{"k":"v"}'] [--json]
cf session turn <id> "<prompt>"              [--files tok1,tok2] [--timeout 120] [--json] [--trace path]
cf session turn <id> -                       # read prompt from stdin
cf session get <id>                          [--json]
cf session list                              [--include-closed] [--json]
cf session close <id>                        [--json]

Output is JSON to stdout — pipe to jq for shaping. Errors go to stderr.

Exit codes

Code Meaning
0 ok
1 curl/transport failure
2 usage error (bad/missing args)
3 missing required token
4 HTTP 4xx (auth, not found, bad request)
5 HTTP 5xx (server / claude failure)

Examples

Health check in a cron preamble

cf healthz | jq -e '.claude_present' >/dev/null || { echo "clawdforge not ready"; exit 1; }

One-shot prompt with JSON output

cf run 'Reply with JSON: {"city":"Lake Elsinore","feel":"summer"}' --model sonnet \
  | jq -r '.result.city'

Long prompt via stdin (anything bigger than the OS argv limit)

cat <<'PROMPT' | cf run - --timeout 180
You are a precise recipe parser. Given the following text…
PROMPT

The bash CLI doesn't do anything special for size — it relies on clawdforge's server-side stdin path for prompts > 64KB.

Upload a file then attach it to a run

ft=$(cf upload /tmp/recipe.png --ttl 3600 | jq -r '.file_token')
cf run "extract the recipe from this image as JSON" --files "$ft" --timeout 120

Mint a per-app token

cf admin token-mint johnny5 --ip-cidrs 172.24.0.0/16
# → {"name":"johnny5","token":"cf_...","ip_cidrs":["172.24.0.0/16"]}

Sessions (v0.2)

Multi-turn sessions backed by ACPX on the server. Use these when you need context to persist across turns — "build this with me step by step", "now try X… now try Y", etc. For one-shot prompts, stick with cf run.

Sessions don't autoclose — call cf session close <id> when done. They time out server-side after 1h idle anyway.

The 5 subcommands

Command Purpose
cf session new Create a session. Prints just the UUID to stdout for piping.
cf session turn <id> "<prompt>" Send a prompt. Default output = concatenated text events.
cf session get <id> Print the session's state JSON (turn count, timestamps, closed_at).
cf session list List the calling token's sessions as a tab-separated table (or JSON with --json).
cf session close <id> Soft-close the session. Idempotent — exits 0 even on already-closed.

The --json flag mirrors the v0.1 convention: when present, every subcommand prints the full server response JSON to stdout instead of its default human-readable shape.

End-to-end: build a feature across 3 turns

sid=$(cf session new --agent claude --meta '{"task":"add-cache-layer"}')

cf session turn "$sid" "Read src/store.go and tell me where caching should live."

cf session turn "$sid" "Now write a Redis-backed cache wrapping the existing Get/Put."

cf session turn "$sid" "Add tests for cache hit / miss / TTL expiry."

cf session close "$sid"
# → "closed"

Default vs JSON output

cf session turn defaults to concatenating just the text events to stdout (matches cf run style). Use --json for the full event batch including thinking and tool_call frames, plus stop_reason and duration_ms:

cf session turn "$sid" "summarize" --json | jq '.events | map(.type) | unique'
# → ["text","thinking","tool_call"]

--trace path writes the full JSON response to a file regardless of --json. Useful when you want the human text on stdout AND a structured audit trail in the same call:

cf session turn "$sid" "explain the PR" --trace ./traces/turn-$(date +%s).json

Listing and inspecting

cf session list
# SESSION_ID                                 AGENT   TURNS  CREATED_AT   CLOSED_AT
# 9f1c...                                    claude  3      1714000000   -
# c71e...                                    claude  1      1714003600   1714005400

cf session list --json | jq '.sessions[] | select(.closed_at == null) | .session_id'

cf session get "$sid" | jq '.turn_count'

Idempotent close in cleanup paths

cf session close exits 0 whether the session was already closed or not, so it's safe to call from trap handlers:

sid=$(cf session new)
trap 'cf session close "$sid" >/dev/null 2>&1 || true' EXIT

cf session turn "$sid" "long-running task..."
# trap fires on exit; "closed" first time, "already-closed" any subsequent.

Errors

The same exit codes as v0.1 apply:

  • 2 — bad usage (unknown flag, invalid session id, malformed --meta)
  • 3CLAWDFORGE_TOKEN not set
  • 4 — HTTP 4xx (404 for cross-token access — server doesn't leak existence)
  • 5 — HTTP 5xx (acpx pool full, internal failure, etc.)

Bearer tokens never appear in error output, the same hardening as v0.1.

Pattern: bash-only no-jq fallback

If you can't install jq, parse with read:

read -r status size < <(cf upload "$path" | grep -o '"file_token":"[^"]*"\|"size":[0-9]*' | tr '\n' ' ')

But really, install jq.

License

Same as the rest of clawdforge (MIT).