Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/smallestai/cli/banner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Startup banner for the smallestai CLI.

Concentric rings (left-aligned, gently pulsing) above the SMALLEST AI wordmark,
in the brand blue #3B82F6. The pulse only runs on an interactive TTY; piped,
CI, NO_COLOR, or SMALLESTAI_NO_ANIM output gets a single static frame. Narrow
terminals fall back to a compact text wordmark so nothing wraps.
"""

from __future__ import annotations

import math
import os
import shutil
import sys
import time

_BRAND = (59, 130, 246) # #3B82F6

_WORDMARK = [
r"███████╗███╗ ███╗ █████╗ ██╗ ██╗ ███████╗███████╗████████╗ █████╗ ██╗",
r"██╔════╝████╗ ████║██╔══██╗██║ ██║ ██╔════╝██╔════╝╚══██╔══╝ ██╔══██╗██║",
r"███████╗██╔████╔██║███████║██║ ██║ █████╗ ███████╗ ██║ ███████║██║",
r"╚════██║██║╚██╔╝██║██╔══██║██║ ██║ ██╔══╝ ╚════██║ ██║ ██╔══██║██║",
r"███████║██║ ╚═╝ ██║██║ ██║███████╗███████╗███████╗███████║ ██║ ██║ ██║██║",
r"╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝",
]
_WORDMARK_WIDTH = 83
_INDENT = " "
_MIN_WIDTH = _WORDMARK_WIDTH + 4 # leave a little breathing room

_TAGLINE = "Build, deploy, and run voice agents and speech models."

# Braille rasterisation: 2x4 dots per glyph, mapped to the Unicode braille block.
_BR = [0x1, 0x2, 0x4, 0x40, 0x8, 0x10, 0x20, 0x80]
_OFF = [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3)]
_RING_COLS, _RING_ROWS = 16, 7
_BASE_RADII = (1.0, 0.62, 0.26)

_RESET = "\033[0m"
_HIDE = "\033[?25l"
_SHOW = "\033[?25h"


def _fg(rgb: tuple[int, int, int]) -> str:
return f"\033[38;2;{rgb[0]};{rgb[1]};{rgb[2]}m"


def _rings(radii: tuple[float, ...], thick: float = 0.045) -> list[str]:
cols, rows = _RING_COLS, _RING_ROWS
w, h = cols * 2, rows * 4
cx, cy = (w - 1) / 2, (h - 1) / 2
rr = min(cx, cy)
grid = [[0] * w for _ in range(h)]
for y in range(h):
for x in range(w):
d = math.hypot((x - cx) / rr, (y - cy) / rr)
if any(abs(d - r) < thick for r in radii):
grid[y][x] = 1
lines = []
for r in range(rows):
line = "".join(
chr(0x2800 + sum(_BR[i] for i, (a, b) in enumerate(_OFF) if grid[r * 4 + b][c * 2 + a])) or " "
for c in range(cols)
)
lines.append(line)
return lines


def _pulse_radii(t: float) -> tuple[float, ...]:
s = 0.28 * math.sin(t * 2 * math.pi)
return tuple(min(1.02, r + s) for r in _BASE_RADII)


def _color_enabled() -> bool:
return sys.stdout.isatty() and not os.environ.get("NO_COLOR")


def _animate_enabled() -> bool:
return (
sys.stdout.isatty()
and not os.environ.get("NO_COLOR")
and not os.environ.get("CI")
and not os.environ.get("SMALLESTAI_NO_ANIM")
)


def _term_width() -> int:
try:
return shutil.get_terminal_size((80, 24)).columns
except Exception:
return 80


def print_banner() -> None:
"""Render the rings + wordmark to stdout (animated on an interactive TTY)."""
color = _color_enabled()
blue = _fg(_BRAND) if color else ""
reset = _RESET if color else ""
out = sys.stdout

if _term_width() < _MIN_WIDTH:
out.write(f"\n{_INDENT}{blue}SMALLEST AI{reset}\n")
out.flush()
return

ring_static = _rings(_BASE_RADII)

if _animate_enabled():
out.write(_HIDE)
try:
for line in ring_static:
out.write(f"{_INDENT}{blue}{line}{reset}\n")
fps = 20
for f in range(1, int(1.2 * fps) + 1):
out.write(f"\033[{_RING_ROWS}A")
for line in _rings(_pulse_radii(f / fps)):
out.write(f"\r{_INDENT}{blue}{line}{reset}\033[K\n")
out.flush()
time.sleep(1 / fps)
out.write(f"\033[{_RING_ROWS}A")
for line in ring_static:
out.write(f"\r{_INDENT}{blue}{line}{reset}\033[K\n")
finally:
out.write(_SHOW + reset)
else:
for line in ring_static:
out.write(f"{_INDENT}{blue}{line}{reset}\n")

out.write("\n")
for w in _WORDMARK:
out.write(f"{_INDENT}{blue}{w}{reset}\n")
out.flush()
21 changes: 8 additions & 13 deletions src/smallestai/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from smallestai.cli.agent_crew import initialise_agent_crew_app
from smallestai.cli.agents import initialise_agents_app
from smallestai.cli.auth import initialise_auth_app
from smallestai.cli.banner import print_banner
from smallestai.cli.calls import initialise_calls_app
from smallestai.cli.campaigns import initialise_campaigns_app
from smallestai.cli.lib.atoms import AtomsAPIClient
Expand All @@ -20,29 +21,21 @@

app = typer.Typer(help="SmallestAI CLI", no_args_is_help=False, rich_markup_mode="rich")

_BANNER = r"""[bold magenta]
███████╗███╗ ███╗ █████╗ ██╗ ██╗ ███████╗███████╗████████╗
██╔════╝████╗ ████║██╔══██╗██║ ██║ ██╔════╝██╔════╝╚══██╔══╝
███████╗██╔████╔██║███████║██║ ██║ █████╗ ███████╗ ██║
╚════██║██║╚██╔╝██║██╔══██║██║ ██║ ██╔══╝ ╚════██║ ██║
███████║██║ ╚═╝ ██║██║ ██║███████╗███████╗███████╗███████║ ██║
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝ ╚═╝[/bold magenta]"""

_COMMANDS = [
("agent-crew", "Init, deploy, and manage crew (custom-LLM) voice agents"),
("agents", "Create, inspect, and call voice agents"),
("calls", "Inspect call logs, transcripts, and recordings"),
("campaigns", "Manage outbound calling campaigns"),
("phone-numbers", "Search, rent, and manage phone numbers"),
("waves", "Text-to-speech, speech-to-text, and voices"),
("models", "Text-to-speech, speech-to-text, and voices"),
("mcp", "Set up the Smallest AI MCP server for Cursor / Claude"),
("auth", "Log in and manage credentials"),
]


def _print_welcome() -> None:
console.print(_BANNER)
console.print(" [dim]Build, deploy, and run voice agents and speech models.[/dim]\n")
print_banner()
console.print("\n [dim]Build, deploy, and run voice agents and speech models.[/dim]\n")
table = Table(show_header=False, box=None, padding=(0, 2, 0, 2))
table.add_column(style="bold cyan", no_wrap=True)
table.add_column(style="white")
Expand Down Expand Up @@ -79,8 +72,10 @@ def _root(ctx: typer.Context) -> None:
calls_app = initialise_calls_app(auth_client)
app.add_typer(calls_app, name="calls")

waves_app = initialise_waves_app(auth_client)
app.add_typer(waves_app, name="waves")
models_app = initialise_waves_app(auth_client)
app.add_typer(models_app, name="models")
# Back-compat: keep the old `waves` name working, hidden from help.
app.add_typer(models_app, name="waves", hidden=True)

campaigns_app = initialise_campaigns_app(auth_client)
app.add_typer(campaigns_app, name="campaigns")
Expand Down
4 changes: 2 additions & 2 deletions tests/custom/test_cli_banner_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def test_bare_cli_shows_banner_and_commands_not_error():
result = runner.invoke(app, [])
assert result.exit_code == 0
assert "Missing command" not in result.output
# command list is present
for name in ("agent-crew", "agents", "calls", "waves", "mcp"):
# command list is present (speech/voices group is now "models")
for name in ("agent-crew", "agents", "calls", "models", "mcp"):
assert name in result.output


Expand Down
44 changes: 44 additions & 0 deletions tests/custom/test_cli_banner_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Banner rendering: ring rasterisation, static/compact/animated paths."""

