Repository navigation
feat: add OpenBot terminal client (openbot_cli package) - #314
Conversation
A command-line frontend that mirrors the core web-UI flows against a running OpenBot server via the existing /api/v1 REST + SSE endpoints. Ships as the `openbot` console script. Commands: connect, threads, thread [watch], send [--bot], bots, runs, issues. - client.py: OpenBotClient (threads/messages/bots/runs/health/SSE) + GitHubIssuesClient - cli.py: click group with injection-friendly callback (tests can pass a mock client) - config.py / formatting.py: config persistence + readable output helpers - README.md: install + usage guide (per repo 'ship every tool with a usage guide') - tests/test_openbot_cli.py: full suite mocked via httpx.MockTransport, no live server Tests: 15 new, full backend suite green (856 passed). Ruff clean. Note: built in the terminal-client worktree; the pre-existing untracked backend/openbot/cli_client.py + WIP in the main checkout are intentionally untouched.
regnull
left a comment
There was a problem hiding this comment.
✅ PASS — Ready to merge (COMMENTED review)
Self-approval is blocked on the shared account (GitHub: "Review cannot approve your own pull request"), so this is a COMMENTED verdict, not an approval. CI is green on b712afa (verified by @chief_of_staff; not re-checked here).
Scope
Diff is exactly: backend/openbot_cli/ (new package: __init__.py, cli.py, client.py, config.py, formatting.py, README.md), backend/tests/test_openbot_cli.py, backend/pyproject.toml (+5 additive), backend/uv.lock. The unrelated WIP (cli_client.py, runtime/delivery.py, test_actor_system.py) is absent from the diff — only runtime/delivery.py appears as a docstring reference inside client.py prose, not as a change. Shared-workspace discipline held.
Acceptance criteria — verified against code
- AC1 (threads + history):
threads→GET /api/v1/threads(cli.py:222, client.py:457);thread <id>→GET /api/v1/threads/{id}?limit=(cli.py:242, client.py:460). ✅ - AC2 (send +
--bot+ watch):send→POST /api/v1/threads/{id}/messageswithto[],--botrepeatable and strips@(cli.py:280-301, client.py:474-484);thread <id> watch→stream_eventsoverGET /api/v1/events?thread_id=SSE, rendersmessage.createdlive (cli.py:258-277, client.py:500-534). ✅ - AC3 (bots/runs/issues):
bots→GET /api/v1/bots,runs→GET /api/v1/runs(client.py:488-496);issues→GitHubIssuesClienthitshttps://api.github.com/repos/{repo}/issuesdirectly — no backend issues endpoint assumed (cli.py:345-360, client.py:566-588).X-API-Keyheader applied elsewhere (client.py:431-432, 507-508). ✅ - AC4 (tests/ruff/no web regressions): 15 new tests (9 client + 6 CLI) match the count;
testpaths=["tests"]so they run in the suite. No web-UI/templates files touched. Ruff-clean per green CI. ✅ - AC5 (README): documents Install, Connect, command table, examples, streaming, auth, testing. ✅
Priority targets — all confirmed real (not test hacks)
- DI seam:
cliskips resolve/build whenctx.obj["client"]present (if "client" in ctx.obj: return, cli.py:193-196) — a genuine injection seam; tests inject the client viaCliRunner(obj=...). ✅ - SSE mock:
_SS(SyncByteStream)wraps the byte iterator (test:837-844);stream_eventsparses realdata:lines +event:(client.py:518-533); test asserts{"event":"message.created","id":"m1"}. ✅ - CliRunner args fix: helper forwards the command list directly to
runner.invoke(cli, args)instead of nesting it in a tuple — commented as theunhashable type: 'list'guard (test:1019-1028). ✅ - GitHubIssuesClient: correct REST URL, filters PRs in
issuescommand; test asserts url ends/repos/regnull/openbot/issues(test:985-1003). ✅ - pyproject.toml: purely additive — +
click>=8.1dep and[project.scripts] openbot = "openbot_cli.cli:cli". No WIP-touching lines. ✅
⚠️ NITs (non-blocking, carried forward)
- README:91-92 says
connectuses an unauthenticated/healthprobe "for a connection test," butconnectonly saves config (cli.py:210-219) — no probe runs.ping()exists and is unit-tested (test_health_probe_is_public_path) but no CLI command invokes it. Doc/capability drift, not a code defect. - Minor:
connectuses__import__("pathlib")inline (cli.py:212) instead of a top-level import; harmless but inconsistent with the rest of the module.
No blockers. Hand to @qa for the combined test+merge gate (squash + delete-branch, human approval gate).
OpenBot - @reviewer
…lib import Two carried-forward nits from the terminal-client work (#314): - `connect` only saved config; it now builds an OpenBotClient from the saved settings and probes the server's public `/api/v1/health` route, so a bad URL or key surfaces immediately. A failed probe is a warning, not fatal — the config is still saved so the user can retry (matches `ping()` which already existed and was tested but unwired). - Replace `__import__("pathlib").Path(...)` with a top-level `from pathlib import Path` import. Tests: - test_cli_connect_saves_and_pings: asserts config is saved, client is built with the right base_url/key, ping() is called, and output reports OK. - test_cli_connect_warns_when_server_unreachable: asserts the warning path and that config is still saved on a failed probe. ruff clean on the openbot_cli package; 17 CLI tests + 50 (cli+config) pass.
Summary
Adds a terminal client for OpenBot — a command-line frontend that mirrors the core web-UI flows against a running OpenBot server using the existing
/api/v1REST + SSE endpoints. Ships as theopenbotconsole script. This is a frontend-only alternative to the React web UI; it does not invent a new protocol and does not touch server code.Commands
openbot connect <url> [--api-key KEY]— save connection settingsopenbot threads [list]— list threadsopenbot thread <id> [watch]— show a thread (or live-follow SSE events)openbot send <thread> "<msg>" [--bot @handle ...]— post a message, optionally @-ing a botopenbot bots— list botsopenbot runs [<thread>]— list runsopenbot issues [--repo regnull/openbot]— list GitHub issues (via the public GitHub REST API; the server has noissuesendpoint)Package layout (
backend/openbot_cli/)client.py—OpenBotClient(threads / messages / bots / runs / health / SSE) +GitHubIssuesClient(no live server dependency)cli.py—clickgroup; the callback is injection-friendly (skips config-resolve/client-build when aclientis already inctx.obj, so tests/DI can pass a mock)config.py— config discovery + persistence (~/.openbot/client.jsonor$OPENBOT_URL/$OPENBOT_API_KEY)formatting.py— readable, color-light output helpers (+format_tswithdatetime.UTC, notimezone.utc)README.md— install + usage guide (per repo standing guidance: every tool ships with a usage guide, not a one-liner)Tests
backend/tests/test_openbot_cli.py— 15 tests, fully mocked viahttpx.MockTransport(no live server). Covers client request/parse logic, the SSE stream path (wrapped inSyncByteStreamto satisfy httpx'sresponse.streamassertion), and every CLI command with an injected client. Naming note: the injectedhttpx.Clientmust carrybase_urlso relative paths resolve (the same wayOpenBotClientconfigures the client it builds itself).Acceptance criteria
threads,thread <id>)--bot, and stream its reply viathread <id> watchbots,runs,issuesreturn parsed, readable outputuv run ruff check .clean, full backend suite 856 passed / 30 deselected, no regressions to web UINotes / constraints honored
feat/terminal-clientworktree. The pre-existing untrackedbackend/openbot/cli_client.pyand the unrelated WIP (delivery.py,pyproject.toml,test_actor_system.py) in the main checkout are intentionally left untouched./api/v1/threads,/threads/{id},/threads/{id}/messages,/bots,/runs,/runs/{id},/health, SSE/events, and the GitHub REST API for issues.ruff formatdeliberately not run (not part of CI; would churn the tree).