CLI commands print human-readable output to the terminal via Rich. cli.log is the machine-readable counterpart: every significant CLI invocation writes a structured CSV row to <workspace>/logs/cli.log using the same hiro_commons.log.Logger infrastructure as server.log.
System overview
hirocli command (@app.command)
└─ Logger.get("CLI.<MODULE>").info("hirocli <cmd>", key=val)
│
└─ _CsvRenderer ──────────→ <workspace>/logs/cli.log (CSV file)
│
├─ LogTailTool ──→ admin UI live tail
├─ LogSearchTool ──→ admin UI search / CLI / AI agent
└─ hirocli logs search --source cli
Logging lives in the commands layer (commands/*.py), not the tools layer. This is critical because Tool classes are shared between CLI processes and the in-server AI agent. Logging inside tools would cause double-writes and log contamination when the agent invokes the same tools.
Module-prefix routing
Log events are routed to the correct file by module prefix, configured centrally in Logger.open_log_dir():
| Module prefix | Destination file | Rule |
|---|
CLI.* | cli.log | include_prefix="CLI." |
| Everything else | server.log | exclude_prefix=("CLI.",) |
This means Logger.get("CLI.SERVER").info(...) automatically lands in cli.log — no per-call sink setup needed. In foreground mode (where CLI and server share a process), the routing prevents CLI events from leaking into server.log and vice versa.
Source files
| Layer | File |
|---|
| Logger API + routing | hiro-commons/src/hiro_commons/log.py |
| App-level callback (opens sinks) | hirocli/src/hirocli/commands/app.py |
| Command wrappers (log here) | hirocli/src/hirocli/commands/root.py, channel.py, device.py, workspace.py |
| Log reading tools | hirocli/src/hirocli/tools/logs.py |
| Admin UI page | hirocli/src/hirocli/ui/pages/logs.py |
Relationship to server-logging
cli.log uses the exact same CSV format as server.log:
timestamp,level,module,message,extra
Both are created by Logger.open_log_dir(log_dir) and read by LogTailTool / LogSearchTool. The only difference is the source process: server.log is written by the long-running server process; cli.log is written by short-lived CLI invocations.
See Server logging for the full CSV format spec and Logger API.
Module names reference
Each command group uses a fixed CLI.* module identifier. The CLI. prefix lets you filter all CLI activity in one query, or narrow to a specific group.
| Module | Covers | Source file |
|---|
CLI.SETUP | setup command | commands/root.py |
CLI.SERVER | start, stop, restart, teardown, uninstall commands | commands/root.py |
CLI.CHANNEL | All channel subcommands | commands/channel.py |
CLI.DEVICE | device add, device revoke | commands/device.py |
CLI.WORKSPACE | workspace create, workspace remove, workspace update | commands/workspace.py |
Log events reference
The message field is the CLI command path (e.g. hirocli start). The extra field contains the actual flags and parameters, plus key outcome fields.
Server lifecycle
| Message | Module | Key extras |
|---|
hirocli setup | CLI.SETUP | workspace, gateway_url, http_port, skip_autostart, elevated_task, start_server, device_id, autostart |
hirocli start | CLI.SERVER | workspace, foreground, admin, already_running, pid, http_port |
hirocli stop | CLI.SERVER | workspace, was_running, pid |
hirocli restart | CLI.SERVER | workspace, foreground, admin, was_running, old_pid, new_pid, http_port |
hirocli teardown | CLI.SERVER | workspace, purge, autostart_removed |
hirocli uninstall | CLI.SERVER | workspace, purge, autostart_removed |
Channel management
| Message | Module | Key extras |
|---|
hirocli channel install | CLI.CHANNEL | channel, package, editable |
hirocli channel setup | CLI.CHANNEL | channel, enabled, command |
hirocli channel enable | CLI.CHANNEL | channel |
hirocli channel disable | CLI.CHANNEL | channel |
hirocli channel remove | CLI.CHANNEL | channel, found |
Device management
| Message | Module | Key extras |
|---|
hirocli device add | CLI.DEVICE | ttl_seconds, code_length, expires_at |
hirocli device revoke | CLI.DEVICE | device_id, found |
The pairing code itself is not logged — it is a short-lived secret used for device onboarding.
Workspace management
| Message | Module | Key extras |
|---|
hirocli workspace create | CLI.WORKSPACE | name, path, set_default |
hirocli workspace remove | CLI.WORKSPACE | workspace, purge |
hirocli workspace update | CLI.WORKSPACE | workspace, name, set_default, gateway_url |
Viewing CLI logs
Admin UI
The admin UI log viewer at /logs shows a CLI source chip alongside Server, Channels, and Gateway. The chip appears only when cli.log exists. Toggle it on or off just like any other source.
CLI
# All CLI activity
hirocli logs search --source cli
# Only errors from CLI commands
hirocli logs search --source cli --level ERROR
# Find all setup operations
hirocli logs search --source cli --module CLI.SETUP
# Find when the server was last started
hirocli logs search --source cli "hirocli start"
AI agent
The LogSearchTool and LogTailTool both accept source="cli" (or source="all" to include CLI alongside the other sources). The agent can answer questions like:
"When was the server last restarted?"
"Has the telegram channel been set up?"
"What device IDs have been revoked?"
How to add new log points
Add logging in the command function (@app.command), before calling Tool.execute():
@channel_app.command("my-command")
def my_command(workspace: Optional[str] = ..., flag: bool = False) -> None:
"""My new command."""
Logger.get("CLI.MY_MODULE").info("hirocli my-command", flag=flag)
result = MyTool().execute(workspace=workspace, ...)
console.print(f"[green]Done.[/green]")
Sink setup is handled automatically by Logger.open_log_dir() in the _cli_init callback — no manual sink initialisation needed.
Why log before execute?
- Correct chronological order — the CLI entry appears before any server-side logs the tool may trigger (e.g. server startup).
- No foreground double-logging — prefix routing ensures
CLI.* events go only to cli.log, even when the server adds server.log in the same process.
Two rules
- Never log inside
Tool.execute() — tools are shared between the CLI process and the AI agent running inside the server.
- Always log before the tool call — this ensures correct timestamp ordering and clean separation of log files.
The only exception is commands that create the workspace (e.g. setup, workspace create), where the path doesn’t exist yet — those call Logger.open_log_dir() after the tool, then log.
Follow the module names reference conventions: use CLI.<COMPONENT> in ALL-CAPS, ≤ 12 characters total.