import smallestai.cli.banner as banner


def test_rings_shape_and_width():
lines = banner._rings(banner._BASE_RADII)
assert len(lines) == banner._RING_ROWS
assert all(len(line) == banner._RING_COLS for line in lines)


def test_static_banner_has_wordmark(capsys, monkeypatch):
# Wide, non-animated: single static frame, full wordmark.
monkeypatch.setattr(banner, "_term_width", lambda: 120)
monkeypatch.setattr(banner, "_animate_enabled", lambda: False)
banner.print_banner()
out = capsys.readouterr().out
assert banner._WORDMARK[0] in out


def test_compact_fallback_on_narrow(capsys, monkeypatch):
monkeypatch.setattr(banner, "_term_width", lambda: 60)
banner.print_banner()
out = capsys.readouterr().out
assert "SMALLEST AI" in out
assert banner._WORDMARK[0] not in out # no big figlet when narrow


def test_animated_path_runs(capsys, monkeypatch):
# Force the animated branch but no real sleeping.
monkeypatch.setattr(banner, "_term_width", lambda: 120)
monkeypatch.setattr(banner, "_animate_enabled", lambda: True)
monkeypatch.setattr(banner, "_color_enabled", lambda: True)
monkeypatch.setattr(banner.time, "sleep", lambda _s: None)
banner.print_banner()
out = capsys.readouterr().out
assert banner._WORDMARK[0] in out
assert "\033[?25l" in out and "\033[?25h" in out # cursor hidden then restored


if __name__ == "__main__":
import pytest

pytest.main([__file__, "-v"])
Loading