Improve agent CLI chat UX with markdown output and clearer interaction feedback

This commit is contained in:
Chris Alexander 2026-02-08 20:50:31 +00:00
parent 8af98004b3
commit 0a2d557268
No known key found for this signature in database

View File

@ -10,7 +10,10 @@ import sys
import typer import typer
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.table import Table from rich.table import Table
from rich.text import Text
from nanobot import __version__, __logo__ from nanobot import __version__, __logo__
@ -21,6 +24,30 @@ app = typer.Typer(
) )
console = Console() console = Console()
EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"}
def _print_agent_response(response: str, render_markdown: bool) -> None:
"""Render assistant response with consistent terminal styling."""
content = response or ""
body = Markdown(content) if render_markdown else Text(content)
console.print()
console.print(
Panel(
body,
title=f"{__logo__} Nanobot",
title_align="left",
border_style="cyan",
padding=(0, 1),
)
)
console.print()
def _is_exit_command(command: str) -> bool:
"""Return True when input should end interactive chat."""
return command.lower() in EXIT_COMMANDS
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Lightweight CLI input: readline for arrow keys / history, termios for flush # Lightweight CLI input: readline for arrow keys / history, termios for flush
@ -44,6 +71,7 @@ def _flush_pending_tty_input() -> None:
try: try:
import termios import termios
termios.tcflush(fd, termios.TCIFLUSH) termios.tcflush(fd, termios.TCIFLUSH)
return return
except Exception: except Exception:
@ -75,6 +103,7 @@ def _restore_terminal() -> None:
return return
try: try:
import termios import termios
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, _SAVED_TERM_ATTRS) termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, _SAVED_TERM_ATTRS)
except Exception: except Exception:
pass pass
@ -87,6 +116,7 @@ def _enable_line_editing() -> None:
# Save terminal state before readline touches it # Save terminal state before readline touches it
try: try:
import termios import termios
_SAVED_TERM_ATTRS = termios.tcgetattr(sys.stdin.fileno()) _SAVED_TERM_ATTRS = termios.tcgetattr(sys.stdin.fileno())
except Exception: except Exception:
pass pass
@ -148,9 +178,7 @@ def version_callback(value: bool):
@app.callback() @app.callback()
def main( def main(
version: bool = typer.Option( version: bool = typer.Option(None, "--version", "-v", callback=version_callback, is_eager=True),
None, "--version", "-v", callback=version_callback, is_eager=True
),
): ):
"""nanobot - Personal AI Assistant.""" """nanobot - Personal AI Assistant."""
pass pass
@ -167,34 +195,34 @@ def onboard():
from nanobot.config.loader import get_config_path, save_config from nanobot.config.loader import get_config_path, save_config
from nanobot.config.schema import Config from nanobot.config.schema import Config
from nanobot.utils.helpers import get_workspace_path from nanobot.utils.helpers import get_workspace_path
config_path = get_config_path() config_path = get_config_path()
if config_path.exists(): if config_path.exists():
console.print(f"[yellow]Config already exists at {config_path}[/yellow]") console.print(f"[yellow]Config already exists at {config_path}[/yellow]")
if not typer.confirm("Overwrite?"): if not typer.confirm("Overwrite?"):
raise typer.Exit() raise typer.Exit()
# Create default config # Create default config
config = Config() config = Config()
save_config(config) save_config(config)
console.print(f"[green]✓[/green] Created config at {config_path}") console.print(f"[green]✓[/green] Created config at {config_path}")
# Create workspace # Create workspace
workspace = get_workspace_path() workspace = get_workspace_path()
console.print(f"[green]✓[/green] Created workspace at {workspace}") console.print(f"[green]✓[/green] Created workspace at {workspace}")
# Create default bootstrap files # Create default bootstrap files
_create_workspace_templates(workspace) _create_workspace_templates(workspace)
console.print(f"\n{__logo__} nanobot is ready!") console.print(f"\n{__logo__} nanobot is ready!")
console.print("\nNext steps:") console.print("\nNext steps:")
console.print(" 1. Add your API key to [cyan]~/.nanobot/config.json[/cyan]") console.print(" 1. Add your API key to [cyan]~/.nanobot/config.json[/cyan]")
console.print(" Get one at: https://openrouter.ai/keys") console.print(" Get one at: https://openrouter.ai/keys")
console.print(" 2. Chat: [cyan]nanobot agent -m \"Hello!\"[/cyan]") console.print(' 2. Chat: [cyan]nanobot agent -m "Hello!"[/cyan]')
console.print("\n[dim]Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot#-chat-apps[/dim]") console.print(
"\n[dim]Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot#-chat-apps[/dim]"
)
def _create_workspace_templates(workspace: Path): def _create_workspace_templates(workspace: Path):
@ -238,13 +266,13 @@ Information about the user goes here.
- Language: (your preferred language) - Language: (your preferred language)
""", """,
} }
for filename, content in templates.items(): for filename, content in templates.items():
file_path = workspace / filename file_path = workspace / filename
if not file_path.exists(): if not file_path.exists():
file_path.write_text(content) file_path.write_text(content)
console.print(f" [dim]Created {filename}[/dim]") console.print(f" [dim]Created {filename}[/dim]")
# Create memory directory and MEMORY.md # Create memory directory and MEMORY.md
memory_dir = workspace / "memory" memory_dir = workspace / "memory"
memory_dir.mkdir(exist_ok=True) memory_dir.mkdir(exist_ok=True)
@ -272,6 +300,7 @@ This file stores important information that should persist across sessions.
def _make_provider(config): def _make_provider(config):
"""Create LiteLLMProvider from config. Exits if no API key found.""" """Create LiteLLMProvider from config. Exits if no API key found."""
from nanobot.providers.litellm_provider import LiteLLMProvider from nanobot.providers.litellm_provider import LiteLLMProvider
p = config.get_provider() p = config.get_provider()
model = config.agents.defaults.model model = config.agents.defaults.model
if not (p and p.api_key) and not model.startswith("bedrock/"): if not (p and p.api_key) and not model.startswith("bedrock/"):
@ -306,22 +335,23 @@ def gateway(
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
from nanobot.cron.types import CronJob from nanobot.cron.types import CronJob
from nanobot.heartbeat.service import HeartbeatService from nanobot.heartbeat.service import HeartbeatService
if verbose: if verbose:
import logging import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
console.print(f"{__logo__} Starting nanobot gateway on port {port}...") console.print(f"{__logo__} Starting nanobot gateway on port {port}...")
config = load_config() config = load_config()
bus = MessageBus() bus = MessageBus()
provider = _make_provider(config) provider = _make_provider(config)
session_manager = SessionManager(config.workspace_path) session_manager = SessionManager(config.workspace_path)
# Create cron service first (callback set after agent creation) # Create cron service first (callback set after agent creation)
cron_store_path = get_data_dir() / "cron" / "jobs.json" cron_store_path = get_data_dir() / "cron" / "jobs.json"
cron = CronService(cron_store_path) cron = CronService(cron_store_path)
# Create agent with cron service # Create agent with cron service
agent = AgentLoop( agent = AgentLoop(
bus=bus, bus=bus,
@ -335,7 +365,7 @@ def gateway(
restrict_to_workspace=config.tools.restrict_to_workspace, restrict_to_workspace=config.tools.restrict_to_workspace,
session_manager=session_manager, session_manager=session_manager,
) )
# Set cron callback (needs agent) # Set cron callback (needs agent)
async def on_cron_job(job: CronJob) -> str | None: async def on_cron_job(job: CronJob) -> str | None:
"""Execute a cron job through the agent.""" """Execute a cron job through the agent."""
@ -347,40 +377,44 @@ def gateway(
) )
if job.payload.deliver and job.payload.to: if job.payload.deliver and job.payload.to:
from nanobot.bus.events import OutboundMessage from nanobot.bus.events import OutboundMessage
await bus.publish_outbound(OutboundMessage(
channel=job.payload.channel or "cli", await bus.publish_outbound(
chat_id=job.payload.to, OutboundMessage(
content=response or "" channel=job.payload.channel or "cli",
)) chat_id=job.payload.to,
content=response or "",
)
)
return response return response
cron.on_job = on_cron_job cron.on_job = on_cron_job
# Create heartbeat service # Create heartbeat service
async def on_heartbeat(prompt: str) -> str: async def on_heartbeat(prompt: str) -> str:
"""Execute heartbeat through the agent.""" """Execute heartbeat through the agent."""
return await agent.process_direct(prompt, session_key="heartbeat") return await agent.process_direct(prompt, session_key="heartbeat")
heartbeat = HeartbeatService( heartbeat = HeartbeatService(
workspace=config.workspace_path, workspace=config.workspace_path,
on_heartbeat=on_heartbeat, on_heartbeat=on_heartbeat,
interval_s=30 * 60, # 30 minutes interval_s=30 * 60, # 30 minutes
enabled=True enabled=True,
) )
# Create channel manager # Create channel manager
channels = ChannelManager(config, bus, session_manager=session_manager) channels = ChannelManager(config, bus, session_manager=session_manager)
if channels.enabled_channels: if channels.enabled_channels:
console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}") console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}")
else: else:
console.print("[yellow]Warning: No channels enabled[/yellow]") console.print("[yellow]Warning: No channels enabled[/yellow]")
cron_status = cron.status() cron_status = cron.status()
if cron_status["jobs"] > 0: if cron_status["jobs"] > 0:
console.print(f"[green]✓[/green] Cron: {cron_status['jobs']} scheduled jobs") console.print(f"[green]✓[/green] Cron: {cron_status['jobs']} scheduled jobs")
console.print(f"[green]✓[/green] Heartbeat: every 30m") console.print(f"[green]✓[/green] Heartbeat: every 30m")
async def run(): async def run():
try: try:
await cron.start() await cron.start()
@ -395,12 +429,10 @@ def gateway(
cron.stop() cron.stop()
agent.stop() agent.stop()
await channels.stop_all() await channels.stop_all()
asyncio.run(run()) asyncio.run(run())
# ============================================================================ # ============================================================================
# Agent Commands # Agent Commands
# ============================================================================ # ============================================================================
@ -410,17 +442,29 @@ def gateway(
def agent( def agent(
message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"), message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"), session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"),
markdown: bool = typer.Option(
True, "--markdown/--no-markdown", help="Render assistant output as Markdown"
),
logs: bool = typer.Option(
False, "--logs/--no-logs", help="Show nanobot runtime logs during chat"
),
): ):
"""Interact with the agent directly.""" """Interact with the agent directly."""
from nanobot.config.loader import load_config from nanobot.config.loader import load_config
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.agent.loop import AgentLoop from nanobot.agent.loop import AgentLoop
from loguru import logger
config = load_config() config = load_config()
bus = MessageBus() bus = MessageBus()
provider = _make_provider(config) provider = _make_provider(config)
if logs:
logger.enable("nanobot")
else:
logger.disable("nanobot")
agent_loop = AgentLoop( agent_loop = AgentLoop(
bus=bus, bus=bus,
provider=provider, provider=provider,
@ -429,13 +473,14 @@ def agent(
exec_config=config.tools.exec, exec_config=config.tools.exec,
restrict_to_workspace=config.tools.restrict_to_workspace, restrict_to_workspace=config.tools.restrict_to_workspace,
) )
if message: if message:
# Single message mode # Single message mode
async def run_once(): async def run_once():
response = await agent_loop.process_direct(message, session_id) with console.status("[dim]Nanobot is thinking...[/dim]", spinner="dots"):
console.print(f"\n{__logo__} {response}") response = await agent_loop.process_direct(message, session_id)
_print_agent_response(response, render_markdown=markdown)
asyncio.run(run_once()) asyncio.run(run_once())
else: else:
# Interactive mode # Interactive mode
@ -451,23 +496,32 @@ def agent(
os._exit(0) os._exit(0)
signal.signal(signal.SIGINT, _exit_on_sigint) signal.signal(signal.SIGINT, _exit_on_sigint)
async def run_interactive(): async def run_interactive():
while True: while True:
try: try:
_flush_pending_tty_input() _flush_pending_tty_input()
user_input = await _read_interactive_input_async() user_input = await _read_interactive_input_async()
if not user_input.strip(): command = user_input.strip()
if not command:
continue continue
response = await agent_loop.process_direct(user_input, session_id) if _is_exit_command(command):
console.print(f"\n{__logo__} {response}\n") console.print("\nGoodbye!")
break
with console.status("[dim]Nanobot is thinking...[/dim]", spinner="dots"):
response = await agent_loop.process_direct(user_input, session_id)
_print_agent_response(response, render_markdown=markdown)
except KeyboardInterrupt: except KeyboardInterrupt:
_save_history() _save_history()
_restore_terminal() _restore_terminal()
console.print("\nGoodbye!") console.print("\nGoodbye!")
break break
except EOFError:
console.print("\nGoodbye!")
break
asyncio.run(run_interactive()) asyncio.run(run_interactive())
@ -494,27 +548,15 @@ def channels_status():
# WhatsApp # WhatsApp
wa = config.channels.whatsapp wa = config.channels.whatsapp
table.add_row( table.add_row("WhatsApp", "" if wa.enabled else "", wa.bridge_url)
"WhatsApp",
"" if wa.enabled else "",
wa.bridge_url
)
dc = config.channels.discord dc = config.channels.discord
table.add_row( table.add_row("Discord", "" if dc.enabled else "", dc.gateway_url)
"Discord",
"" if dc.enabled else "",
dc.gateway_url
)
# Telegram # Telegram
tg = config.channels.telegram tg = config.channels.telegram
tg_config = f"token: {tg.token[:10]}..." if tg.token else "[dim]not configured[/dim]" tg_config = f"token: {tg.token[:10]}..." if tg.token else "[dim]not configured[/dim]"
table.add_row( table.add_row("Telegram", "" if tg.enabled else "", tg_config)
"Telegram",
"" if tg.enabled else "",
tg_config
)
console.print(table) console.print(table)
@ -523,57 +565,57 @@ def _get_bridge_dir() -> Path:
"""Get the bridge directory, setting it up if needed.""" """Get the bridge directory, setting it up if needed."""
import shutil import shutil
import subprocess import subprocess
# User's bridge location # User's bridge location
user_bridge = Path.home() / ".nanobot" / "bridge" user_bridge = Path.home() / ".nanobot" / "bridge"
# Check if already built # Check if already built
if (user_bridge / "dist" / "index.js").exists(): if (user_bridge / "dist" / "index.js").exists():
return user_bridge return user_bridge
# Check for npm # Check for npm
if not shutil.which("npm"): if not shutil.which("npm"):
console.print("[red]npm not found. Please install Node.js >= 18.[/red]") console.print("[red]npm not found. Please install Node.js >= 18.[/red]")
raise typer.Exit(1) raise typer.Exit(1)
# Find source bridge: first check package data, then source dir # Find source bridge: first check package data, then source dir
pkg_bridge = Path(__file__).parent.parent / "bridge" # nanobot/bridge (installed) pkg_bridge = Path(__file__).parent.parent / "bridge" # nanobot/bridge (installed)
src_bridge = Path(__file__).parent.parent.parent / "bridge" # repo root/bridge (dev) src_bridge = Path(__file__).parent.parent.parent / "bridge" # repo root/bridge (dev)
source = None source = None
if (pkg_bridge / "package.json").exists(): if (pkg_bridge / "package.json").exists():
source = pkg_bridge source = pkg_bridge
elif (src_bridge / "package.json").exists(): elif (src_bridge / "package.json").exists():
source = src_bridge source = src_bridge
if not source: if not source:
console.print("[red]Bridge source not found.[/red]") console.print("[red]Bridge source not found.[/red]")
console.print("Try reinstalling: pip install --force-reinstall nanobot") console.print("Try reinstalling: pip install --force-reinstall nanobot")
raise typer.Exit(1) raise typer.Exit(1)
console.print(f"{__logo__} Setting up bridge...") console.print(f"{__logo__} Setting up bridge...")
# Copy to user directory # Copy to user directory
user_bridge.parent.mkdir(parents=True, exist_ok=True) user_bridge.parent.mkdir(parents=True, exist_ok=True)
if user_bridge.exists(): if user_bridge.exists():
shutil.rmtree(user_bridge) shutil.rmtree(user_bridge)
shutil.copytree(source, user_bridge, ignore=shutil.ignore_patterns("node_modules", "dist")) shutil.copytree(source, user_bridge, ignore=shutil.ignore_patterns("node_modules", "dist"))
# Install and build # Install and build
try: try:
console.print(" Installing dependencies...") console.print(" Installing dependencies...")
subprocess.run(["npm", "install"], cwd=user_bridge, check=True, capture_output=True) subprocess.run(["npm", "install"], cwd=user_bridge, check=True, capture_output=True)
console.print(" Building...") console.print(" Building...")
subprocess.run(["npm", "run", "build"], cwd=user_bridge, check=True, capture_output=True) subprocess.run(["npm", "run", "build"], cwd=user_bridge, check=True, capture_output=True)
console.print("[green]✓[/green] Bridge ready\n") console.print("[green]✓[/green] Bridge ready\n")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
console.print(f"[red]Build failed: {e}[/red]") console.print(f"[red]Build failed: {e}[/red]")
if e.stderr: if e.stderr:
console.print(f"[dim]{e.stderr.decode()[:500]}[/dim]") console.print(f"[dim]{e.stderr.decode()[:500]}[/dim]")
raise typer.Exit(1) raise typer.Exit(1)
return user_bridge return user_bridge
@ -581,12 +623,12 @@ def _get_bridge_dir() -> Path:
def channels_login(): def channels_login():
"""Link device via QR code.""" """Link device via QR code."""
import subprocess import subprocess
bridge_dir = _get_bridge_dir() bridge_dir = _get_bridge_dir()
console.print(f"{__logo__} Starting bridge...") console.print(f"{__logo__} Starting bridge...")
console.print("Scan the QR code to connect.\n") console.print("Scan the QR code to connect.\n")
try: try:
subprocess.run(["npm", "start"], cwd=bridge_dir, check=True) subprocess.run(["npm", "start"], cwd=bridge_dir, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -610,24 +652,25 @@ def cron_list(
"""List scheduled jobs.""" """List scheduled jobs."""
from nanobot.config.loader import get_data_dir from nanobot.config.loader import get_data_dir
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
store_path = get_data_dir() / "cron" / "jobs.json" store_path = get_data_dir() / "cron" / "jobs.json"
service = CronService(store_path) service = CronService(store_path)
jobs = service.list_jobs(include_disabled=all) jobs = service.list_jobs(include_disabled=all)
if not jobs: if not jobs:
console.print("No scheduled jobs.") console.print("No scheduled jobs.")
return return
table = Table(title="Scheduled Jobs") table = Table(title="Scheduled Jobs")
table.add_column("ID", style="cyan") table.add_column("ID", style="cyan")
table.add_column("Name") table.add_column("Name")
table.add_column("Schedule") table.add_column("Schedule")
table.add_column("Status") table.add_column("Status")
table.add_column("Next Run") table.add_column("Next Run")
import time import time
for job in jobs: for job in jobs:
# Format schedule # Format schedule
if job.schedule.kind == "every": if job.schedule.kind == "every":
@ -636,17 +679,19 @@ def cron_list(
sched = job.schedule.expr or "" sched = job.schedule.expr or ""
else: else:
sched = "one-time" sched = "one-time"
# Format next run # Format next run
next_run = "" next_run = ""
if job.state.next_run_at_ms: if job.state.next_run_at_ms:
next_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(job.state.next_run_at_ms / 1000)) next_time = time.strftime(
"%Y-%m-%d %H:%M", time.localtime(job.state.next_run_at_ms / 1000)
)
next_run = next_time next_run = next_time
status = "[green]enabled[/green]" if job.enabled else "[dim]disabled[/dim]" status = "[green]enabled[/green]" if job.enabled else "[dim]disabled[/dim]"
table.add_row(job.id, job.name, sched, status, next_run) table.add_row(job.id, job.name, sched, status, next_run)
console.print(table) console.print(table)
@ -659,13 +704,15 @@ def cron_add(
at: str = typer.Option(None, "--at", help="Run once at time (ISO format)"), at: str = typer.Option(None, "--at", help="Run once at time (ISO format)"),
deliver: bool = typer.Option(False, "--deliver", "-d", help="Deliver response to channel"), deliver: bool = typer.Option(False, "--deliver", "-d", help="Deliver response to channel"),
to: str = typer.Option(None, "--to", help="Recipient for delivery"), to: str = typer.Option(None, "--to", help="Recipient for delivery"),
channel: str = typer.Option(None, "--channel", help="Channel for delivery (e.g. 'telegram', 'whatsapp')"), channel: str = typer.Option(
None, "--channel", help="Channel for delivery (e.g. 'telegram', 'whatsapp')"
),
): ):
"""Add a scheduled job.""" """Add a scheduled job."""
from nanobot.config.loader import get_data_dir from nanobot.config.loader import get_data_dir
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
from nanobot.cron.types import CronSchedule from nanobot.cron.types import CronSchedule
# Determine schedule type # Determine schedule type
if every: if every:
schedule = CronSchedule(kind="every", every_ms=every * 1000) schedule = CronSchedule(kind="every", every_ms=every * 1000)
@ -673,15 +720,16 @@ def cron_add(
schedule = CronSchedule(kind="cron", expr=cron_expr) schedule = CronSchedule(kind="cron", expr=cron_expr)
elif at: elif at:
import datetime import datetime
dt = datetime.datetime.fromisoformat(at) dt = datetime.datetime.fromisoformat(at)
schedule = CronSchedule(kind="at", at_ms=int(dt.timestamp() * 1000)) schedule = CronSchedule(kind="at", at_ms=int(dt.timestamp() * 1000))
else: else:
console.print("[red]Error: Must specify --every, --cron, or --at[/red]") console.print("[red]Error: Must specify --every, --cron, or --at[/red]")
raise typer.Exit(1) raise typer.Exit(1)
store_path = get_data_dir() / "cron" / "jobs.json" store_path = get_data_dir() / "cron" / "jobs.json"
service = CronService(store_path) service = CronService(store_path)
job = service.add_job( job = service.add_job(
name=name, name=name,
schedule=schedule, schedule=schedule,
@ -690,7 +738,7 @@ def cron_add(
to=to, to=to,
channel=channel, channel=channel,
) )
console.print(f"[green]✓[/green] Added job '{job.name}' ({job.id})") console.print(f"[green]✓[/green] Added job '{job.name}' ({job.id})")
@ -701,10 +749,10 @@ def cron_remove(
"""Remove a scheduled job.""" """Remove a scheduled job."""
from nanobot.config.loader import get_data_dir from nanobot.config.loader import get_data_dir
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
store_path = get_data_dir() / "cron" / "jobs.json" store_path = get_data_dir() / "cron" / "jobs.json"
service = CronService(store_path) service = CronService(store_path)
if service.remove_job(job_id): if service.remove_job(job_id):
console.print(f"[green]✓[/green] Removed job {job_id}") console.print(f"[green]✓[/green] Removed job {job_id}")
else: else:
@ -719,10 +767,10 @@ def cron_enable(
"""Enable or disable a job.""" """Enable or disable a job."""
from nanobot.config.loader import get_data_dir from nanobot.config.loader import get_data_dir
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
store_path = get_data_dir() / "cron" / "jobs.json" store_path = get_data_dir() / "cron" / "jobs.json"
service = CronService(store_path) service = CronService(store_path)
job = service.enable_job(job_id, enabled=not disable) job = service.enable_job(job_id, enabled=not disable)
if job: if job:
status = "disabled" if disable else "enabled" status = "disabled" if disable else "enabled"
@ -739,13 +787,13 @@ def cron_run(
"""Manually run a job.""" """Manually run a job."""
from nanobot.config.loader import get_data_dir from nanobot.config.loader import get_data_dir
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
store_path = get_data_dir() / "cron" / "jobs.json" store_path = get_data_dir() / "cron" / "jobs.json"
service = CronService(store_path) service = CronService(store_path)
async def run(): async def run():
return await service.run_job(job_id, force=force) return await service.run_job(job_id, force=force)
if asyncio.run(run()): if asyncio.run(run()):
console.print(f"[green]✓[/green] Job executed") console.print(f"[green]✓[/green] Job executed")
else: else:
@ -768,14 +816,18 @@ def status():
console.print(f"{__logo__} nanobot Status\n") console.print(f"{__logo__} nanobot Status\n")
console.print(f"Config: {config_path} {'[green]✓[/green]' if config_path.exists() else '[red]✗[/red]'}") console.print(
console.print(f"Workspace: {workspace} {'[green]✓[/green]' if workspace.exists() else '[red]✗[/red]'}") f"Config: {config_path} {'[green]✓[/green]' if config_path.exists() else '[red]✗[/red]'}"
)
console.print(
f"Workspace: {workspace} {'[green]✓[/green]' if workspace.exists() else '[red]✗[/red]'}"
)
if config_path.exists(): if config_path.exists():
from nanobot.providers.registry import PROVIDERS from nanobot.providers.registry import PROVIDERS
console.print(f"Model: {config.agents.defaults.model}") console.print(f"Model: {config.agents.defaults.model}")
# Check API keys from registry # Check API keys from registry
for spec in PROVIDERS: for spec in PROVIDERS:
p = getattr(config.providers, spec.name, None) p = getattr(config.providers, spec.name, None)
@ -789,7 +841,9 @@ def status():
console.print(f"{spec.label}: [dim]not set[/dim]") console.print(f"{spec.label}: [dim]not set[/dim]")
else: else:
has_key = bool(p.api_key) has_key = bool(p.api_key)
console.print(f"{spec.label}: {'[green]✓[/green]' if has_key else '[dim]not set[/dim]'}") console.print(
f"{spec.label}: {'[green]✓[/green]' if has_key else '[dim]not set[/dim]'}"
)
if __name__ == "__main__": if __name__ == "__main__":