bonito-cli 0.9.0__tar.gz → 0.9.2__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.
Files changed (47) hide show
  1. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/.gitignore +1 -0
  2. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/PKG-INFO +1 -1
  3. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/auth.py +1 -1
  4. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/deploy.py +147 -4
  5. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/projects.py +1 -1
  6. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/pyproject.toml +1 -1
  7. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/CHANGELOG.md +0 -0
  8. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/CLI_SPEC.md +0 -0
  9. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/README.md +0 -0
  10. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/__init__.py +0 -0
  11. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/__main__.py +0 -0
  12. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/api.py +0 -0
  13. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/app.py +0 -0
  14. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/__init__.py +0 -0
  15. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/admin.py +0 -0
  16. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/agents.py +0 -0
  17. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/analytics.py +0 -0
  18. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/approval.py +0 -0
  19. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/audit.py +0 -0
  20. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/chat.py +0 -0
  21. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/compliance.py +0 -0
  22. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/deployments.py +0 -0
  23. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/gateway.py +0 -0
  24. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/groups.py +0 -0
  25. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/init.py +0 -0
  26. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/kb.py +0 -0
  27. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/mcp.py +0 -0
  28. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/memory.py +0 -0
  29. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/models.py +0 -0
  30. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/plan.py +0 -0
  31. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/policies.py +0 -0
  32. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/providers.py +0 -0
  33. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/rbac.py +0 -0
  34. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/routing.py +0 -0
  35. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/scheduler.py +0 -0
  36. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/secrets.py +0 -0
  37. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/sso.py +0 -0
  38. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/config.py +0 -0
  39. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/__init__.py +0 -0
  40. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/enterprise.yaml +0 -0
  41. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/multi-provider-failover.yaml +0 -0
  42. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/quickstart.yaml +0 -0
  43. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/support-agent.yaml +0 -0
  44. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/__init__.py +0 -0
  45. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/auth.py +0 -0
  46. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/display.py +0 -0
  47. {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/feature_gate.py +0 -0
@@ -12,3 +12,4 @@ secrets/*.yaml
12
12
  !secrets/*.enc.yaml
13
13
  benchmarks/locomo-repo
14
14
  bonito-kb-sa-key.json
15
+ .claude/worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bonito-cli
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Bonito CLI — Unified multi-cloud AI management from your terminal
5
5
  Author-email: Bonito <hello@getbonito.com>
6
6
  License: MIT
@@ -299,7 +299,7 @@ def token_create(
299
299
 
300
300
  try:
301
301
  with console.status("[cyan]Creating token…[/cyan]"):
302
- result = api.post("/tokens", json=body)
302
+ result = api.post("/tokens", data=body)
303
303
 
304
304
  raw_token = result.get("token", "")
305
305
  console.print(
@@ -68,7 +68,41 @@ def _find_unresolved(value: Any, path: str = "") -> List[str]:
68
68
 
69
69
  _REQUIRED_TOP_KEYS = {"version", "name"}
70
70
  _KNOWN_TOP_KEYS = {"version", "name", "description", "gateway", "mcp_servers",
71
- "knowledge_bases", "agents", "observability", "secrets"}
71
+ "knowledge_bases", "agents", "observability", "secrets",
72
+ "integrations", "budget_stops"}
73
+
74
+
75
+ def _normalize_section(section: Any) -> Any:
76
+ """Accept a section as EITHER a mapping (name -> cfg) OR a list of
77
+ {name, ...} entries; always return a mapping keyed by name. Non-conforming
78
+ shapes are returned unchanged so _validate can report them."""
79
+ if isinstance(section, list):
80
+ out: Dict[str, Any] = {}
81
+ for entry in section:
82
+ if isinstance(entry, dict) and entry.get("name"):
83
+ name = entry["name"]
84
+ out[name] = {k: v for k, v in entry.items() if k != "name"}
85
+ return out
86
+ return section
87
+
88
+
89
+ def _normalize_agents(section: Any) -> Any:
90
+ """Normalize agents to the mapping form + reconcile field aliases so the
91
+ list-style schema (model_id / system_prompt_path / model_config) works the
92
+ same as the mapping-style schema (model.primary / system_prompt / parameters)."""
93
+ agents = _normalize_section(section)
94
+ if not isinstance(agents, dict):
95
+ return agents
96
+ for cfg in agents.values():
97
+ if not isinstance(cfg, dict):
98
+ continue
99
+ if "system_prompt" not in cfg and "system_prompt_path" in cfg:
100
+ cfg["system_prompt"] = cfg["system_prompt_path"]
101
+ if "model" not in cfg and "model_id" in cfg:
102
+ cfg["model"] = {"primary": cfg["model_id"]}
103
+ if "parameters" not in cfg and isinstance(cfg.get("model_config"), dict):
104
+ cfg["parameters"] = cfg["model_config"]
105
+ return agents
72
106
 
73
107
 
74
108
  def _validate(cfg: Dict[str, Any]) -> List[str]:
@@ -93,6 +127,14 @@ def _validate(cfg: Dict[str, Any]) -> List[str]:
93
127
  if "knowledge_bases" in cfg and not isinstance(cfg["knowledge_bases"], dict):
94
128
  errors.append("'knowledge_bases' must be a mapping of kb-name -> config")
95
129
 
130
+ if "integrations" in cfg:
131
+ if not isinstance(cfg["integrations"], list):
132
+ errors.append("'integrations' must be a list of integration entries")
133
+ else:
134
+ for i, entry in enumerate(cfg["integrations"]):
135
+ if not isinstance(entry, dict) or not entry.get("type"):
136
+ errors.append(f"integrations[{i}]: must be a mapping with a 'type'")
137
+
96
138
  # Validate each agent
97
139
  for agent_name, agent_cfg in (cfg.get("agents") or {}).items():
98
140
  if not isinstance(agent_cfg, dict):
@@ -117,22 +159,26 @@ class DeployResult:
117
159
  self.mcp_servers: List[Dict[str, str]] = []
118
160
  self.knowledge_bases: List[Dict[str, str]] = []
119
161
  self.agents: List[Dict[str, str]] = []
162
+ self.integrations: List[Dict[str, str]] = []
120
163
 
121
164
  @property
122
165
  def total(self) -> int:
123
- return len(self.providers) + len(self.mcp_servers) + len(self.knowledge_bases) + len(self.agents)
166
+ return (
167
+ len(self.providers) + len(self.mcp_servers) + len(self.knowledge_bases)
168
+ + len(self.agents) + len(self.integrations)
169
+ )
124
170
 
125
171
  @property
126
172
  def errors(self) -> int:
127
173
  return sum(
128
- 1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents)
174
+ 1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents, self.integrations)
129
175
  for item in items if item["status"] == "error"
130
176
  )
131
177
 
132
178
  @property
133
179
  def skipped(self) -> int:
134
180
  return sum(
135
- 1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents)
181
+ 1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents, self.integrations)
136
182
  for item in items if item["status"] == "skipped"
137
183
  )
138
184
 
@@ -827,6 +873,93 @@ def _print_next_steps(
827
873
  ))
828
874
 
829
875
 
876
+ # -- step: integrations ------------------------------------------------------
877
+
878
+
879
+ def _deploy_integrations(
880
+ integrations: List[Dict[str, Any]],
881
+ agent_id_map: Dict[str, str],
882
+ result: DeployResult,
883
+ dry_run: bool,
884
+ verbose: bool,
885
+ ) -> None:
886
+ """Provision declared integrations (Slack today) against the API.
887
+
888
+ Each entry is connected, pointed at an inbound agent, and bound to the
889
+ listed agents. Agent references resolve by name via agent_id_map.
890
+ Schema: integrations: [{type: slack, bot_token, signing_secret,
891
+ inbound_agent, inbound_routes: {channel_id: agent}, bindings: [agent...]}].
892
+ """
893
+ if not integrations:
894
+ return
895
+
896
+ console.print("\n[bold cyan]Integrations[/bold cyan]")
897
+ for entry in integrations:
898
+ if not isinstance(entry, dict):
899
+ result.integrations.append({"name": "integration", "status": "error", "detail": "must be a mapping"})
900
+ continue
901
+ itype = (entry.get("type") or "").lower()
902
+ name = itype or "integration"
903
+ try:
904
+ if dry_run:
905
+ result.integrations.append({"name": name, "status": "ok", "detail": "dry-run"})
906
+ console.print(f" [dim]~ {name} (dry-run)[/dim]")
907
+ continue
908
+
909
+ if itype != "slack":
910
+ result.integrations.append({"name": name, "status": "error", "detail": f"unsupported type '{itype}' (only slack today)"})
911
+ console.print(f" [red]x {name}: unsupported (only slack today)[/red]")
912
+ continue
913
+
914
+ inbound_name = entry.get("inbound_agent")
915
+ inbound_id = agent_id_map.get(inbound_name) if inbound_name else None
916
+ if inbound_name and not inbound_id:
917
+ result.integrations.append({"name": name, "status": "error", "detail": f"inbound_agent '{inbound_name}' not in this deploy"})
918
+ console.print(f" [red]x slack: inbound_agent '{inbound_name}' not found[/red]")
919
+ continue
920
+
921
+ # 1. Connect the customer-owned Slack app (team_id auto-resolved)
922
+ payload: Dict[str, Any] = {
923
+ "bot_token": entry.get("bot_token"),
924
+ "signing_secret": entry.get("signing_secret"),
925
+ }
926
+ if inbound_id:
927
+ payload["inbound_agent_id"] = inbound_id
928
+ conn = api.post("/integrations/slack/connect/app", payload)
929
+ conn_id = conn.get("id")
930
+ detail = [f"connected ({conn.get('account_label') or 'slack'})"]
931
+
932
+ # 2. Per-channel inbound routing overrides (channel_id -> agent name)
933
+ routes_cfg = entry.get("inbound_routes") or {}
934
+ resolved = {ch: agent_id_map[a] for ch, a in routes_cfg.items() if a in agent_id_map}
935
+ if resolved:
936
+ body: Dict[str, Any] = {"inbound_routes": resolved}
937
+ if inbound_id:
938
+ body["inbound_agent_id"] = inbound_id
939
+ api.patch(f"/integrations/connections/{conn_id}/inbound", body)
940
+ detail.append(f"{len(resolved)} channel route(s)")
941
+
942
+ # 3. Outbound bindings (give agents the slack_post_message tool)
943
+ bound = 0
944
+ for agent_name in (entry.get("bindings") or []):
945
+ aid = agent_id_map.get(agent_name)
946
+ if not aid:
947
+ continue
948
+ try:
949
+ api.post("/integrations/bindings", {"agent_id": aid, "connection_id": conn_id})
950
+ bound += 1
951
+ except APIError:
952
+ pass
953
+ if bound:
954
+ detail.append(f"{bound} binding(s)")
955
+
956
+ result.integrations.append({"name": name, "status": "ok", "detail": ", ".join(detail)})
957
+ console.print(f" [green]v[/green] slack [dim]({', '.join(detail)})[/dim]")
958
+ except (APIError, Exception) as exc:
959
+ result.integrations.append({"name": name, "status": "error", "detail": str(exc)})
960
+ console.print(f" [red]x {name}: {exc}[/red]")
961
+
962
+
830
963
  # -- main command ------------------------------------------------------------
831
964
 
832
965
 
@@ -884,6 +1017,13 @@ def deploy(
884
1017
  # -- 2. Resolve env vars -------------------------------------------------
885
1018
  cfg = _resolve_env(cfg)
886
1019
 
1020
+ # Accept BOTH the mapping form (name -> cfg) and the list form
1021
+ # ([{name, ...}]) for agents + knowledge_bases, reconciling field aliases.
1022
+ if "agents" in cfg:
1023
+ cfg["agents"] = _normalize_agents(cfg["agents"])
1024
+ if "knowledge_bases" in cfg:
1025
+ cfg["knowledge_bases"] = _normalize_section(cfg["knowledge_bases"])
1026
+
887
1027
  unresolved = _find_unresolved(cfg)
888
1028
  if unresolved:
889
1029
  console.print(f"\n[yellow]{len(unresolved)} unresolved environment variable(s):[/yellow]")
@@ -966,6 +1106,9 @@ def deploy(
966
1106
  # -- 8. Agent connections (delegation) -----------------------------------
967
1107
  _create_agent_connections(agents, agent_id_map, result, dry_run, verbose)
968
1108
 
1109
+ # -- 8b. Integrations (Slack etc.) ---------------------------------------
1110
+ _deploy_integrations(cfg.get("integrations", []), agent_id_map, result, dry_run, verbose)
1111
+
969
1112
  # -- 9. Summary ----------------------------------------------------------
970
1113
  _print_summary(result, stack_name, dry_run)
971
1114
 
@@ -367,7 +367,7 @@ def project_token_create(
367
367
 
368
368
  try:
369
369
  with console.status("[cyan]Creating project token…[/cyan]"):
370
- result = api.post(f"/projects/{project_id}/tokens", json=body)
370
+ result = api.post(f"/projects/{project_id}/tokens", data=body)
371
371
 
372
372
  raw_token = result.get("token", "")
373
373
  console.print(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bonito-cli"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "Bonito CLI — Unified multi-cloud AI management from your terminal"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes