arqera-cli 0.2.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.
arqera_cli/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """ARQERA CLI -- AI governance from your terminal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from arqera_cli.cli import main
6
+
7
+ __all__ = ["main"]
8
+ __version__ = "0.2.0"
arqera_cli/cli.py ADDED
@@ -0,0 +1,1341 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ARQERA CLI -- command-line interface for the ARQERA platform.
4
+
5
+ Usage:
6
+ arqera login Authenticate with your API key
7
+ arqera whoami Check identity and connection
8
+ arqera chat [MESSAGE] Chat with Ore (streaming, interactive)
9
+ arqera briefing View today's briefing
10
+ arqera governance status View governance health
11
+ arqera governance evaluate Evaluate an action
12
+ arqera agents list List available agents
13
+ arqera agents run ID Run an agent
14
+ arqera evidence search Search the evidence trail
15
+ arqera dimensions View dimension scores
16
+ arqera init Initialize a new Arqera project
17
+ arqera deploy Deploy a workflow manifest
18
+ arqera validate Validate an agent manifest
19
+ arqera publish Publish an agent to the marketplace
20
+ arqera mcp install Install ARQERA MCP server for Claude
21
+
22
+ Examples:
23
+ arqera login --api-key ak_live_abc123
24
+ arqera chat "What's my trust score?"
25
+ arqera briefing
26
+ arqera governance evaluate --action email.send --description "Send newsletter"
27
+ arqera evidence search --type governance_evaluation --limit 10
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ import os
35
+ import shutil
36
+ import sys
37
+ import time
38
+ import urllib.request
39
+ import webbrowser
40
+ from pathlib import Path
41
+ from urllib.error import HTTPError, URLError
42
+ from typing import Any, Dict, Iterator, Optional
43
+
44
+
45
+ # =============================================================================
46
+ # CONFIGURATION
47
+ # =============================================================================
48
+
49
+ ARQERA_DIR = Path.home() / ".arqera"
50
+ CREDENTIALS_FILE = ARQERA_DIR / "credentials.json"
51
+ SWARM_KEY_FILE = ARQERA_DIR / "swarm-key"
52
+ DEFAULT_BASE_URL = "https://api.arqera.io/api"
53
+ MANIFEST_FILENAME = "arqera.yaml"
54
+
55
+ TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
56
+
57
+ MANIFEST_TEMPLATE = """\
58
+ # Arqera Project Manifest
59
+ # Docs: https://docs.arqera.io/sdk/manifest
60
+
61
+ version: "0.2"
62
+ intent:
63
+ id: {project_name}.main
64
+ title: "{project_title}"
65
+ description: "{project_description}"
66
+ parameters: {{}}
67
+ policy:
68
+ approvals_required: false
69
+ approval_level: "standard"
70
+ data_access:
71
+ - "read:protocol"
72
+ - "write:evidence"
73
+ workflow:
74
+ id: "{project_name}.v1"
75
+ steps:
76
+ - id: execute
77
+ action:
78
+ type: execute
79
+ module: core
80
+ params: {{}}
81
+ agent:
82
+ id: "{project_name}-agent"
83
+ name: "{project_title} Agent"
84
+ capabilities:
85
+ - "text-generation"
86
+ context:
87
+ tenant_id: "{{{{ env.ARQERA_TENANT_ID }}}}"
88
+ user_id: "{{{{ env.ARQERA_USER_ID }}}}"
89
+ surface: "cli"
90
+ """
91
+
92
+
93
+ # =============================================================================
94
+ # HELPERS
95
+ # =============================================================================
96
+
97
+ def _api_request(
98
+ path: str,
99
+ method: str = "GET",
100
+ body: Optional[Dict[str, Any]] = None,
101
+ api_key: Optional[str] = None,
102
+ base_url: Optional[str] = None,
103
+ timeout: int = 30,
104
+ ) -> Any:
105
+ """Make an API request to the Arqera platform."""
106
+ url = f"{(base_url or DEFAULT_BASE_URL).rstrip('/')}{path}"
107
+ data = json.dumps(body).encode("utf-8") if body else None
108
+ req = urllib.request.Request(url, data=data, method=method)
109
+ req.add_header("Content-Type", "application/json")
110
+ req.add_header("Accept", "application/json")
111
+ req.add_header("User-Agent", "arqera-cli/0.2.0")
112
+
113
+ if api_key:
114
+ req.add_header("X-API-Key", api_key)
115
+
116
+ try:
117
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
118
+ payload = resp.read().decode("utf-8")
119
+ return json.loads(payload) if payload else None
120
+ except HTTPError as exc:
121
+ body_text = exc.read().decode("utf-8") if exc.fp else ""
122
+ return {"error": True, "status": exc.code, "message": body_text}
123
+ except URLError as exc:
124
+ return {"error": True, "status": 0, "message": str(exc.reason)}
125
+
126
+
127
+ def _stream_sse(
128
+ path: str,
129
+ body: Dict[str, Any],
130
+ api_key: str,
131
+ base_url: str,
132
+ ) -> Iterator[str]:
133
+ """Stream SSE response, yielding text chunks."""
134
+ try:
135
+ import httpx
136
+ except ImportError:
137
+ # Fall back to non-streaming if httpx not available
138
+ result = _api_request(path, method="POST", body=body, api_key=api_key, base_url=base_url)
139
+ if isinstance(result, dict):
140
+ content = (
141
+ result.get("response")
142
+ or result.get("message")
143
+ or result.get("content")
144
+ or result.get("reply")
145
+ )
146
+ if content:
147
+ yield content
148
+ else:
149
+ yield json.dumps(result, indent=2)
150
+ return
151
+
152
+ url = f"{base_url.rstrip('/')}{path}"
153
+ headers = {
154
+ "Content-Type": "application/json",
155
+ "Accept": "text/event-stream",
156
+ "X-API-Key": api_key,
157
+ "User-Agent": "arqera-cli/0.2.0",
158
+ }
159
+
160
+ with httpx.Client(timeout=60) as http:
161
+ with http.stream("POST", url, json=body, headers=headers) as response:
162
+ if response.status_code != 200:
163
+ error_body = response.read().decode("utf-8")
164
+ yield f"Error ({response.status_code}): {error_body}"
165
+ return
166
+
167
+ for line in response.iter_lines():
168
+ if not line:
169
+ continue
170
+ if line.startswith("data: "):
171
+ payload = line[6:]
172
+ if payload == "[DONE]":
173
+ return
174
+ try:
175
+ event = json.loads(payload)
176
+ chunk = (
177
+ event.get("chunk")
178
+ or event.get("delta", {}).get("content")
179
+ or event.get("content")
180
+ or event.get("text")
181
+ or ""
182
+ )
183
+ if chunk:
184
+ yield chunk
185
+ except json.JSONDecodeError:
186
+ yield payload
187
+ else:
188
+ try:
189
+ data = json.loads(line)
190
+ if data.get("type") == "chunk":
191
+ yield data.get("chunk", "")
192
+ elif data.get("type") == "error":
193
+ yield f"\nError: {data.get('error', 'Unknown')}"
194
+ return
195
+ except json.JSONDecodeError:
196
+ yield line
197
+
198
+
199
+ def _load_credentials() -> Dict[str, Any]:
200
+ """Load stored credentials."""
201
+ if CREDENTIALS_FILE.exists():
202
+ return json.loads(CREDENTIALS_FILE.read_text())
203
+ return {}
204
+
205
+
206
+ def _save_credentials(creds: Dict[str, Any]) -> None:
207
+ """Save credentials to disk."""
208
+ ARQERA_DIR.mkdir(parents=True, exist_ok=True)
209
+ CREDENTIALS_FILE.write_text(json.dumps(creds, indent=2))
210
+ CREDENTIALS_FILE.chmod(0o600)
211
+
212
+
213
+ def _get_api_key() -> Optional[str]:
214
+ """Get API key from environment, credentials, or swarm key file."""
215
+ # Environment variable takes precedence
216
+ env_key = os.environ.get("ARQERA_API_KEY")
217
+ if env_key:
218
+ return env_key
219
+ creds = _load_credentials()
220
+ if creds.get("api_key"):
221
+ return creds["api_key"]
222
+ if SWARM_KEY_FILE.exists():
223
+ return SWARM_KEY_FILE.read_text().strip()
224
+ return None
225
+
226
+
227
+ def _get_base_url() -> str:
228
+ """Get base URL from environment or credentials."""
229
+ env_url = os.environ.get("ARQERA_BASE_URL")
230
+ if env_url:
231
+ return env_url
232
+ creds = _load_credentials()
233
+ return creds.get("base_url", DEFAULT_BASE_URL)
234
+
235
+
236
+ def _print_error(msg: str) -> None:
237
+ """Print an error message."""
238
+ print(f"\033[31mError:\033[0m {msg}", file=sys.stderr)
239
+
240
+
241
+ def _print_success(msg: str) -> None:
242
+ """Print a success message."""
243
+ print(f"\033[32mOK:\033[0m {msg}")
244
+
245
+
246
+ def _print_json(data: Any) -> None:
247
+ """Print formatted JSON."""
248
+ print(json.dumps(data, indent=2, default=str))
249
+
250
+
251
+ def _require_auth() -> tuple[str, str]:
252
+ """Get API key and base URL, or exit with error."""
253
+ api_key = _get_api_key()
254
+ if not api_key:
255
+ _print_error("Not authenticated. Run 'arqera login' or set ARQERA_API_KEY.")
256
+ sys.exit(1)
257
+ return api_key, _get_base_url()
258
+
259
+
260
+ # =============================================================================
261
+ # COMMANDS
262
+ # =============================================================================
263
+
264
+ def cmd_init(args: argparse.Namespace) -> int:
265
+ """Initialize a new Arqera project."""
266
+ project_dir = Path(args.directory or ".").resolve()
267
+ project_name = args.name or project_dir.name
268
+ project_title = project_name.replace("-", " ").replace("_", " ").title()
269
+
270
+ if args.template:
271
+ template_file = TEMPLATES_DIR / f"intent-{args.template}.yaml"
272
+ if not template_file.exists():
273
+ _print_error(f"Template '{args.template}' not found.")
274
+ available = [f.stem.replace("intent-", "") for f in TEMPLATES_DIR.glob("intent-*.yaml")]
275
+ if available:
276
+ print(f"Available templates: {', '.join(available)}")
277
+ return 1
278
+ manifest_dest = project_dir / MANIFEST_FILENAME
279
+ project_dir.mkdir(parents=True, exist_ok=True)
280
+ shutil.copy2(template_file, manifest_dest)
281
+ _print_success(f"Initialized from template '{args.template}' at {manifest_dest}")
282
+ else:
283
+ manifest_path = project_dir / MANIFEST_FILENAME
284
+ if manifest_path.exists() and not args.force:
285
+ _print_error(f"{MANIFEST_FILENAME} already exists. Use --force to overwrite.")
286
+ return 1
287
+
288
+ project_dir.mkdir(parents=True, exist_ok=True)
289
+ content = MANIFEST_TEMPLATE.format(
290
+ project_name=project_name,
291
+ project_title=project_title,
292
+ project_description=f"{project_title} powered by Arqera",
293
+ )
294
+ manifest_path.write_text(content)
295
+ _print_success(f"Initialized project at {project_dir}")
296
+
297
+ print(f" Manifest: {MANIFEST_FILENAME}")
298
+ print(f" Project: {project_name}")
299
+ print()
300
+ print("Next steps:")
301
+ print(" 1. Edit arqera.yaml to configure your workflow")
302
+ print(" 2. Run 'arqera login' to authenticate")
303
+ print(" 3. Run 'arqera deploy' to deploy your workflow")
304
+ return 0
305
+
306
+
307
+ def cmd_login(args: argparse.Namespace) -> int:
308
+ """Authenticate via browser (device code flow) or API key."""
309
+ base_url = args.base_url or DEFAULT_BASE_URL
310
+
311
+ if args.api_key:
312
+ return _login_with_api_key(args.api_key, base_url)
313
+
314
+ return _login_with_device_code(base_url)
315
+
316
+
317
+ def _login_with_api_key(api_key: str, base_url: str) -> int:
318
+ """Login with a provided API key."""
319
+ result = _api_request("/diagnostic/whoami", api_key=api_key, base_url=base_url)
320
+ if isinstance(result, dict) and result.get("error"):
321
+ _print_error(f"Authentication failed: {result.get('message', 'Unknown error')}")
322
+ return 1
323
+
324
+ _save_credentials({
325
+ "api_key": api_key,
326
+ "base_url": base_url,
327
+ "user": result.get("user") if isinstance(result, dict) else None,
328
+ "email": result.get("email") if isinstance(result, dict) else None,
329
+ "logged_in_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
330
+ })
331
+
332
+ user_name = result.get("user", "unknown") if isinstance(result, dict) else "unknown"
333
+ _print_success(f"Logged in as {user_name}")
334
+ print(f" Credentials saved to {CREDENTIALS_FILE}")
335
+ return 0
336
+
337
+
338
+ def _login_with_device_code(base_url: str) -> int:
339
+ """Login via browser-based device code flow."""
340
+ result = _api_request("/auth/device/code", method="POST", body={}, base_url=base_url)
341
+ if isinstance(result, dict) and result.get("error"):
342
+ _print_error(f"Failed to start login: {result.get('message', 'Unknown error')}")
343
+ print("Tip: Use 'arqera login --api-key YOUR_KEY' for non-interactive login.")
344
+ return 1
345
+
346
+ device_code = result.get("device_code", "")
347
+ verification_url = result.get("verification_url", "")
348
+ expires_in = result.get("expires_in", 900)
349
+ interval = result.get("interval", 2)
350
+
351
+ print("Opening browser to authenticate...")
352
+ print(f" If browser doesn't open, visit: {verification_url}")
353
+ print(f" Your code: {device_code}")
354
+ print()
355
+
356
+ try:
357
+ webbrowser.open(verification_url)
358
+ except Exception:
359
+ pass
360
+
361
+ print("Waiting for authentication...", end="", flush=True)
362
+ deadline = time.time() + expires_in
363
+ while time.time() < deadline:
364
+ time.sleep(interval)
365
+ print(".", end="", flush=True)
366
+
367
+ poll_result = _api_request(
368
+ "/auth/device/token",
369
+ method="POST",
370
+ body={"device_code": device_code},
371
+ base_url=base_url,
372
+ )
373
+
374
+ if isinstance(poll_result, dict):
375
+ status = poll_result.get("status", "")
376
+ if status == "complete":
377
+ print()
378
+ api_key = poll_result.get("api_key") or poll_result.get("access_token")
379
+ if not api_key:
380
+ _print_error("Authentication succeeded but no credentials returned.")
381
+ return 1
382
+
383
+ whoami = _api_request("/diagnostic/whoami", api_key=api_key, base_url=base_url)
384
+ user_name = "unknown"
385
+ email = None
386
+ if isinstance(whoami, dict) and not whoami.get("error"):
387
+ user_name = whoami.get("user", "unknown")
388
+ email = whoami.get("email")
389
+
390
+ _save_credentials({
391
+ "api_key": api_key,
392
+ "base_url": base_url,
393
+ "user": user_name,
394
+ "email": email,
395
+ "logged_in_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
396
+ })
397
+
398
+ _print_success(f"Logged in as {email or user_name}")
399
+ print(f" Credentials saved to {CREDENTIALS_FILE}")
400
+ return 0
401
+
402
+ elif status == "expired":
403
+ print()
404
+ _print_error("Device code expired. Please try again.")
405
+ return 1
406
+
407
+ print()
408
+ _print_error("Authentication timed out. Please try again.")
409
+ return 1
410
+
411
+
412
+ def cmd_whoami(args: argparse.Namespace) -> int:
413
+ """Check identity and swarm connection."""
414
+ api_key, base_url = _require_auth()
415
+
416
+ result = _api_request("/diagnostic/whoami", api_key=api_key, base_url=base_url)
417
+
418
+ if isinstance(result, dict) and result.get("error"):
419
+ print("Not connected.")
420
+ _print_error(result.get("message", "Connection failed"))
421
+ return 1
422
+
423
+ if not isinstance(result, dict) or not result.get("connected"):
424
+ print("Not connected.")
425
+ return 1
426
+
427
+ is_admin = result.get("admin", False)
428
+ user_name = result.get("user", "unknown")
429
+
430
+ if is_admin:
431
+ print(f"Authenticated as {user_name} (admin).")
432
+ else:
433
+ print(f"Authenticated as {user_name}.")
434
+
435
+ if args.verbose:
436
+ print(f" Email: {result.get('email', 'N/A')}")
437
+ print(f" Actor: {result.get('actor_type', 'N/A')}")
438
+ print(f" Admin: {is_admin}")
439
+ print(f" API: {base_url}")
440
+
441
+ return 0
442
+
443
+
444
+ def cmd_chat(args: argparse.Namespace) -> int:
445
+ """Chat with Ore from the terminal (streaming)."""
446
+ api_key, base_url = _require_auth()
447
+ session_id = f"cli-{int(time.time())}"
448
+
449
+ if args.message:
450
+ return _chat_send(args.message, session_id, api_key, base_url, stream=not args.no_stream)
451
+
452
+ # Interactive REPL mode
453
+ print("Ore chat (Ctrl+C to exit)")
454
+ print()
455
+ try:
456
+ while True:
457
+ try:
458
+ message = input("\033[36myou>\033[0m ").strip()
459
+ except EOFError:
460
+ print()
461
+ break
462
+ if not message:
463
+ continue
464
+ _chat_send(message, session_id, api_key, base_url, stream=not args.no_stream)
465
+ print() # blank line between exchanges
466
+ except KeyboardInterrupt:
467
+ print()
468
+
469
+ return 0
470
+
471
+
472
+ def _chat_send(
473
+ message: str,
474
+ session_id: str,
475
+ api_key: str,
476
+ base_url: str,
477
+ stream: bool = True,
478
+ ) -> int:
479
+ """Send a single chat message and print the response."""
480
+ if stream:
481
+ # Streaming mode -- tokens arrive in real time
482
+ print("\033[33more>\033[0m ", end="", flush=True)
483
+ body = {"message": message, "session_id": session_id, "stream": True}
484
+ got_content = False
485
+ for chunk in _stream_sse("/ara/chat", body, api_key, base_url):
486
+ print(chunk, end="", flush=True)
487
+ got_content = True
488
+ if got_content:
489
+ print() # newline after stream
490
+ return 0
491
+
492
+ # Non-streaming fallback
493
+ result = _api_request(
494
+ "/ara/chat",
495
+ method="POST",
496
+ body={"message": message, "session_id": session_id},
497
+ api_key=api_key,
498
+ base_url=base_url,
499
+ )
500
+
501
+ if isinstance(result, dict) and result.get("error"):
502
+ _print_error(result.get("message", "Chat request failed"))
503
+ return 1
504
+
505
+ if isinstance(result, dict):
506
+ reply = (
507
+ result.get("response")
508
+ or result.get("message")
509
+ or result.get("reply")
510
+ or result.get("content")
511
+ )
512
+ if reply:
513
+ print(f"\033[33more>\033[0m {reply}")
514
+ else:
515
+ _print_json(result)
516
+ else:
517
+ print(result)
518
+
519
+ return 0
520
+
521
+
522
+ def cmd_briefing(args: argparse.Namespace) -> int:
523
+ """View today's briefing."""
524
+ api_key, base_url = _require_auth()
525
+
526
+ result = _api_request(
527
+ "/page-data/home/briefing",
528
+ api_key=api_key,
529
+ base_url=base_url,
530
+ )
531
+
532
+ if isinstance(result, dict) and result.get("error"):
533
+ _print_error(f"Failed to fetch briefing: {result.get('message', 'Unknown error')}")
534
+ return 1
535
+
536
+ if not isinstance(result, dict):
537
+ print("No briefing data available.")
538
+ return 0
539
+
540
+ # Display greeting
541
+ greeting = result.get("greeting", "")
542
+ if greeting:
543
+ print(f"\033[1m{greeting}\033[0m")
544
+ print()
545
+
546
+ # Display sections
547
+ sections = result.get("sections", [])
548
+ for section in sections:
549
+ title = section.get("title", "")
550
+ summary = section.get("summary", "")
551
+ if title:
552
+ print(f" \033[36m{title}\033[0m")
553
+ if summary:
554
+ print(f" {summary}")
555
+ items = section.get("items", [])
556
+ for item in items:
557
+ if isinstance(item, str):
558
+ print(f" - {item}")
559
+ elif isinstance(item, dict):
560
+ label = item.get("label", item.get("title", ""))
561
+ value = item.get("value", item.get("summary", ""))
562
+ if label:
563
+ print(f" - {label}: {value}")
564
+ print()
565
+
566
+ # Display energy
567
+ energy = result.get("energy", {})
568
+ if energy and isinstance(energy, dict):
569
+ level = energy.get("level", "")
570
+ label = energy.get("label", "")
571
+ if level or label:
572
+ print(f" Energy: {label or level}")
573
+
574
+ # Display dimensions summary
575
+ dimensions = result.get("dimensions", [])
576
+ if dimensions:
577
+ print()
578
+ print(" \033[36mDimensions\033[0m")
579
+ for dim in dimensions:
580
+ name = dim.get("label", dim.get("name", dim.get("dimension", "")))
581
+ score = dim.get("score", 0)
582
+ trend = dim.get("trend", "")
583
+ trend_arrow = {"up": "^", "down": "v", "stable": "-"}.get(trend, "")
584
+ bar_len = int(score * 20)
585
+ bar = "#" * bar_len + "." * (20 - bar_len)
586
+ print(f" {name:20s} [{bar}] {score:.0%} {trend_arrow}")
587
+
588
+ if args.json:
589
+ print()
590
+ _print_json(result)
591
+
592
+ return 0
593
+
594
+
595
+ def cmd_governance_status(args: argparse.Namespace) -> int:
596
+ """View governance health status."""
597
+ api_key, base_url = _require_auth()
598
+
599
+ # Fetch trust score
600
+ trust_result = _api_request("/ara/trust/score", api_key=api_key, base_url=base_url)
601
+ # Fetch compliance status
602
+ compliance_result = _api_request("/compliance", api_key=api_key, base_url=base_url)
603
+
604
+ print("\033[1mGovernance Status\033[0m")
605
+ print()
606
+
607
+ # Trust score
608
+ if isinstance(trust_result, dict) and not trust_result.get("error"):
609
+ overall = trust_result.get("overall", 0)
610
+ print(f" Trust Score: {overall:.0%}")
611
+ components = trust_result.get("components", {})
612
+ if components:
613
+ for name, value in components.items():
614
+ label = name.replace("_", " ").title()
615
+ print(f" {label}: {value:.0%}")
616
+ else:
617
+ print(" Trust Score: unavailable")
618
+
619
+ print()
620
+
621
+ # Compliance
622
+ if isinstance(compliance_result, dict) and not compliance_result.get("error"):
623
+ print(" Compliance:")
624
+ frameworks = compliance_result.get("frameworks", [])
625
+ if frameworks:
626
+ for fw in frameworks:
627
+ fw_name = fw.get("name", fw.get("framework", ""))
628
+ fw_status = fw.get("status", "")
629
+ print(f" {fw_name}: {fw_status}")
630
+ else:
631
+ status = compliance_result.get("status", "unknown")
632
+ print(f" Status: {status}")
633
+ else:
634
+ print(" Compliance: unavailable")
635
+
636
+ if args.json:
637
+ print()
638
+ _print_json({"trust": trust_result, "compliance": compliance_result})
639
+
640
+ return 0
641
+
642
+
643
+ def cmd_governance_evaluate(args: argparse.Namespace) -> int:
644
+ """Evaluate an action against governance laws."""
645
+ api_key, base_url = _require_auth()
646
+
647
+ body: Dict[str, Any] = {"action": args.action}
648
+ if args.description:
649
+ body["description"] = args.description
650
+ if args.context:
651
+ try:
652
+ body["context"] = json.loads(args.context)
653
+ except json.JSONDecodeError:
654
+ _print_error("Invalid JSON for --context")
655
+ return 1
656
+
657
+ result = _api_request(
658
+ "/ara/governance/evaluate",
659
+ method="POST",
660
+ body=body,
661
+ api_key=api_key,
662
+ base_url=base_url,
663
+ )
664
+
665
+ if isinstance(result, dict) and result.get("error"):
666
+ _print_error(f"Evaluation failed: {result.get('message', 'Unknown error')}")
667
+ return 1
668
+
669
+ if not isinstance(result, dict):
670
+ _print_json(result)
671
+ return 0
672
+
673
+ verdict = result.get("verdict", "unknown")
674
+ confidence = result.get("confidence", 0)
675
+ reasons = result.get("reasons", [])
676
+
677
+ # Colour the verdict
678
+ colour = {"proceed": "\033[32m", "review": "\033[33m", "deny": "\033[31m"}.get(
679
+ verdict, "\033[0m"
680
+ )
681
+
682
+ print(f" Verdict: {colour}{verdict}\033[0m")
683
+ print(f" Confidence: {confidence:.0%}")
684
+ if reasons:
685
+ print(" Reasons:")
686
+ for reason in reasons:
687
+ print(f" - {reason}")
688
+
689
+ laws = result.get("laws_evaluated", [])
690
+ if laws:
691
+ print(f" Laws checked: {', '.join(laws)}")
692
+
693
+ if args.json:
694
+ print()
695
+ _print_json(result)
696
+
697
+ return 0
698
+
699
+
700
+ def cmd_evidence_search(args: argparse.Namespace) -> int:
701
+ """Search the evidence trail."""
702
+ api_key, base_url = _require_auth()
703
+
704
+ params = f"?page=1&page_size={args.limit}"
705
+ if args.type:
706
+ params += f"&artifact_type={args.type}"
707
+ if args.start_date:
708
+ params += f"&start_date={args.start_date}"
709
+ if args.end_date:
710
+ params += f"&end_date={args.end_date}"
711
+
712
+ result = _api_request(
713
+ f"/ara/evidence{params}",
714
+ api_key=api_key,
715
+ base_url=base_url,
716
+ )
717
+
718
+ if isinstance(result, dict) and result.get("error"):
719
+ _print_error(f"Search failed: {result.get('message', 'Unknown error')}")
720
+ return 1
721
+
722
+ artifacts: list[Any] = []
723
+ if isinstance(result, list):
724
+ artifacts = result
725
+ elif isinstance(result, dict):
726
+ artifacts = result.get("items", result.get("artifacts", []))
727
+
728
+ if not artifacts:
729
+ print("No evidence artifacts found.")
730
+ return 0
731
+
732
+ print(f" Found {len(artifacts)} artifact(s):")
733
+ print()
734
+ for artifact in artifacts:
735
+ art_id = artifact.get("id", "")
736
+ art_type = artifact.get("artifact_type", "")
737
+ created = artifact.get("created_at", "")
738
+ print(f" [{created}] {art_type} (#{art_id})")
739
+ payload = artifact.get("payload", {})
740
+ if payload:
741
+ summary = json.dumps(payload, default=str)
742
+ if len(summary) > 120:
743
+ summary = summary[:117] + "..."
744
+ print(f" {summary}")
745
+
746
+ return 0
747
+
748
+
749
+ def cmd_dimensions(args: argparse.Namespace) -> int:
750
+ """View dimension scores."""
751
+ api_key, base_url = _require_auth()
752
+
753
+ result = _api_request("/briefing/dimensions", api_key=api_key, base_url=base_url)
754
+
755
+ if isinstance(result, dict) and result.get("error"):
756
+ _print_error(f"Failed to fetch dimensions: {result.get('message', 'Unknown error')}")
757
+ return 1
758
+
759
+ items: list[Any] = []
760
+ if isinstance(result, list):
761
+ items = result
762
+ elif isinstance(result, dict):
763
+ items = result.get("dimensions", result.get("items", []))
764
+
765
+ if not items:
766
+ print("No dimension data available.")
767
+ return 0
768
+
769
+ print("\033[1mLife Dimensions\033[0m")
770
+ print()
771
+
772
+ for dim in items:
773
+ name = dim.get("label", dim.get("name", dim.get("dimension", "")))
774
+ score = dim.get("score", 0)
775
+ trend = dim.get("trend", "stable")
776
+ conductance = dim.get("conductance", 0)
777
+ trend_arrow = {"up": "^", "down": "v", "stable": "-"}.get(trend, "")
778
+ bar_len = int(score * 20)
779
+ bar = "#" * bar_len + "." * (20 - bar_len)
780
+ print(f" {name:20s} [{bar}] {score:.0%} {trend_arrow} (c={conductance:.2f})")
781
+
782
+ if args.json:
783
+ print()
784
+ _print_json(result)
785
+
786
+ return 0
787
+
788
+
789
+ def cmd_deploy(args: argparse.Namespace) -> int:
790
+ """Deploy a workflow manifest to the platform."""
791
+ manifest_path = Path(args.manifest or MANIFEST_FILENAME)
792
+ if not manifest_path.exists():
793
+ _print_error(f"Manifest not found: {manifest_path}")
794
+ print("Run 'arqera init' to create one.")
795
+ return 1
796
+
797
+ api_key, base_url = _require_auth()
798
+ manifest_content = manifest_path.read_text()
799
+
800
+ if args.dry_run:
801
+ print("[DRY RUN] Would deploy manifest:")
802
+ print(f" File: {manifest_path}")
803
+ print(f" API: {base_url}")
804
+ print(f" Size: {len(manifest_content)} bytes")
805
+ return 0
806
+
807
+ result = _api_request(
808
+ "/v1/protocol/manifest",
809
+ method="POST",
810
+ body={"manifest_yaml": manifest_content, "status": "active"},
811
+ api_key=api_key,
812
+ base_url=base_url,
813
+ )
814
+
815
+ if isinstance(result, dict) and result.get("error"):
816
+ _print_error(f"Deploy failed: {result.get('message', 'Unknown error')}")
817
+ return 1
818
+
819
+ _print_success("Workflow deployed successfully.")
820
+ if isinstance(result, dict):
821
+ if result.get("id"):
822
+ print(f" Manifest ID: {result['id']}")
823
+ if result.get("version"):
824
+ print(f" Version: {result['version']}")
825
+ return 0
826
+
827
+
828
+ def cmd_logs(args: argparse.Namespace) -> int:
829
+ """Stream execution logs."""
830
+ api_key, base_url = _require_auth()
831
+ params = f"?limit={args.limit}"
832
+ if args.workflow_id:
833
+ params += f"&workflow_id={args.workflow_id}"
834
+
835
+ result = _api_request(
836
+ f"/v1/protocol/executions{params}",
837
+ api_key=api_key,
838
+ base_url=base_url,
839
+ )
840
+
841
+ if isinstance(result, dict) and result.get("error"):
842
+ _print_error(f"Failed to fetch logs: {result.get('message', 'Unknown error')}")
843
+ return 1
844
+
845
+ entries = []
846
+ if isinstance(result, list):
847
+ entries = result
848
+ elif isinstance(result, dict) and result.get("items"):
849
+ entries = result["items"]
850
+
851
+ if entries:
852
+ for entry in entries:
853
+ ts = entry.get("created_at", "")
854
+ status = entry.get("status", "unknown")
855
+ wf_id = entry.get("workflow_id", "N/A")
856
+ print(f"[{ts}] {status:12s} {wf_id}")
857
+ else:
858
+ _print_json(result)
859
+
860
+ return 0
861
+
862
+
863
+ def cmd_agents_list(args: argparse.Namespace) -> int:
864
+ """List available agents."""
865
+ api_key, base_url = _require_auth()
866
+ result = _api_request("/v1/agents", api_key=api_key, base_url=base_url)
867
+
868
+ if isinstance(result, dict) and result.get("error"):
869
+ _print_error(f"Failed to list agents: {result.get('message', 'Unknown error')}")
870
+ return 1
871
+
872
+ agents = result if isinstance(result, list) else (
873
+ result.get("items", []) if isinstance(result, dict) else []
874
+ )
875
+
876
+ if not agents:
877
+ print("No agents available.")
878
+ return 0
879
+
880
+ print(f"{'ID':<30s} {'Name':<25s} {'Status':<10s} {'Type':<15s}")
881
+ print("-" * 80)
882
+ for agent in agents:
883
+ agent_id = str(agent.get("id", "N/A"))
884
+ name = agent.get("name", "N/A")
885
+ status = agent.get("status", "N/A")
886
+ agent_type = agent.get("type", agent.get("role", "N/A"))
887
+ print(f"{agent_id:<30s} {name:<25s} {status:<10s} {agent_type:<15s}")
888
+
889
+ return 0
890
+
891
+
892
+ def cmd_agents_run(args: argparse.Namespace) -> int:
893
+ """Run an agent."""
894
+ api_key, base_url = _require_auth()
895
+
896
+ body: Dict[str, Any] = {"agent_id": args.agent_id}
897
+ if args.input:
898
+ try:
899
+ body["input"] = json.loads(args.input)
900
+ except json.JSONDecodeError:
901
+ body["input"] = {"prompt": args.input}
902
+
903
+ if args.model:
904
+ body["model"] = args.model
905
+
906
+ result = _api_request(
907
+ "/v1/agents/run",
908
+ method="POST",
909
+ body=body,
910
+ api_key=api_key,
911
+ base_url=base_url,
912
+ )
913
+
914
+ if isinstance(result, dict) and result.get("error"):
915
+ _print_error(f"Agent execution failed: {result.get('message', 'Unknown error')}")
916
+ return 1
917
+
918
+ _print_json(result)
919
+ return 0
920
+
921
+
922
+ def _load_manifest_file(manifest_path: Path) -> Optional[Dict[str, Any]]:
923
+ """Load and parse a YAML or JSON manifest file, returning dict or None on error."""
924
+ content = manifest_path.read_text()
925
+ # Try YAML first (requires pyyaml), fall back to JSON
926
+ try:
927
+ import yaml
928
+ return yaml.safe_load(content)
929
+ except ImportError:
930
+ pass
931
+ except Exception:
932
+ pass
933
+ try:
934
+ return json.loads(content)
935
+ except json.JSONDecodeError as exc:
936
+ _print_error(f"Failed to parse manifest: {exc}")
937
+ return None
938
+
939
+
940
+ def _find_manifest_file(manifest_arg: Optional[str]) -> Optional[Path]:
941
+ """Find the manifest file. Checks explicit path, then arqera.yaml, then manifest.yaml."""
942
+ if manifest_arg:
943
+ p = Path(manifest_arg)
944
+ if p.exists():
945
+ return p
946
+ _print_error(f"Manifest not found: {p}")
947
+ return None
948
+ for name in ("arqera.yaml", "manifest.yaml"):
949
+ p = Path(name)
950
+ if p.exists():
951
+ return p
952
+ _print_error("No manifest file found. Expected arqera.yaml or manifest.yaml in current directory.")
953
+ return None
954
+
955
+
956
+ def cmd_validate(args: argparse.Namespace) -> int:
957
+ """Validate an agent manifest against the AgentManifest v1 schema."""
958
+ manifest_path = _find_manifest_file(getattr(args, "manifest", None))
959
+ if not manifest_path:
960
+ return 1
961
+
962
+ manifest_data = _load_manifest_file(manifest_path)
963
+ if manifest_data is None:
964
+ return 1
965
+
966
+ api_key, base_url = _require_auth()
967
+
968
+ result = _api_request(
969
+ "/marketplace/manifest/validate",
970
+ method="POST",
971
+ body={"manifest": manifest_data},
972
+ api_key=api_key,
973
+ base_url=base_url,
974
+ )
975
+
976
+ if isinstance(result, dict) and result.get("error"):
977
+ _print_error(f"Validation request failed: {result.get('message', 'Unknown error')}")
978
+ return 1
979
+
980
+ if not isinstance(result, dict):
981
+ _print_error("Unexpected response from validation endpoint.")
982
+ return 1
983
+
984
+ if result.get("valid"):
985
+ _print_success(f"Manifest is valid ({manifest_path})")
986
+ if args.json:
987
+ _print_json(result.get("manifest", {}))
988
+ return 0
989
+
990
+ errors = result.get("errors", [])
991
+ _print_error(f"Manifest validation failed ({len(errors)} error(s)):")
992
+ for err in errors:
993
+ print(f" - {err}")
994
+ return 1
995
+
996
+
997
+ def cmd_publish(args: argparse.Namespace) -> int:
998
+ """Validate and publish an agent manifest to the marketplace."""
999
+ manifest_path = _find_manifest_file(getattr(args, "manifest", None))
1000
+ if not manifest_path:
1001
+ return 1
1002
+
1003
+ manifest_data = _load_manifest_file(manifest_path)
1004
+ if manifest_data is None:
1005
+ return 1
1006
+
1007
+ api_key, base_url = _require_auth()
1008
+
1009
+ # Step 1: Validate first
1010
+ val_result = _api_request(
1011
+ "/marketplace/manifest/validate",
1012
+ method="POST",
1013
+ body={"manifest": manifest_data},
1014
+ api_key=api_key,
1015
+ base_url=base_url,
1016
+ )
1017
+
1018
+ if isinstance(val_result, dict) and not val_result.get("valid"):
1019
+ errors = val_result.get("errors", [])
1020
+ _print_error(f"Manifest validation failed ({len(errors)} error(s)):")
1021
+ for err in errors:
1022
+ print(f" - {err}")
1023
+ return 1
1024
+
1025
+ if isinstance(val_result, dict) and val_result.get("error"):
1026
+ _print_error(f"Validation request failed: {val_result.get('message', 'Unknown error')}")
1027
+ return 1
1028
+
1029
+ # Step 2: Publish to marketplace
1030
+ listing_body: Dict[str, Any] = {
1031
+ "name": manifest_data.get("name", "Unnamed Agent"),
1032
+ "description": manifest_data.get("listing", {}).get("description", ""),
1033
+ "type": manifest_data.get("slug", ""),
1034
+ "category": manifest_data.get("category", "utilities"),
1035
+ "manifest": manifest_data,
1036
+ }
1037
+
1038
+ # Map pricing from manifest
1039
+ pricing = manifest_data.get("pricing", {})
1040
+ if pricing:
1041
+ listing_body["price_type"] = pricing.get("type", "free")
1042
+ listing_body["price_cents"] = pricing.get("cents", 0)
1043
+
1044
+ result = _api_request(
1045
+ "/marketplace/listings",
1046
+ method="POST",
1047
+ body=listing_body,
1048
+ api_key=api_key,
1049
+ base_url=base_url,
1050
+ )
1051
+
1052
+ if isinstance(result, dict) and result.get("error"):
1053
+ _print_error(f"Publish failed: {result.get('message', 'Unknown error')}")
1054
+ return 1
1055
+
1056
+ _print_success("Agent published to marketplace.")
1057
+ if isinstance(result, dict):
1058
+ if result.get("id"):
1059
+ print(f" Listing ID: {result['id']}")
1060
+ if result.get("type"):
1061
+ print(f" Slug: {result['type']}")
1062
+ if result.get("status"):
1063
+ print(f" Status: {result['status']}")
1064
+ return 0
1065
+
1066
+
1067
+ def cmd_mcp_install(args: argparse.Namespace) -> int:
1068
+ """Install ARQERA MCP server for Claude Code / Claude Desktop."""
1069
+ api_key, base_url = _require_auth()
1070
+
1071
+ mcp_server_path = Path(__file__).resolve().parent.parent / "mcp" / "server.py"
1072
+ if not mcp_server_path.exists():
1073
+ _print_error(f"MCP server not found at {mcp_server_path}")
1074
+ return 1
1075
+
1076
+ mcp_config = {
1077
+ "command": sys.executable,
1078
+ "args": [str(mcp_server_path)],
1079
+ "env": {
1080
+ "ARQERA_API_KEY": api_key,
1081
+ "ARQERA_BASE_URL": base_url,
1082
+ },
1083
+ }
1084
+
1085
+ claude_config_path = Path.home() / ".claude" / "claude_desktop_config.json"
1086
+ claude_config_path.parent.mkdir(parents=True, exist_ok=True)
1087
+
1088
+ config: Dict[str, Any] = {}
1089
+ if claude_config_path.exists():
1090
+ try:
1091
+ config = json.loads(claude_config_path.read_text())
1092
+ except json.JSONDecodeError:
1093
+ config = {}
1094
+
1095
+ if "mcpServers" not in config:
1096
+ config["mcpServers"] = {}
1097
+
1098
+ config["mcpServers"]["arqera"] = mcp_config
1099
+ claude_config_path.write_text(json.dumps(config, indent=2))
1100
+
1101
+ _print_success("ARQERA MCP server installed for Claude Code")
1102
+ print(f" Config: {claude_config_path}")
1103
+ print(f" Server: {mcp_server_path}")
1104
+ print()
1105
+ print("Restart Claude Code to activate.")
1106
+ return 0
1107
+
1108
+
1109
+ # =============================================================================
1110
+ # ARGUMENT PARSER
1111
+ # =============================================================================
1112
+
1113
+ def build_parser() -> argparse.ArgumentParser:
1114
+ """Build the CLI argument parser."""
1115
+ parser = argparse.ArgumentParser(
1116
+ prog="arqera",
1117
+ description="ARQERA CLI -- AI governance from your terminal",
1118
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1119
+ epilog="""\
1120
+ Examples:
1121
+ arqera login --api-key ak_live_abc123
1122
+ arqera chat "What's my trust score?"
1123
+ arqera chat # interactive mode
1124
+ arqera briefing
1125
+ arqera governance status
1126
+ arqera governance evaluate --action email.send
1127
+ arqera dimensions
1128
+ arqera evidence search --type governance_evaluation
1129
+ arqera agents list
1130
+ arqera validate # validate arqera.yaml
1131
+ arqera publish # publish agent to marketplace
1132
+
1133
+ Documentation: https://docs.arqera.io/sdk/cli
1134
+ """,
1135
+ )
1136
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
1137
+
1138
+ # init
1139
+ p_init = subparsers.add_parser(
1140
+ "init", help="Initialize a new Arqera project",
1141
+ description="Create an arqera.yaml manifest in the target directory.",
1142
+ )
1143
+ p_init.add_argument("directory", nargs="?", help="Project directory (default: current)")
1144
+ p_init.add_argument("--name", "-n", help="Project name (default: directory name)")
1145
+ p_init.add_argument(
1146
+ "--template", "-t",
1147
+ help="Use a template (e.g. support-triage, workflow-automation)",
1148
+ )
1149
+ p_init.add_argument("--force", "-f", action="store_true", help="Overwrite existing manifest")
1150
+
1151
+ # login
1152
+ p_login = subparsers.add_parser(
1153
+ "login", help="Authenticate and store credentials",
1154
+ description="Log in with an API key or browser-based device code flow.",
1155
+ )
1156
+ p_login.add_argument("--api-key", "-k", help="API key (opens browser if not provided)")
1157
+ p_login.add_argument("--base-url", help=f"API base URL (default: {DEFAULT_BASE_URL})")
1158
+
1159
+ # whoami
1160
+ p_whoami = subparsers.add_parser(
1161
+ "whoami", help="Check identity and connection",
1162
+ description="Verify your authentication and display account details.",
1163
+ )
1164
+ p_whoami.add_argument("--verbose", "-v", action="store_true", help="Show detailed info")
1165
+
1166
+ # chat
1167
+ p_chat = subparsers.add_parser(
1168
+ "chat", help="Chat with Ore (streaming)",
1169
+ description="Send messages to Ore. Tokens stream in real time.\n"
1170
+ "Omit the message for interactive REPL mode.",
1171
+ )
1172
+ p_chat.add_argument("message", nargs="?", default=None, help="Message (omit for interactive)")
1173
+ p_chat.add_argument("--no-stream", action="store_true", help="Disable streaming")
1174
+
1175
+ # briefing
1176
+ p_briefing = subparsers.add_parser(
1177
+ "briefing", help="View today's briefing",
1178
+ description="Display your personalised daily briefing with\n"
1179
+ "greeting, key sections, energy, and dimension scores.",
1180
+ )
1181
+ p_briefing.add_argument("--json", action="store_true", help="Output raw JSON")
1182
+
1183
+ # governance
1184
+ p_governance = subparsers.add_parser("governance", help="Governance operations")
1185
+ gov_sub = p_governance.add_subparsers(dest="governance_command", help="Governance commands")
1186
+
1187
+ p_gov_status = gov_sub.add_parser(
1188
+ "status", help="View governance health",
1189
+ description="Display trust score, compliance status, and governance metrics.",
1190
+ )
1191
+ p_gov_status.add_argument("--json", action="store_true", help="Output raw JSON")
1192
+
1193
+ p_gov_eval = gov_sub.add_parser(
1194
+ "evaluate", help="Evaluate an action against governance laws",
1195
+ description="Submit an action for governance evaluation and see the verdict.",
1196
+ )
1197
+ p_gov_eval.add_argument("--action", "-a", required=True, help="Action identifier (e.g. email.send)")
1198
+ p_gov_eval.add_argument("--description", "-d", help="Human-readable description")
1199
+ p_gov_eval.add_argument("--context", "-c", help="Additional context as JSON string")
1200
+ p_gov_eval.add_argument("--json", action="store_true", help="Output raw JSON")
1201
+
1202
+ # evidence
1203
+ p_evidence = subparsers.add_parser("evidence", help="Evidence trail operations")
1204
+ evidence_sub = p_evidence.add_subparsers(dest="evidence_command", help="Evidence commands")
1205
+
1206
+ p_ev_search = evidence_sub.add_parser(
1207
+ "search", help="Search the evidence trail",
1208
+ description="Search evidence artifacts with optional filters.",
1209
+ )
1210
+ p_ev_search.add_argument("--type", "-t", help="Filter by artifact type")
1211
+ p_ev_search.add_argument("--start-date", help="Start date (ISO format)")
1212
+ p_ev_search.add_argument("--end-date", help="End date (ISO format)")
1213
+ p_ev_search.add_argument("--limit", "-l", type=int, default=20, help="Max results (default: 20)")
1214
+
1215
+ # dimensions
1216
+ p_dimensions = subparsers.add_parser(
1217
+ "dimensions", help="View dimension scores",
1218
+ description="Display scores for all life dimensions with trends\n"
1219
+ "and Physarum conductance values.",
1220
+ )
1221
+ p_dimensions.add_argument("--json", action="store_true", help="Output raw JSON")
1222
+
1223
+ # agents
1224
+ p_agents = subparsers.add_parser("agents", help="Manage agents")
1225
+ agents_sub = p_agents.add_subparsers(dest="agents_command", help="Agent commands")
1226
+
1227
+ agents_sub.add_parser("list", help="List available agents")
1228
+
1229
+ p_agents_run = agents_sub.add_parser("run", help="Run an agent")
1230
+ p_agents_run.add_argument("agent_id", help="Agent ID to execute")
1231
+ p_agents_run.add_argument("--input", "-i", help="Input JSON or text prompt")
1232
+ p_agents_run.add_argument("--model", help="Model override")
1233
+
1234
+ # validate
1235
+ p_validate = subparsers.add_parser(
1236
+ "validate", help="Validate an agent manifest",
1237
+ description="Validate arqera.yaml or manifest.yaml against the AgentManifest v1 schema.",
1238
+ )
1239
+ p_validate.add_argument("--manifest", "-m", help="Manifest file (default: arqera.yaml)")
1240
+ p_validate.add_argument("--json", action="store_true", help="Output validated manifest as JSON")
1241
+
1242
+ # publish
1243
+ p_publish = subparsers.add_parser(
1244
+ "publish", help="Publish an agent to the marketplace",
1245
+ description="Validate and submit an agent manifest as a marketplace listing.",
1246
+ )
1247
+ p_publish.add_argument("--manifest", "-m", help="Manifest file (default: arqera.yaml)")
1248
+
1249
+ # deploy
1250
+ p_deploy = subparsers.add_parser("deploy", help="Deploy a workflow manifest")
1251
+ p_deploy.add_argument("--manifest", "-m", help=f"Manifest file (default: {MANIFEST_FILENAME})")
1252
+ p_deploy.add_argument("--dry-run", action="store_true", help="Preview without deploying")
1253
+
1254
+ # logs
1255
+ p_logs = subparsers.add_parser("logs", help="Stream execution logs")
1256
+ p_logs.add_argument("--workflow-id", "-w", help="Filter by workflow ID")
1257
+ p_logs.add_argument("--limit", "-l", type=int, default=20, help="Number of entries (default: 20)")
1258
+
1259
+ # mcp
1260
+ p_mcp = subparsers.add_parser("mcp", help="MCP server management")
1261
+ mcp_sub = p_mcp.add_subparsers(dest="mcp_command", help="MCP commands")
1262
+ mcp_sub.add_parser("install", help="Install ARQERA MCP server for Claude Code")
1263
+
1264
+ return parser
1265
+
1266
+
1267
+ # =============================================================================
1268
+ # MAIN
1269
+ # =============================================================================
1270
+
1271
+ def main() -> int:
1272
+ """CLI entry point."""
1273
+ parser = build_parser()
1274
+ args = parser.parse_args()
1275
+
1276
+ if not args.command:
1277
+ parser.print_help()
1278
+ return 0
1279
+
1280
+ # Top-level commands
1281
+ top_commands = {
1282
+ "init": cmd_init,
1283
+ "login": cmd_login,
1284
+ "whoami": cmd_whoami,
1285
+ "chat": cmd_chat,
1286
+ "briefing": cmd_briefing,
1287
+ "validate": cmd_validate,
1288
+ "publish": cmd_publish,
1289
+ "deploy": cmd_deploy,
1290
+ "logs": cmd_logs,
1291
+ "dimensions": cmd_dimensions,
1292
+ }
1293
+
1294
+ if args.command in top_commands:
1295
+ return top_commands[args.command](args)
1296
+
1297
+ # Nested commands
1298
+ if args.command == "governance":
1299
+ if not getattr(args, "governance_command", None):
1300
+ parser.parse_args(["governance", "--help"])
1301
+ return 0
1302
+ gov_commands = {
1303
+ "status": cmd_governance_status,
1304
+ "evaluate": cmd_governance_evaluate,
1305
+ }
1306
+ return gov_commands[args.governance_command](args)
1307
+
1308
+ if args.command == "evidence":
1309
+ if not getattr(args, "evidence_command", None):
1310
+ parser.parse_args(["evidence", "--help"])
1311
+ return 0
1312
+ ev_commands = {
1313
+ "search": cmd_evidence_search,
1314
+ }
1315
+ return ev_commands[args.evidence_command](args)
1316
+
1317
+ if args.command == "mcp":
1318
+ if not getattr(args, "mcp_command", None):
1319
+ parser.parse_args(["mcp", "--help"])
1320
+ return 0
1321
+ mcp_commands = {
1322
+ "install": cmd_mcp_install,
1323
+ }
1324
+ return mcp_commands[args.mcp_command](args)
1325
+
1326
+ if args.command == "agents":
1327
+ if not getattr(args, "agents_command", None):
1328
+ parser.parse_args(["agents", "--help"])
1329
+ return 0
1330
+ agent_commands = {
1331
+ "list": cmd_agents_list,
1332
+ "run": cmd_agents_run,
1333
+ }
1334
+ return agent_commands[args.agents_command](args)
1335
+
1336
+ parser.print_help()
1337
+ return 1
1338
+
1339
+
1340
+ if __name__ == "__main__":
1341
+ sys.exit(main())
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: arqera-cli
3
+ Version: 0.2.0
4
+ Summary: ARQERA CLI -- AI governance from your terminal
5
+ Project-URL: Homepage, https://arqera.io
6
+ Project-URL: Repository, https://github.com/Arqera-IO/ARQERA
7
+ Project-URL: Documentation, https://docs.arqera.io/sdk/cli
8
+ Author-email: ARQERA <engineering@arqera.io>
9
+ License-Expression: Apache-2.0
10
+ Keywords: ai,arqera,cli,compliance,evidence,governance
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: streaming
26
+ Requires-Dist: httpx>=0.25.0; extra == 'streaming'
@@ -0,0 +1,6 @@
1
+ arqera_cli/__init__.py,sha256=q_5jq8UEgp0a9kgqetDF8l56HptFRs1-lTxyTOq4KGM,165
2
+ arqera_cli/cli.py,sha256=UVi99gueRsyyel4IQoSTseGZabPAnK7dsubd23lfXug,45229
3
+ arqera_cli-0.2.0.dist-info/METADATA,sha256=aKnTjbJhll8VklOw6w_l4GlE6_-RNfoWw5x3Qj3oO4Y,1123
4
+ arqera_cli-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
5
+ arqera_cli-0.2.0.dist-info/entry_points.txt,sha256=VR5C1jBjZSbmPs3FAih0v2z06y-24ufOhE21TqEqXoI,47
6
+ arqera_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ arqera = arqera_cli.cli:main