bonito-cli 0.7.4__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/CHANGELOG.md +6 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/PKG-INFO +1 -1
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/__init__.py +1 -1
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/agents.py +173 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/auth.py +152 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/deploy.py +22 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/pyproject.toml +1 -1
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/.gitignore +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/CLI_SPEC.md +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/README.md +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/__main__.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/api.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/app.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/__init__.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/admin.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/analytics.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/approval.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/audit.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/chat.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/compliance.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/deployments.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/gateway.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/groups.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/init.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/kb.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/mcp.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/memory.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/models.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/plan.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/policies.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/projects.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/providers.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/rbac.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/routing.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/scheduler.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/secrets.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/commands/sso.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/config.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/examples/__init__.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/examples/enterprise.yaml +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/examples/multi-provider-failover.yaml +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/examples/quickstart.yaml +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/examples/support-agent.yaml +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/utils/__init__.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/utils/auth.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/utils/display.py +0 -0
- {bonito_cli-0.7.4 → bonito_cli-0.8.0}/bonito_cli/utils/feature_gate.py +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Bonito CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.0] - 2026-05-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Personal access tokens**: `bonito auth token create`, `bonito auth token list`, `bonito auth token revoke` — manage PATs (bp-...) for programmatic API access
|
|
9
|
+
- **Token login**: `bonito auth token login --token bp-...` — authenticate using a PAT instead of email/password
|
|
10
|
+
|
|
5
11
|
## [0.6.0] - 2026-05-07
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -554,5 +554,178 @@ def list_triggers(
|
|
|
554
554
|
console.print(table)
|
|
555
555
|
|
|
556
556
|
|
|
557
|
+
# ─── Scaling / HPA Commands ───
|
|
558
|
+
|
|
559
|
+
scaling_app = typer.Typer(help="Agent autoscaling (HPA) management")
|
|
560
|
+
app.add_typer(scaling_app, name="scaling")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@scaling_app.command("status")
|
|
564
|
+
def scaling_status(
|
|
565
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
566
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
567
|
+
):
|
|
568
|
+
"""Show current autoscaling status for an agent."""
|
|
569
|
+
ensure_authenticated()
|
|
570
|
+
try:
|
|
571
|
+
data = api.get(f"/agents/{agent_id}/scaling")
|
|
572
|
+
except APIError as e:
|
|
573
|
+
print_error(f"Failed to get scaling status: {e}")
|
|
574
|
+
raise typer.Exit(1)
|
|
575
|
+
|
|
576
|
+
if json_output:
|
|
577
|
+
console.print_json(json.dumps(data))
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
table = Table(title="Agent Scaling Status", show_lines=True)
|
|
581
|
+
table.add_column("Property", style="cyan")
|
|
582
|
+
table.add_column("Value", style="white")
|
|
583
|
+
|
|
584
|
+
table.add_row("Autoscale Enabled", "✅" if data["autoscale_enabled"] else "❌")
|
|
585
|
+
table.add_row("Base RPM", str(data["base_rpm"]))
|
|
586
|
+
table.add_row("Effective RPM", str(data["effective_rpm"]))
|
|
587
|
+
table.add_row("Scaling Active", "🔥 Yes" if data["scaling_active"] else "No")
|
|
588
|
+
table.add_row("Current Usage", f"{data['current_rpm_usage']} RPM")
|
|
589
|
+
table.add_row("Utilization", f"{data['utilization']:.0%}")
|
|
590
|
+
table.add_row("Replica Count", str(data["replica_count"]))
|
|
591
|
+
if data.get("last_scaled_at"):
|
|
592
|
+
table.add_row("Last Scaled", data["last_scaled_at"])
|
|
593
|
+
|
|
594
|
+
cfg = data.get("config", {})
|
|
595
|
+
table.add_row("Capacity Threshold", f"{cfg.get('capacity_threshold', 0.6):.0%}")
|
|
596
|
+
table.add_row("Scale-Down Threshold", f"{cfg.get('scale_down_threshold', 0.3):.0%}")
|
|
597
|
+
table.add_row("Max Replicas", str(cfg.get("max_replicas", 5)))
|
|
598
|
+
table.add_row("Mode", cfg.get("mode", "virtual"))
|
|
599
|
+
|
|
600
|
+
console.print(table)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@scaling_app.command("configure")
|
|
604
|
+
def scaling_configure(
|
|
605
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
606
|
+
threshold: float = typer.Option(0.6, "--threshold", "-t", help="Capacity threshold (0.1-0.95)"),
|
|
607
|
+
scale_down: float = typer.Option(0.3, "--scale-down", help="Scale-down threshold"),
|
|
608
|
+
max_replicas: int = typer.Option(5, "--max-replicas", "-m", help="Max replicas (1-10)"),
|
|
609
|
+
cooldown: int = typer.Option(300, "--cooldown", help="Scale-down cooldown in seconds"),
|
|
610
|
+
mode: str = typer.Option("virtual", "--mode", help="Scaling mode: virtual or replica"),
|
|
611
|
+
enable: bool = typer.Option(True, "--enable/--disable", help="Enable or disable autoscaling"),
|
|
612
|
+
):
|
|
613
|
+
"""Configure autoscaling for an agent. Enterprise+ only."""
|
|
614
|
+
ensure_authenticated()
|
|
615
|
+
try:
|
|
616
|
+
data = api.post(f"/agents/{agent_id}/scaling/configure", {
|
|
617
|
+
"enabled": enable,
|
|
618
|
+
"capacity_threshold": threshold,
|
|
619
|
+
"scale_down_threshold": scale_down,
|
|
620
|
+
"max_replicas": max_replicas,
|
|
621
|
+
"scale_down_cooldown_seconds": cooldown,
|
|
622
|
+
"mode": mode,
|
|
623
|
+
})
|
|
624
|
+
print_success(f"Autoscaling {'enabled' if enable else 'disabled'} for agent {agent_id}")
|
|
625
|
+
if enable:
|
|
626
|
+
cfg = data.get("autoscale_config", {})
|
|
627
|
+
print_info(f" Threshold: {cfg.get('capacity_threshold', 0.6):.0%} | Max replicas: {cfg.get('max_replicas', 5)} | Mode: {cfg.get('mode', 'virtual')}")
|
|
628
|
+
except APIError as e:
|
|
629
|
+
print_error(f"Failed to configure scaling: {e}")
|
|
630
|
+
raise typer.Exit(1)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@scaling_app.command("events")
|
|
634
|
+
def scaling_events(
|
|
635
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
636
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Number of events to show"),
|
|
637
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
638
|
+
):
|
|
639
|
+
"""List recent scaling events for an agent."""
|
|
640
|
+
ensure_authenticated()
|
|
641
|
+
try:
|
|
642
|
+
data = api.get(f"/agents/{agent_id}/scaling/events", params={"limit": limit})
|
|
643
|
+
except APIError as e:
|
|
644
|
+
print_error(f"Failed to get scaling events: {e}")
|
|
645
|
+
raise typer.Exit(1)
|
|
646
|
+
|
|
647
|
+
if json_output:
|
|
648
|
+
console.print_json(json.dumps(data))
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
events = data.get("events", [])
|
|
652
|
+
if not events:
|
|
653
|
+
print_info("No scaling events found.")
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
table = Table(title=f"Scaling Events ({data.get('total', 0)} total)")
|
|
657
|
+
table.add_column("Time", style="dim")
|
|
658
|
+
table.add_column("Type", style="cyan")
|
|
659
|
+
table.add_column("Previous RPM", justify="right")
|
|
660
|
+
table.add_column("New RPM", justify="right")
|
|
661
|
+
table.add_column("Utilization", justify="right")
|
|
662
|
+
|
|
663
|
+
for e in events:
|
|
664
|
+
event_type = e["event_type"]
|
|
665
|
+
style = "green" if "up" in event_type else "yellow"
|
|
666
|
+
table.add_row(
|
|
667
|
+
format_timestamp(e.get("created_at")),
|
|
668
|
+
f"[{style}]{event_type}[/{style}]",
|
|
669
|
+
str(e["previous_capacity"]),
|
|
670
|
+
str(e["new_capacity"]),
|
|
671
|
+
f"{e['trigger_utilization']:.0%}",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
console.print(table)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
@scaling_app.command("queue")
|
|
678
|
+
def scaling_queue(
|
|
679
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
680
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
681
|
+
):
|
|
682
|
+
"""Show current queue depth and status for an agent."""
|
|
683
|
+
ensure_authenticated()
|
|
684
|
+
try:
|
|
685
|
+
data = api.get(f"/agents/{agent_id}/queue")
|
|
686
|
+
except APIError as e:
|
|
687
|
+
print_error(f"Failed to get queue status: {e}")
|
|
688
|
+
raise typer.Exit(1)
|
|
689
|
+
|
|
690
|
+
if json_output:
|
|
691
|
+
console.print_json(json.dumps(data))
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
table = Table(title="Agent Queue Status", show_lines=True)
|
|
695
|
+
table.add_column("Property", style="cyan")
|
|
696
|
+
table.add_column("Value", style="white")
|
|
697
|
+
|
|
698
|
+
table.add_row("Queue Depth", str(data.get("depth", 0)))
|
|
699
|
+
table.add_row("Status", data.get("status", "unknown"))
|
|
700
|
+
if data.get("oldest_item_age_seconds") is not None:
|
|
701
|
+
age = data["oldest_item_age_seconds"]
|
|
702
|
+
table.add_row("Oldest Item Age", f"{age}s")
|
|
703
|
+
if data.get("estimated_wait_seconds") is not None:
|
|
704
|
+
table.add_row("Estimated Wait", f"{data['estimated_wait_seconds']}s")
|
|
705
|
+
if data.get("processing") is not None:
|
|
706
|
+
table.add_row("Currently Processing", str(data["processing"]))
|
|
707
|
+
|
|
708
|
+
console.print(table)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@scaling_app.command("manual")
|
|
712
|
+
def scaling_manual(
|
|
713
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
714
|
+
direction: str = typer.Argument(..., help="Scale direction: up or down"),
|
|
715
|
+
):
|
|
716
|
+
"""Manually scale an agent up or down. Enterprise+ only."""
|
|
717
|
+
ensure_authenticated()
|
|
718
|
+
if direction not in ("up", "down"):
|
|
719
|
+
print_error("Direction must be 'up' or 'down'")
|
|
720
|
+
raise typer.Exit(1)
|
|
721
|
+
|
|
722
|
+
try:
|
|
723
|
+
data = api.post(f"/agents/{agent_id}/scaling/manual", {"direction": direction})
|
|
724
|
+
print_success(f"Scaled {direction}: {data['previous_rpm']} → {data['new_rpm']} RPM")
|
|
725
|
+
except APIError as e:
|
|
726
|
+
print_error(f"Failed to scale: {e}")
|
|
727
|
+
raise typer.Exit(1)
|
|
728
|
+
|
|
729
|
+
|
|
557
730
|
if __name__ == "__main__":
|
|
558
731
|
app()
|
|
@@ -12,6 +12,7 @@ from rich.prompt import Prompt
|
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
14
|
from ..api import api, APIError
|
|
15
|
+
from ..utils.auth import ensure_authenticated
|
|
15
16
|
from ..config import (
|
|
16
17
|
clear_credentials,
|
|
17
18
|
is_authenticated,
|
|
@@ -275,3 +276,154 @@ def status():
|
|
|
275
276
|
console.print(f"[red]✗ API error: {exc}[/red]")
|
|
276
277
|
if exc.status_code == 401:
|
|
277
278
|
console.print(" Token may be expired. Run: [cyan]bonito auth login[/cyan]")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ── token management ─────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
token_app = typer.Typer(help="🔑 Personal access token management")
|
|
284
|
+
app.add_typer(token_app, name="token")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@token_app.command("create")
|
|
288
|
+
def token_create(
|
|
289
|
+
name: str = typer.Option(..., "--name", "-n", help="Token name"),
|
|
290
|
+
scopes: Optional[str] = typer.Option(None, "--scopes", "-s", help="Comma-separated scopes (omit for full access)"),
|
|
291
|
+
expires_in: int = typer.Option(90, "--expires-in", help="Expiry in days (1-365)"),
|
|
292
|
+
):
|
|
293
|
+
"""Create a personal access token (bp-...)."""
|
|
294
|
+
ensure_authenticated()
|
|
295
|
+
|
|
296
|
+
body = {"name": name, "expires_in_days": expires_in}
|
|
297
|
+
if scopes:
|
|
298
|
+
body["scopes"] = [s.strip() for s in scopes.split(",")]
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
with console.status("[cyan]Creating token…[/cyan]"):
|
|
302
|
+
result = api.post("/tokens", json=body)
|
|
303
|
+
|
|
304
|
+
raw_token = result.get("token", "")
|
|
305
|
+
console.print(
|
|
306
|
+
Panel(
|
|
307
|
+
f"[green]✓ Token created[/green]\n\n"
|
|
308
|
+
f" [bold yellow]{raw_token}[/bold yellow]\n\n"
|
|
309
|
+
f" [dim]Copy this now — it won't be shown again.[/dim]\n"
|
|
310
|
+
f" Name: {result.get('name')}\n"
|
|
311
|
+
f" Prefix: {result.get('token_prefix')}\n"
|
|
312
|
+
f" Expires: {result.get('expires_at', '')[:10]}",
|
|
313
|
+
title="🔑 Personal Access Token",
|
|
314
|
+
border_style="green",
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
except APIError as exc:
|
|
318
|
+
console.print(f"[red]✗ {exc}[/red]")
|
|
319
|
+
raise typer.Exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@token_app.command("list")
|
|
323
|
+
def token_list():
|
|
324
|
+
"""List your personal access tokens."""
|
|
325
|
+
ensure_authenticated()
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
with console.status("[cyan]Fetching tokens…[/cyan]"):
|
|
329
|
+
tokens = api.get("/tokens")
|
|
330
|
+
|
|
331
|
+
if not tokens:
|
|
332
|
+
console.print("[yellow]No personal access tokens found.[/yellow]")
|
|
333
|
+
console.print(" Create one: [cyan]bonito auth token create --name my-token[/cyan]")
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
table = Table(title="Personal Access Tokens", border_style="dim")
|
|
337
|
+
table.add_column("Name", style="cyan")
|
|
338
|
+
table.add_column("Prefix")
|
|
339
|
+
table.add_column("Expires")
|
|
340
|
+
table.add_column("Last Used")
|
|
341
|
+
table.add_column("Status")
|
|
342
|
+
table.add_column("ID", style="dim")
|
|
343
|
+
|
|
344
|
+
for t in tokens:
|
|
345
|
+
status_str = "[red]Revoked[/red]" if t.get("revoked_at") else "[green]Active[/green]"
|
|
346
|
+
expires = t.get("expires_at", "")[:10]
|
|
347
|
+
last_used = t.get("last_used_at", "")
|
|
348
|
+
last_used = last_used[:10] if last_used else "Never"
|
|
349
|
+
table.add_row(
|
|
350
|
+
t.get("name", "—"),
|
|
351
|
+
t.get("token_prefix", "—"),
|
|
352
|
+
expires,
|
|
353
|
+
last_used,
|
|
354
|
+
status_str,
|
|
355
|
+
str(t.get("id", ""))[:8] + "…",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
console.print(table)
|
|
359
|
+
|
|
360
|
+
except APIError as exc:
|
|
361
|
+
console.print(f"[red]✗ {exc}[/red]")
|
|
362
|
+
raise typer.Exit(1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@token_app.command("revoke")
|
|
366
|
+
def token_revoke(
|
|
367
|
+
token_id: str = typer.Argument(..., help="Token ID to revoke"),
|
|
368
|
+
):
|
|
369
|
+
"""Revoke a personal access token."""
|
|
370
|
+
ensure_authenticated()
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
with console.status("[cyan]Revoking token…[/cyan]"):
|
|
374
|
+
api.delete(f"/tokens/{token_id}")
|
|
375
|
+
console.print("[green]✓ Token revoked[/green]")
|
|
376
|
+
except APIError as exc:
|
|
377
|
+
console.print(f"[red]✗ {exc}[/red]")
|
|
378
|
+
raise typer.Exit(1)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@token_app.command("login")
|
|
382
|
+
def token_login(
|
|
383
|
+
token: str = typer.Option(..., "--token", "-t", help="Personal access token (bp-...)"),
|
|
384
|
+
):
|
|
385
|
+
"""Authenticate using a personal access token instead of email/password."""
|
|
386
|
+
if not token.startswith("bp-"):
|
|
387
|
+
console.print("[red]✗ Token must start with 'bp-' (personal access token)[/red]")
|
|
388
|
+
raise typer.Exit(1)
|
|
389
|
+
|
|
390
|
+
# Verify the token works
|
|
391
|
+
try:
|
|
392
|
+
with console.status("[cyan]Verifying token…[/cyan]"):
|
|
393
|
+
resp = httpx.get(
|
|
394
|
+
f"{api.base_url}/api/auth/me",
|
|
395
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
396
|
+
timeout=15,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if resp.status_code != 200:
|
|
400
|
+
console.print("[red]✗ Token verification failed. Check the token and try again.[/red]")
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
|
|
403
|
+
me = resp.json()
|
|
404
|
+
|
|
405
|
+
# Store the PAT as the access_token in credentials
|
|
406
|
+
creds = {
|
|
407
|
+
"access_token": token,
|
|
408
|
+
"email": me.get("email", ""),
|
|
409
|
+
"user": me,
|
|
410
|
+
}
|
|
411
|
+
save_credentials(creds)
|
|
412
|
+
|
|
413
|
+
# Reset HTTP client
|
|
414
|
+
if api._client and not api._client.is_closed:
|
|
415
|
+
api._client.close()
|
|
416
|
+
api._client = None
|
|
417
|
+
|
|
418
|
+
console.print(
|
|
419
|
+
Panel(
|
|
420
|
+
f"[green]✓ Authenticated via PAT as [bold]{me.get('email', '')}[/bold][/green]\n"
|
|
421
|
+
f" Organization: {me.get('org', {}).get('name', '—')}",
|
|
422
|
+
title="🔑 Token Login",
|
|
423
|
+
border_style="green",
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
except httpx.RequestError as exc:
|
|
428
|
+
console.print(f"[red]✗ Connection failed: {exc}[/red]")
|
|
429
|
+
raise typer.Exit(1)
|
|
@@ -505,6 +505,28 @@ def _deploy_agents(
|
|
|
505
505
|
elif verbose:
|
|
506
506
|
console.print(f" [dim yellow]! KB '{kb_name}' not found - skipping[/dim yellow]")
|
|
507
507
|
|
|
508
|
+
# Rate limit
|
|
509
|
+
if cfg.get("rate_limit_rpm"):
|
|
510
|
+
payload["rate_limit_rpm"] = cfg["rate_limit_rpm"]
|
|
511
|
+
|
|
512
|
+
# HPA / Autoscaling
|
|
513
|
+
scaling = cfg.get("scaling", {})
|
|
514
|
+
if scaling:
|
|
515
|
+
payload["autoscale_enabled"] = scaling.get("enabled", False)
|
|
516
|
+
autoscale_config = {}
|
|
517
|
+
if "capacity_threshold" in scaling:
|
|
518
|
+
autoscale_config["capacity_threshold"] = scaling["capacity_threshold"]
|
|
519
|
+
if "scale_down_threshold" in scaling:
|
|
520
|
+
autoscale_config["scale_down_threshold"] = scaling["scale_down_threshold"]
|
|
521
|
+
if "max_replicas" in scaling:
|
|
522
|
+
autoscale_config["max_replicas"] = scaling["max_replicas"]
|
|
523
|
+
if "scale_down_cooldown_seconds" in scaling:
|
|
524
|
+
autoscale_config["scale_down_cooldown_seconds"] = scaling["scale_down_cooldown_seconds"]
|
|
525
|
+
if "mode" in scaling:
|
|
526
|
+
autoscale_config["mode"] = scaling["mode"]
|
|
527
|
+
if autoscale_config:
|
|
528
|
+
payload["autoscale_config"] = autoscale_config
|
|
529
|
+
|
|
508
530
|
# Note: agent secrets are managed via /api/secrets, not in the agent payload
|
|
509
531
|
|
|
510
532
|
# Check if agent already exists — update instead of creating a duplicate
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|