proxima-cli 1.0.0__py3-none-any.whl

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.
prox/commands/agent.py ADDED
@@ -0,0 +1,345 @@
1
+ """prox agent — manage agents on the platform."""
2
+
3
+ import json
4
+ import subprocess
5
+ import tarfile
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ import yaml
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from ..api import gateway_get, gateway_post, gateway_put, gateway_delete, APIError
15
+ from ..config import get_value
16
+
17
+ app = typer.Typer(help="Manage platform agents.")
18
+ console = Console()
19
+
20
+
21
+ @app.command("list")
22
+ def list_agents(
23
+ domain: Optional[str] = typer.Option(None, "--domain", "-d", help="Filter by domain"),
24
+ status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (active/draft)"),
25
+ ):
26
+ """List all agents on the platform."""
27
+ try:
28
+ params = {}
29
+ if domain:
30
+ params["domain"] = domain
31
+ if status:
32
+ params["status"] = status
33
+ data = gateway_get("/build/agents", params=params)
34
+ except APIError as e:
35
+ console.print(f"[red]Error:[/red] {e.detail}")
36
+ raise typer.Exit(1)
37
+
38
+ agents = data.get("agents", [])
39
+ if not agents:
40
+ console.print("[dim]No agents found.[/dim]")
41
+ return
42
+
43
+ table = Table(title=f"Agents ({len(agents)})")
44
+ table.add_column("ID", style="bold")
45
+ table.add_column("Name")
46
+ table.add_column("Domain")
47
+ table.add_column("Status")
48
+ table.add_column("Version", justify="right")
49
+
50
+ for a in agents:
51
+ st = a.get("status", "draft")
52
+ st_style = "green" if st == "active" else "yellow" if st == "draft" else "dim"
53
+ table.add_row(
54
+ a.get("id", ""),
55
+ a.get("name", ""),
56
+ a.get("domain", ""),
57
+ f"[{st_style}]{st}[/{st_style}]",
58
+ str(a.get("version", 1)),
59
+ )
60
+
61
+ console.print(table)
62
+
63
+
64
+ @app.command("get")
65
+ def get_agent(agent_id: str = typer.Argument(help="Agent ID")):
66
+ """Get agent details."""
67
+ try:
68
+ data = gateway_get(f"/build/agents/{agent_id}")
69
+ except APIError as e:
70
+ console.print(f"[red]Error:[/red] {e.detail}")
71
+ raise typer.Exit(1)
72
+
73
+ agent = data.get("agent", data)
74
+ console.print(f"\n[bold]{agent.get('name', agent_id)}[/bold]")
75
+ console.print(f" ID: {agent.get('id')}")
76
+ console.print(f" Codename: {agent.get('codename', '—')}")
77
+ console.print(f" Domain: {agent.get('domain', '—')}")
78
+ console.print(f" Role: {agent.get('role', 'agent')}")
79
+ console.print(f" Status: {agent.get('status', '—')}")
80
+ console.print(f" Version: {agent.get('version', 1)}")
81
+ console.print(f" Model: {agent.get('llm', {}).get('model', '—')}")
82
+ if agent.get("toolboxes"):
83
+ console.print(f" Toolboxes: {', '.join(agent['toolboxes'])}")
84
+ if agent.get("knowledge_bases"):
85
+ console.print(f" Knowledge: {', '.join(agent['knowledge_bases'])}")
86
+ console.print()
87
+
88
+
89
+ @app.command("create")
90
+ def create_agent(
91
+ name: str = typer.Option(None, "--name", "-n", help="Agent name"),
92
+ codename: str = typer.Option(None, "--codename", help="Agent codename"),
93
+ domain: str = typer.Option(None, "--domain", "-d", help="Domain"),
94
+ role: str = typer.Option("agent", "--role", help="Role: agent or orchestrator"),
95
+ from_file: Optional[Path] = typer.Option(None, "--from", help="Create from agent.yaml file"),
96
+ ):
97
+ """Create a new agent."""
98
+ if from_file:
99
+ if not from_file.exists():
100
+ console.print(f"[red]Error:[/red] File not found: {from_file}")
101
+ raise typer.Exit(1)
102
+ config = yaml.safe_load(from_file.read_text())
103
+ else:
104
+ if not name:
105
+ console.print("[red]Error:[/red] Provide --name or --from")
106
+ raise typer.Exit(1)
107
+ agent_id = name.lower().replace(" ", "-").replace("_", "-")
108
+ config = {"id": agent_id, "name": name, "codename": codename or "", "domain": domain or "", "role": role}
109
+
110
+ try:
111
+ data = gateway_post("/build/agents", config)
112
+ except APIError as e:
113
+ console.print(f"[red]Error:[/red] {e.detail}")
114
+ raise typer.Exit(1)
115
+
116
+ console.print(f"[green]✓[/green] Agent created: {config.get('id', config.get('name'))}")
117
+
118
+
119
+ @app.command("update")
120
+ def update_agent(
121
+ agent_id: str = typer.Argument(help="Agent ID"),
122
+ prompt: Optional[str] = typer.Option(None, "--prompt", help="System prompt"),
123
+ model: Optional[str] = typer.Option(None, "--model", help="Model ID"),
124
+ from_file: Optional[Path] = typer.Option(None, "--from", help="Update from agent.yaml"),
125
+ ):
126
+ """Update an agent's configuration."""
127
+ if from_file:
128
+ if not from_file.exists():
129
+ console.print(f"[red]Error:[/red] File not found: {from_file}")
130
+ raise typer.Exit(1)
131
+ config = yaml.safe_load(from_file.read_text())
132
+ else:
133
+ config = {}
134
+ if prompt:
135
+ config["system_prompt"] = prompt
136
+ if model:
137
+ config["llm"] = {"model": model}
138
+ if not config:
139
+ console.print("[red]Error:[/red] Provide --prompt, --model, or --from")
140
+ raise typer.Exit(1)
141
+
142
+ try:
143
+ gateway_put(f"/build/agents/{agent_id}", config)
144
+ except APIError as e:
145
+ console.print(f"[red]Error:[/red] {e.detail}")
146
+ raise typer.Exit(1)
147
+
148
+ console.print(f"[green]✓[/green] Agent updated: {agent_id}")
149
+
150
+
151
+ @app.command("publish")
152
+ def publish_agent(agent_id: str = typer.Argument(help="Agent ID to publish")):
153
+ """Publish an agent (draft → active)."""
154
+ try:
155
+ gateway_post(f"/build/agents/{agent_id}/publish")
156
+ except APIError as e:
157
+ console.print(f"[red]Error:[/red] {e.detail}")
158
+ raise typer.Exit(1)
159
+
160
+ console.print(f"[green]✓[/green] Published: {agent_id}")
161
+
162
+
163
+ @app.command("delete")
164
+ def delete_agent(
165
+ agent_id: str = typer.Argument(help="Agent ID to delete"),
166
+ confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
167
+ ):
168
+ """Delete an agent."""
169
+ if not confirm:
170
+ typer.confirm(f"Delete agent '{agent_id}'? This cannot be undone", abort=True)
171
+
172
+ try:
173
+ gateway_delete(f"/build/agents/{agent_id}")
174
+ except APIError as e:
175
+ console.print(f"[red]Error:[/red] {e.detail}")
176
+ raise typer.Exit(1)
177
+
178
+ console.print(f"[green]✓[/green] Deleted: {agent_id}")
179
+
180
+
181
+ @app.command("deploy")
182
+ def deploy_agent(
183
+ from_path: Path = typer.Option(..., "--from", help="Path to agent package directory"),
184
+ ):
185
+ """Full agent deploy: build toolbox → register → configure → publish.
186
+
187
+ Expects directory structure:
188
+ agent.yaml, toolbox/, workspace.json, knowledge.yaml, ontology.yaml, data/synthetic/
189
+ """
190
+ if not from_path.exists():
191
+ console.print(f"[red]Error:[/red] Path not found: {from_path}")
192
+ raise typer.Exit(1)
193
+
194
+ agent_yaml = from_path / "agent.yaml"
195
+ if not agent_yaml.exists():
196
+ console.print(f"[red]Error:[/red] No agent.yaml found in {from_path}")
197
+ raise typer.Exit(1)
198
+
199
+ config = yaml.safe_load(agent_yaml.read_text())
200
+ agent_id = config["id"]
201
+ console.print(f"\n[bold]Deploying:[/bold] {config.get('name', agent_id)} ({agent_id})\n")
202
+
203
+ # Step 1: Build and push toolbox (if exists)
204
+ toolbox_dir = from_path / "toolbox"
205
+ if toolbox_dir.exists() and (toolbox_dir / "Dockerfile").exists():
206
+ _deploy_toolbox(agent_id, toolbox_dir, config)
207
+
208
+ # Step 2: Upload knowledge data (if exists)
209
+ knowledge_yaml = from_path / "knowledge.yaml"
210
+ if knowledge_yaml.exists():
211
+ _deploy_knowledge(agent_id, knowledge_yaml, from_path)
212
+
213
+ # Step 2b: Deploy ontology (if exists)
214
+ ontology_yaml = from_path / "ontology.yaml"
215
+ if ontology_yaml.exists():
216
+ console.print(" [bold]2b.[/bold] Deploying ontology...")
217
+ try:
218
+ ont_data = yaml.safe_load(ontology_yaml.read_text())
219
+ ont_id = ont_data.get("id", f"{agent_id}-ontology")
220
+ gateway_post("/build/ontology", ont_data)
221
+ # Connect ontology to agent config
222
+ config["ontology"] = {"id": ont_id}
223
+ console.print(f" [green]\u2713[/green] Ontology deployed: {ont_id}")
224
+ except APIError:
225
+ # May already exist — try update
226
+ try:
227
+ gateway_put(f"/build/ontology/{ont_data.get('id', '')}", ont_data)
228
+ config["ontology"] = {"id": ont_data.get('id', '')}
229
+ console.print(f" [green]\u2713[/green] Ontology updated: {ont_data.get('id', '')}")
230
+ except APIError as e:
231
+ console.print(f" [yellow]\u26a0[/yellow] Ontology: {e.detail}")
232
+
233
+ # Step 3: Create/update agent in registry
234
+ console.print(" [bold]3.[/bold] Registering agent...")
235
+ try:
236
+ gateway_put(f"/build/agents/{agent_id}", config)
237
+ console.print(" [green]✓[/green] Agent registered")
238
+ except APIError as e:
239
+ console.print(f" [red]✗[/red] {e.detail}")
240
+ raise typer.Exit(1)
241
+
242
+ # Step 4: Push workspace (if exists)
243
+ workspace_json = from_path / "workspace.json"
244
+ if workspace_json.exists():
245
+ console.print(" [bold]4.[/bold] Pushing workspace...")
246
+ try:
247
+ ws = json.loads(workspace_json.read_text())
248
+ gateway_put(f"/workspaces/{agent_id}", ws)
249
+ console.print(" [green]✓[/green] Workspace pushed")
250
+ except APIError as e:
251
+ console.print(f" [yellow]⚠[/yellow] Workspace push failed: {e.detail}")
252
+
253
+ # Step 5: Publish
254
+ console.print(" [bold]5.[/bold] Publishing...")
255
+ try:
256
+ gateway_post(f"/build/agents/{agent_id}/publish")
257
+ console.print(" [green]✓[/green] Published (status: active)")
258
+ except APIError as e:
259
+ console.print(f" [yellow]⚠[/yellow] Publish failed: {e.detail}")
260
+
261
+ # Step 6: Health check
262
+ console.print(" [bold]6.[/bold] Health check...")
263
+ try:
264
+ health = gateway_get(f"/agents/{agent_id}/health")
265
+ console.print(f" [green]✓[/green] Healthy")
266
+ except APIError:
267
+ console.print(" [yellow]⚠[/yellow] Health check unavailable")
268
+
269
+ console.print(f"\n[green]✓ Deploy complete:[/green] {agent_id}\n")
270
+
271
+
272
+ def _deploy_toolbox(agent_id: str, toolbox_dir: Path, config: dict):
273
+ """Build Docker image, push to registry, register toolbox."""
274
+ registry = get_value("registry")
275
+ if not registry:
276
+ console.print(" [bold]1.[/bold] Toolbox — [yellow]skipped[/yellow] (no registry configured)")
277
+ return
278
+
279
+ image = f"{registry}/{agent_id}-toolbox:latest"
280
+ console.print(f" [bold]1.[/bold] Building toolbox → {image}")
281
+
282
+ # Build
283
+ result = subprocess.run(
284
+ ["docker", "build", "--platform", "linux/amd64", "-t", image, "."],
285
+ cwd=toolbox_dir, capture_output=True, text=True,
286
+ )
287
+ if result.returncode != 0:
288
+ console.print(f" [red]✗[/red] Docker build failed:\n{result.stderr[:300]}")
289
+ raise typer.Exit(1)
290
+ console.print(" [green]✓[/green] Image built")
291
+
292
+ # Push
293
+ result = subprocess.run(
294
+ ["docker", "push", image],
295
+ capture_output=True, text=True,
296
+ )
297
+ if result.returncode != 0:
298
+ console.print(f" [red]✗[/red] Docker push failed:\n{result.stderr[:300]}")
299
+ raise typer.Exit(1)
300
+ console.print(" [green]✓[/green] Image pushed")
301
+
302
+ # Register toolbox with gateway
303
+ toolbox_id = config.get("toolboxes", [None])[0] or f"{agent_id}-toolbox"
304
+ console.print(f" [bold]2.[/bold] Registering toolbox: {toolbox_id}")
305
+ try:
306
+ gateway_post("/build/toolboxes", {
307
+ "id": toolbox_id,
308
+ "name": f"{config.get('name', agent_id)} Toolbox",
309
+ "endpoint": f"http://{agent_id}:8001",
310
+ "type": "openapi",
311
+ })
312
+ # Discover tools
313
+ gateway_post("/build/toolboxes/discover", {"toolbox_id": toolbox_id})
314
+ console.print(" [green]✓[/green] Toolbox registered + tools discovered")
315
+ except APIError as e:
316
+ console.print(f" [yellow]⚠[/yellow] Toolbox registration: {e.detail}")
317
+
318
+
319
+ def _deploy_knowledge(agent_id: str, knowledge_yaml: Path, base_path: Path):
320
+ """Create knowledge sources and bases from knowledge.yaml."""
321
+ console.print(" [bold]2b.[/bold] Setting up knowledge...")
322
+ kconfig = yaml.safe_load(knowledge_yaml.read_text())
323
+
324
+ # Create sources
325
+ for source in kconfig.get("sources", []):
326
+ try:
327
+ gateway_post("/build/knowledge/sources", source)
328
+ console.print(f" [green]✓[/green] Source: {source.get('id', '?')}")
329
+ except APIError:
330
+ pass # May already exist
331
+
332
+ # Create bases
333
+ for base in kconfig.get("bases", []):
334
+ try:
335
+ gateway_post("/build/knowledge/bases", base)
336
+ console.print(f" [green]✓[/green] Base: {base.get('id', '?')}")
337
+ except APIError:
338
+ pass # May already exist
339
+
340
+ # Upload synthetic data if present
341
+ data_dir = base_path / "data" / "synthetic"
342
+ if data_dir.exists():
343
+ parquets = list(data_dir.glob("*.parquet"))
344
+ if parquets:
345
+ console.print(f" [dim]({len(parquets)} parquet files — upload via knowledge source config)[/dim]")
@@ -0,0 +1,242 @@
1
+ """prox catalog — browse, pull, and push agent packages."""
2
+
3
+ import tarfile
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from ..api import catalog_get, catalog_post, catalog_download, APIError
13
+ from ..config import PACKAGES_DIR, get_license_key, get_master_key
14
+
15
+ app = typer.Typer(help="Browse and manage the Proxima Agent Catalog.")
16
+ console = Console()
17
+
18
+
19
+ @app.command("list")
20
+ def list_agents(
21
+ domain: Optional[str] = typer.Option(None, "--domain", "-d", help="Filter by domain"),
22
+ format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
23
+ ):
24
+ """List all agents in the catalog."""
25
+ try:
26
+ params = {}
27
+ if domain:
28
+ params["domain"] = domain
29
+ data = catalog_get("/catalog/agents", params=params)
30
+ except APIError as e:
31
+ console.print(f"[red]Error:[/red] {e.detail}")
32
+ raise typer.Exit(1)
33
+
34
+ agents = data.get("agents", [])
35
+ if not agents:
36
+ console.print("[dim]No agents found.[/dim]")
37
+ return
38
+
39
+ if format == "json":
40
+ import json
41
+ console.print(json.dumps(agents, indent=2))
42
+ return
43
+
44
+ # Check license to mark available agents
45
+ licensed_ids = set()
46
+ try:
47
+ lic = catalog_get("/catalog/license")
48
+ licensed_ids = set(lic.get("agents", []))
49
+ except APIError:
50
+ pass # No license configured — show all without marks
51
+
52
+ table = Table(title=f"Proxima Catalog ({len(agents)} agents)")
53
+ table.add_column("Agent", style="bold")
54
+ table.add_column("Codename", style="cyan")
55
+ table.add_column("Domain")
56
+ table.add_column("Tools", justify="right")
57
+ table.add_column("Version")
58
+ table.add_column("Access", justify="center")
59
+
60
+ for agent in agents:
61
+ aid = agent["id"]
62
+ access = "✓" if aid in licensed_ids or get_master_key() else "○"
63
+ access_style = "green" if access == "✓" else "dim"
64
+ table.add_row(
65
+ agent.get("name", aid),
66
+ agent.get("codename", ""),
67
+ agent.get("domain", ""),
68
+ str(agent.get("tools", 0)),
69
+ agent.get("latest_version", "—"),
70
+ f"[{access_style}]{access}[/{access_style}]",
71
+ )
72
+
73
+ console.print(table)
74
+ if licensed_ids:
75
+ console.print(f"\n[green]✓[/green] = licensed [dim]○[/dim] = contact Proxima")
76
+
77
+
78
+ @app.command("show")
79
+ def show_agent(agent_id: str = typer.Argument(help="Agent ID")):
80
+ """Show detailed information about a catalog agent."""
81
+ try:
82
+ data = catalog_get(f"/catalog/agents/{agent_id}")
83
+ except APIError as e:
84
+ console.print(f"[red]Error:[/red] {e.detail}")
85
+ raise typer.Exit(1)
86
+
87
+ console.print(f"\n[bold]{data.get('name', agent_id)}[/bold] ({data.get('codename', '')})")
88
+ console.print(f"[dim]{data.get('description', '')}[/dim]\n")
89
+ console.print(f" Domain: {data.get('domain', '—')}")
90
+ console.print(f" Tools: {data.get('tools', 0)}")
91
+ console.print(f" Version: {data.get('latest_version', '—')}")
92
+ console.print(f" Updated: {data.get('updated_at', '—')}")
93
+
94
+ if data.get("tool_names"):
95
+ console.print(f"\n [bold]Tools:[/bold]")
96
+ for t in data["tool_names"]:
97
+ console.print(f" • {t}")
98
+
99
+ if data.get("industries"):
100
+ console.print(f"\n [bold]Industries:[/bold] {', '.join(data['industries'])}")
101
+ if data.get("functions"):
102
+ console.print(f" [bold]Functions:[/bold] {', '.join(data['functions'])}")
103
+ console.print()
104
+
105
+
106
+ @app.command("pull")
107
+ def pull_agent(
108
+ agent_id: str = typer.Argument(help="Agent ID to pull"),
109
+ version: Optional[str] = typer.Option(None, "--version", "-v", help="Specific version (default: latest)"),
110
+ ):
111
+ """Pull an agent package from the catalog."""
112
+ if not get_license_key() and not get_master_key():
113
+ console.print("[red]Error:[/red] No license key configured. Run: prox login --license-key <key>")
114
+ raise typer.Exit(1)
115
+
116
+ console.print(f"[bold]Pulling[/bold] {agent_id}" + (f"@{version}" if version else " (latest)") + "...")
117
+
118
+ # Get download URL / validate access
119
+ try:
120
+ params = {"version": version} if version else {}
121
+ meta = catalog_get(f"/catalog/agents/{agent_id}", params=params)
122
+ except APIError as e:
123
+ if e.status == 403:
124
+ console.print(f"[red]Access denied:[/red] '{agent_id}' is not in your license. Contact Proxima to upgrade.")
125
+ else:
126
+ console.print(f"[red]Error:[/red] {e.detail}")
127
+ raise typer.Exit(1)
128
+
129
+ actual_version = version or meta.get("latest_version", "latest")
130
+ dest_dir = PACKAGES_DIR / agent_id
131
+ dest_dir.mkdir(parents=True, exist_ok=True)
132
+ tarball = dest_dir / f"{agent_id}-{actual_version}.tar.gz"
133
+
134
+ try:
135
+ pull_path = f"/catalog/agents/{agent_id}/pull"
136
+ if version:
137
+ pull_path += f"?version={version}"
138
+ catalog_download(pull_path, str(tarball))
139
+ except APIError as e:
140
+ console.print(f"[red]Download failed:[/red] {e.detail}")
141
+ raise typer.Exit(1)
142
+
143
+ # Extract
144
+ extract_dir = dest_dir / actual_version
145
+ extract_dir.mkdir(parents=True, exist_ok=True)
146
+ with tarfile.open(tarball, "r:gz") as tar:
147
+ tar.extractall(path=extract_dir)
148
+
149
+ console.print(f"[green]✓[/green] Pulled {agent_id}@{actual_version} → {extract_dir}")
150
+ console.print(f"\n Deploy with: [bold]prox agent deploy --from {extract_dir}[/bold]")
151
+
152
+
153
+ @app.command("push")
154
+ def push_agent(
155
+ from_path: Path = typer.Option(..., "--from", help="Path to agent directory"),
156
+ version: str = typer.Option(..., "--version", "-v", help="Version to publish"),
157
+ ):
158
+ """Push an agent package to the catalog (Proxima team only)."""
159
+ if not get_master_key():
160
+ console.print("[red]Error:[/red] Master key required. Run: prox login --master-key <key>")
161
+ raise typer.Exit(1)
162
+
163
+ if not from_path.exists():
164
+ console.print(f"[red]Error:[/red] Path not found: {from_path}")
165
+ raise typer.Exit(1)
166
+
167
+ # Read manifest or agent.yaml for the agent ID
168
+ manifest = from_path / "manifest.json"
169
+ agent_yaml = from_path / "agent.yaml"
170
+ if manifest.exists():
171
+ import json
172
+ meta = json.loads(manifest.read_text())
173
+ agent_id = meta["id"]
174
+ elif agent_yaml.exists():
175
+ import yaml
176
+ meta = yaml.safe_load(agent_yaml.read_text())
177
+ agent_id = meta["id"]
178
+ else:
179
+ console.print("[red]Error:[/red] No manifest.json or agent.yaml found in source directory.")
180
+ raise typer.Exit(1)
181
+
182
+ console.print(f"[bold]Packaging[/bold] {agent_id}@{version}...")
183
+
184
+ # Create tarball
185
+ tarball_path = Path(tempfile.mktemp(suffix=".tar.gz"))
186
+ with tarfile.open(tarball_path, "w:gz") as tar:
187
+ tar.add(from_path, arcname=".")
188
+
189
+ size_mb = tarball_path.stat().st_size / (1024 * 1024)
190
+ console.print(f" Package size: {size_mb:.1f} MB")
191
+ console.print(f"[bold]Pushing[/bold] to catalog...")
192
+
193
+ # Upload
194
+ try:
195
+ import httpx
196
+ from ..config import get_value
197
+ catalog_url = (get_value("catalog") or "https://catalog.proximaintel.com").rstrip("/")
198
+ with open(tarball_path, "rb") as f:
199
+ r = httpx.post(
200
+ f"{catalog_url}/catalog/agents/{agent_id}/push?version={version}",
201
+ headers={"X-Master-Key": get_master_key()},
202
+ content=f.read(),
203
+ timeout=300,
204
+ )
205
+ if r.status_code >= 400:
206
+ console.print(f"[red]Push failed:[/red] {r.text[:300]}")
207
+ raise typer.Exit(1)
208
+ except Exception as e:
209
+ console.print(f"[red]Push failed:[/red] {e}")
210
+ raise typer.Exit(1)
211
+ finally:
212
+ tarball_path.unlink(missing_ok=True)
213
+
214
+ console.print(f"[green]✓[/green] Pushed {agent_id}@{version} to catalog")
215
+
216
+
217
+ @app.command("license")
218
+ def check_license():
219
+ """Check current license entitlements."""
220
+ if not get_license_key() and not get_master_key():
221
+ console.print("[yellow]No license key configured.[/yellow]")
222
+ console.print(" Run: prox login --license-key <key>")
223
+ return
224
+
225
+ if get_master_key():
226
+ console.print("[bold green]Master key active[/bold green] — full catalog access")
227
+ return
228
+
229
+ try:
230
+ data = catalog_get("/catalog/license")
231
+ except APIError as e:
232
+ console.print(f"[red]Error:[/red] {e.detail}")
233
+ raise typer.Exit(1)
234
+
235
+ console.print(f"\n[bold]License:[/bold] {data.get('client_id', '—')}")
236
+ console.print(f" Tier: {data.get('tier', '—')}")
237
+ console.print(f" Expires: {data.get('expires', '—')}")
238
+ console.print(f" Valid: {'[green]Yes[/green]' if data.get('valid') else '[red]No[/red]'}")
239
+ console.print(f"\n [bold]Licensed agents ({len(data.get('agents', []))}):[/bold]")
240
+ for a in data.get("agents", []):
241
+ console.print(f" • {a}")
242
+ console.print()
@@ -0,0 +1,63 @@
1
+ """prox governance — audit logs and platform metrics."""
2
+
3
+ from typing import Optional
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from ..api import gateway_get, APIError
8
+
9
+ app = typer.Typer(help="View governance logs and stats.")
10
+ console = Console()
11
+
12
+
13
+ @app.command("logs")
14
+ def logs(
15
+ domain: Optional[str] = typer.Option(None, "--domain", "-d"),
16
+ limit: int = typer.Option(20, "--limit", "-n"),
17
+ ):
18
+ """View audit logs."""
19
+ try:
20
+ params = {"limit": limit}
21
+ if domain:
22
+ params["domain"] = domain
23
+ data = gateway_get("/governance/logs", params=params)
24
+ entries = data.get("logs", data.get("entries", []))
25
+ if not entries:
26
+ console.print("[dim]No logs.[/dim]")
27
+ return
28
+ table = Table(title=f"Governance Logs (last {limit})")
29
+ table.add_column("Time", style="dim")
30
+ table.add_column("Type")
31
+ table.add_column("Agent")
32
+ table.add_column("User")
33
+ table.add_column("Tokens", justify="right")
34
+ for e in entries[:limit]:
35
+ table.add_row(
36
+ e.get("timestamp", "")[:19],
37
+ e.get("type", "query"),
38
+ e.get("agent_id", ""),
39
+ e.get("user", ""),
40
+ str(e.get("tokens_used", "")),
41
+ )
42
+ console.print(table)
43
+ except APIError as e:
44
+ console.print(f"[red]Error:[/red] {e.detail}")
45
+ raise typer.Exit(1)
46
+
47
+
48
+ @app.command("stats")
49
+ def stats(domain: Optional[str] = typer.Option(None, "--domain", "-d")):
50
+ """View platform stats."""
51
+ try:
52
+ params = {"domain": domain} if domain else {}
53
+ data = gateway_get("/governance/stats", params=params)
54
+ console.print(f"\n [bold]Platform Stats[/bold]{' (' + domain + ')' if domain else ''}\n")
55
+ console.print(f" Total queries: {data.get('total_queries', 0)}")
56
+ console.print(f" Total tokens: {data.get('total_tokens', 0):,}")
57
+ console.print(f" Total cost: ${data.get('total_cost', 0):.2f}")
58
+ console.print(f" Avg duration: {data.get('avg_duration_ms', 0):.0f}ms")
59
+ console.print(f" Agents active: {data.get('agents_active', 0)}")
60
+ console.print()
61
+ except APIError as e:
62
+ console.print(f"[red]Error:[/red] {e.detail}")
63
+ raise typer.Exit(1)