agoralia-cli 0.1.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.
- agoralia_cli-0.1.0/.gitignore +5 -0
- agoralia_cli-0.1.0/PKG-INFO +92 -0
- agoralia_cli-0.1.0/README.md +73 -0
- agoralia_cli-0.1.0/pyproject.toml +35 -0
- agoralia_cli-0.1.0/src/agoralia_cli/__init__.py +11 -0
- agoralia_cli-0.1.0/src/agoralia_cli/__main__.py +4 -0
- agoralia_cli-0.1.0/src/agoralia_cli/client.py +55 -0
- agoralia_cli-0.1.0/src/agoralia_cli/main.py +205 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agoralia-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agoralia CLI — the gated agent surface (MCP · REST · CLI) for compliant AI voice campaigns
|
|
5
|
+
Project-URL: Homepage, https://agoralia.app
|
|
6
|
+
Project-URL: Documentation, https://docs.agoralia.app
|
|
7
|
+
Project-URL: Source, https://github.com/giacomocavalcabo/agoralia-v2
|
|
8
|
+
Author: Agoralia
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agent,agoralia,ai,cli,mcp,voice
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Communications :: Telephony
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.24
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# agoralia-cli
|
|
21
|
+
|
|
22
|
+
The **Agoralia CLI** — the third agent surface (alongside MCP and the REST API) for running
|
|
23
|
+
compliant AI voice campaigns. It's a thin client over the *already-gated* REST API: same
|
|
24
|
+
policy, same human-approval flow, same audit. It adds nothing privileged — the gate lives
|
|
25
|
+
server-side; the CLI just speaks HTTP to it and renders the guided errors it returns.
|
|
26
|
+
|
|
27
|
+
Every call is tagged `X-Agent-Surface: cli`, so the action audit records that it came from
|
|
28
|
+
the CLI.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install agoralia-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Authenticate
|
|
37
|
+
|
|
38
|
+
The CLI uses a Bearer token (the same kind the MCP surface uses).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export AGORALIA_TOKEN=... # your Agoralia access token
|
|
42
|
+
export AGORALIA_API_URL=https://api.agoralia.app # optional (defaults to localhost:8000)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Generative dashboard pages (the agent composes a view; you see it at /pages/<slug>)
|
|
49
|
+
agoralia pages list
|
|
50
|
+
agoralia pages get q2-reactivation-calls
|
|
51
|
+
agoralia pages create ./spec.json
|
|
52
|
+
|
|
53
|
+
# Human "go" queue — approve/deny dangerous agent actions
|
|
54
|
+
agoralia approvals list
|
|
55
|
+
agoralia approvals decide <approval-id> approved
|
|
56
|
+
|
|
57
|
+
# Read-only audit of what your agents did
|
|
58
|
+
agoralia activity --limit 50
|
|
59
|
+
|
|
60
|
+
# Generic escape hatch to any gated route
|
|
61
|
+
agoralia call GET /campaigns
|
|
62
|
+
agoralia call POST /campaigns/<id>/start --data '{}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## The approval flow
|
|
66
|
+
|
|
67
|
+
Dangerous actions (launching a campaign, spending, deleting) are gated. When you hit one
|
|
68
|
+
without an approval, the CLI tells you exactly what to do:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
$ agoralia call POST /campaigns/abc/start
|
|
72
|
+
⚠ Approval required before this action can run.
|
|
73
|
+
approval_id: appr_123
|
|
74
|
+
→ A human must approve it (POST /approvals/{id}/decide), then retry with header X-Approval-Id.
|
|
75
|
+
Once approved, retry with: --approval-id appr_123
|
|
76
|
+
|
|
77
|
+
# a human approves it…
|
|
78
|
+
$ agoralia approvals decide appr_123 approved
|
|
79
|
+
|
|
80
|
+
# …then retry
|
|
81
|
+
$ agoralia call POST /campaigns/abc/start --approval-id appr_123
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
(Approval enforcement is controlled server-side by the `AGENT_APPROVAL_ENABLED` flag.)
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e .
|
|
90
|
+
python -m pytest tests/
|
|
91
|
+
python -m agoralia_cli --help # same as the `agoralia` command
|
|
92
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# agoralia-cli
|
|
2
|
+
|
|
3
|
+
The **Agoralia CLI** — the third agent surface (alongside MCP and the REST API) for running
|
|
4
|
+
compliant AI voice campaigns. It's a thin client over the *already-gated* REST API: same
|
|
5
|
+
policy, same human-approval flow, same audit. It adds nothing privileged — the gate lives
|
|
6
|
+
server-side; the CLI just speaks HTTP to it and renders the guided errors it returns.
|
|
7
|
+
|
|
8
|
+
Every call is tagged `X-Agent-Surface: cli`, so the action audit records that it came from
|
|
9
|
+
the CLI.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install agoralia-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Authenticate
|
|
18
|
+
|
|
19
|
+
The CLI uses a Bearer token (the same kind the MCP surface uses).
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export AGORALIA_TOKEN=... # your Agoralia access token
|
|
23
|
+
export AGORALIA_API_URL=https://api.agoralia.app # optional (defaults to localhost:8000)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Generative dashboard pages (the agent composes a view; you see it at /pages/<slug>)
|
|
30
|
+
agoralia pages list
|
|
31
|
+
agoralia pages get q2-reactivation-calls
|
|
32
|
+
agoralia pages create ./spec.json
|
|
33
|
+
|
|
34
|
+
# Human "go" queue — approve/deny dangerous agent actions
|
|
35
|
+
agoralia approvals list
|
|
36
|
+
agoralia approvals decide <approval-id> approved
|
|
37
|
+
|
|
38
|
+
# Read-only audit of what your agents did
|
|
39
|
+
agoralia activity --limit 50
|
|
40
|
+
|
|
41
|
+
# Generic escape hatch to any gated route
|
|
42
|
+
agoralia call GET /campaigns
|
|
43
|
+
agoralia call POST /campaigns/<id>/start --data '{}'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The approval flow
|
|
47
|
+
|
|
48
|
+
Dangerous actions (launching a campaign, spending, deleting) are gated. When you hit one
|
|
49
|
+
without an approval, the CLI tells you exactly what to do:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
$ agoralia call POST /campaigns/abc/start
|
|
53
|
+
⚠ Approval required before this action can run.
|
|
54
|
+
approval_id: appr_123
|
|
55
|
+
→ A human must approve it (POST /approvals/{id}/decide), then retry with header X-Approval-Id.
|
|
56
|
+
Once approved, retry with: --approval-id appr_123
|
|
57
|
+
|
|
58
|
+
# a human approves it…
|
|
59
|
+
$ agoralia approvals decide appr_123 approved
|
|
60
|
+
|
|
61
|
+
# …then retry
|
|
62
|
+
$ agoralia call POST /campaigns/abc/start --approval-id appr_123
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
(Approval enforcement is controlled server-side by the `AGENT_APPROVAL_ENABLED` flag.)
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install -e .
|
|
71
|
+
python -m pytest tests/
|
|
72
|
+
python -m agoralia_cli --help # same as the `agoralia` command
|
|
73
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agoralia-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agoralia CLI — the gated agent surface (MCP · REST · CLI) for compliant AI voice campaigns"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Agoralia" }]
|
|
13
|
+
keywords = ["agoralia", "voice", "ai", "agent", "mcp", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Communications :: Telephony",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["httpx>=0.24"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://agoralia.app"
|
|
25
|
+
Documentation = "https://docs.agoralia.app"
|
|
26
|
+
Source = "https://github.com/giacomocavalcabo/agoralia-v2"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
agoralia = "agoralia_cli.main:main"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/agoralia_cli"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = ["src/agoralia_cli", "README.md"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agoralia CLI — the third agent surface (after MCP and REST).
|
|
3
|
+
|
|
4
|
+
A thin client over the *already-gated* REST API: same policy core, same approval flow,
|
|
5
|
+
same audit. It adds nothing privileged — it just speaks HTTP to the gate and tags every
|
|
6
|
+
call `X-Agent-Surface: cli` so the audit knows the origin. The gating lives server-side;
|
|
7
|
+
the CLI only renders the guided errors the gate returns. See docs/AGENT_LAYER.md.
|
|
8
|
+
"""
|
|
9
|
+
from agoralia_cli.client import ApprovalRequired, CliClient, CliError
|
|
10
|
+
|
|
11
|
+
__all__ = ["CliClient", "CliError", "ApprovalRequired"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""HTTP client for the Agoralia CLI — a thin, gated client over the REST API."""
|
|
2
|
+
import httpx
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CliError(Exception):
|
|
6
|
+
"""A non-approval error from the API (with the server's message/detail)."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApprovalRequired(CliError):
|
|
10
|
+
"""The gate blocked a dangerous action pending human approval.
|
|
11
|
+
|
|
12
|
+
Carries the approval_id + remediation so the CLI can tell the user exactly what to
|
|
13
|
+
do (approve it, then retry with --approval-id). This is the guided-error contract
|
|
14
|
+
surfacing on the CLI, identical to MCP/REST.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, approval_id: str | None, remediation: list[str]):
|
|
18
|
+
self.approval_id = approval_id
|
|
19
|
+
self.remediation = remediation
|
|
20
|
+
super().__init__("Approval required before this action can run.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CliClient:
|
|
24
|
+
"""Speaks HTTP to the gated REST API. Auth = Bearer token; surface = cli."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, base_url: str, token: str, *, approval_id: str | None = None, timeout: float = 30.0):
|
|
27
|
+
if not token:
|
|
28
|
+
raise CliError("Not authenticated. Set AGORALIA_TOKEN (or pass --token).")
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
self.timeout = timeout
|
|
31
|
+
self.headers = {"Authorization": f"Bearer {token}", "X-Agent-Surface": "cli"}
|
|
32
|
+
if approval_id:
|
|
33
|
+
self.headers["X-Approval-Id"] = approval_id
|
|
34
|
+
|
|
35
|
+
def request(self, method: str, path: str, *, json: dict | None = None, params: dict | None = None) -> dict:
|
|
36
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
37
|
+
resp = client.request(method, f"{self.base_url}{path}", headers=self.headers, json=json, params=params)
|
|
38
|
+
return self._handle(resp)
|
|
39
|
+
|
|
40
|
+
def _handle(self, resp: httpx.Response) -> dict:
|
|
41
|
+
if resp.status_code == 204:
|
|
42
|
+
return {"ok": True}
|
|
43
|
+
try:
|
|
44
|
+
body = resp.json()
|
|
45
|
+
except Exception:
|
|
46
|
+
body = {}
|
|
47
|
+
if resp.is_success:
|
|
48
|
+
return body
|
|
49
|
+
|
|
50
|
+
detail = body.get("detail", body) if isinstance(body, dict) else body
|
|
51
|
+
# Guided error: a dangerous action awaiting human "go".
|
|
52
|
+
if isinstance(detail, dict) and detail.get("code") == "approval_required":
|
|
53
|
+
raise ApprovalRequired(detail.get("approval_id"), detail.get("remediation", []))
|
|
54
|
+
msg = detail.get("error") if isinstance(detail, dict) else detail
|
|
55
|
+
raise CliError(f"HTTP {resp.status_code}: {msg or 'request failed'}")
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agoralia CLI entrypoint. Thin gated client over the REST API.
|
|
3
|
+
|
|
4
|
+
agoralia auth login --token <TOKEN> # mint a token in the dashboard, then store it
|
|
5
|
+
# or: export AGORALIA_TOKEN=... # Bearer token (same as the MCP surface)
|
|
6
|
+
# AGORALIA_API_URL defaults to https://api.agoralia.app
|
|
7
|
+
|
|
8
|
+
agoralia pages list
|
|
9
|
+
agoralia pages get <slug>
|
|
10
|
+
agoralia pages create spec.json
|
|
11
|
+
agoralia approvals list
|
|
12
|
+
agoralia approvals decide <id> approved
|
|
13
|
+
agoralia activity --limit 50
|
|
14
|
+
agoralia call GET /campaigns # generic escape hatch to any gated route
|
|
15
|
+
agoralia call POST /campaigns/<id>/start --approval-id <id>
|
|
16
|
+
|
|
17
|
+
Every call is gated server-side and tagged surface=cli in the audit. Dangerous actions
|
|
18
|
+
return a guided 'approval required' the CLI renders with the id + how to retry.
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import pathlib
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
from agoralia_cli.client import ApprovalRequired, CliClient, CliError
|
|
27
|
+
|
|
28
|
+
DEFAULT_URL = "https://api.agoralia.app"
|
|
29
|
+
CONFIG_FILE = pathlib.Path(os.path.expanduser("~/.agoralia/credentials.json"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_config() -> dict:
|
|
33
|
+
try:
|
|
34
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
35
|
+
except Exception:
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _save_config(cfg: dict) -> None:
|
|
40
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
|
|
42
|
+
try:
|
|
43
|
+
CONFIG_FILE.chmod(0o600) # token is a secret
|
|
44
|
+
except OSError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_token(args: argparse.Namespace) -> str:
|
|
49
|
+
return args.token or os.getenv("AGORALIA_TOKEN") or _load_config().get("token", "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_url(args: argparse.Namespace) -> str:
|
|
53
|
+
return args.api_url or os.getenv("AGORALIA_API_URL") or _load_config().get("api_url") or DEFAULT_URL
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _client(args: argparse.Namespace) -> CliClient:
|
|
57
|
+
return CliClient(_resolve_url(args), _resolve_token(args), approval_id=args.approval_id)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _handle_auth(args: argparse.Namespace) -> int:
|
|
61
|
+
if args.action == "login":
|
|
62
|
+
token = args.token or os.getenv("AGORALIA_TOKEN", "")
|
|
63
|
+
if not token:
|
|
64
|
+
print(
|
|
65
|
+
"No token provided. Mint one in the dashboard (Settings → CLI tokens, or "
|
|
66
|
+
"POST /cli/token), then run:\n agoralia auth login --token <TOKEN>",
|
|
67
|
+
file=sys.stderr,
|
|
68
|
+
)
|
|
69
|
+
return 1
|
|
70
|
+
cfg = _load_config()
|
|
71
|
+
cfg["token"] = token
|
|
72
|
+
cfg["api_url"] = args.api_url or os.getenv("AGORALIA_API_URL") or DEFAULT_URL
|
|
73
|
+
_save_config(cfg)
|
|
74
|
+
print(f"Logged in. Token stored in {CONFIG_FILE} (api_url: {cfg['api_url']}).")
|
|
75
|
+
return 0
|
|
76
|
+
if args.action == "logout":
|
|
77
|
+
cfg = _load_config()
|
|
78
|
+
cfg.pop("token", None)
|
|
79
|
+
_save_config(cfg)
|
|
80
|
+
print("Logged out (token removed).")
|
|
81
|
+
return 0
|
|
82
|
+
if args.action == "status":
|
|
83
|
+
cfg = _load_config()
|
|
84
|
+
has = bool(_resolve_token(args))
|
|
85
|
+
print(json.dumps({
|
|
86
|
+
"authenticated": has,
|
|
87
|
+
"api_url": _resolve_url(args),
|
|
88
|
+
"token_source": "flag/env" if (args.token or os.getenv("AGORALIA_TOKEN")) else ("config" if cfg.get("token") else "none"),
|
|
89
|
+
}, indent=2))
|
|
90
|
+
return 0
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _print(data) -> None:
|
|
95
|
+
print(json.dumps(data, indent=2, default=str))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
99
|
+
p = argparse.ArgumentParser(prog="agoralia", description="Agoralia CLI — gated agent surface")
|
|
100
|
+
p.add_argument("--api-url", help="API base URL (or AGORALIA_API_URL)")
|
|
101
|
+
p.add_argument("--token", help="Bearer token (or AGORALIA_TOKEN)")
|
|
102
|
+
p.add_argument("--approval-id", help="Retry a dangerous action with a granted approval id")
|
|
103
|
+
sub = p.add_subparsers(dest="resource", required=True)
|
|
104
|
+
|
|
105
|
+
auth = sub.add_parser("auth", help="Authenticate the CLI").add_subparsers(dest="action", required=True)
|
|
106
|
+
al = auth.add_parser("login", help="Store a CLI token")
|
|
107
|
+
al.add_argument("--token", help="CLI token (or AGORALIA_TOKEN)")
|
|
108
|
+
al.add_argument("--api-url", help="API base URL (default https://api.agoralia.app)")
|
|
109
|
+
auth.add_parser("logout", help="Remove the stored token")
|
|
110
|
+
auth.add_parser("status", help="Show auth status")
|
|
111
|
+
|
|
112
|
+
pages = sub.add_parser("pages", help="Generative dashboard pages").add_subparsers(dest="action", required=True)
|
|
113
|
+
pages.add_parser("list")
|
|
114
|
+
g = pages.add_parser("get"); g.add_argument("slug")
|
|
115
|
+
c = pages.add_parser("create"); c.add_argument("spec_file", help="path to a view-spec JSON file")
|
|
116
|
+
|
|
117
|
+
appr = sub.add_parser("approvals", help="Human-go queue").add_subparsers(dest="action", required=True)
|
|
118
|
+
appr.add_parser("list")
|
|
119
|
+
d = appr.add_parser("decide"); d.add_argument("id"); d.add_argument("decision", choices=["approved", "denied"])
|
|
120
|
+
|
|
121
|
+
act = sub.add_parser("activity", help="Audit of agent actions")
|
|
122
|
+
act.add_argument("--limit", type=int, default=50)
|
|
123
|
+
|
|
124
|
+
conns = sub.add_parser("connections", help="Remote MCP servers the copilot can use").add_subparsers(dest="action", required=True)
|
|
125
|
+
conns.add_parser("list")
|
|
126
|
+
cc = conns.add_parser("connect", help="Connect a remote MCP server")
|
|
127
|
+
cc.add_argument("name")
|
|
128
|
+
cc.add_argument("url")
|
|
129
|
+
cc.add_argument("--auth-token", help="OAuth bearer token (stored encrypted server-side)")
|
|
130
|
+
cc.add_argument("--allowed-tool", action="append", default=[], dest="allowed_tools",
|
|
131
|
+
help="Whitelist a tool (repeatable). Omit to allow all tools.")
|
|
132
|
+
cd = conns.add_parser("disconnect"); cd.add_argument("id")
|
|
133
|
+
|
|
134
|
+
call = sub.add_parser("call", help="Generic gated request to any route")
|
|
135
|
+
call.add_argument("method", choices=["GET", "POST", "PATCH", "DELETE"])
|
|
136
|
+
call.add_argument("path")
|
|
137
|
+
call.add_argument("--data", help="JSON body (string or @file)")
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def dispatch(args: argparse.Namespace, client: CliClient) -> dict:
|
|
142
|
+
if args.resource == "pages":
|
|
143
|
+
if args.action == "list":
|
|
144
|
+
return client.request("GET", "/pages")
|
|
145
|
+
if args.action == "get":
|
|
146
|
+
return client.request("GET", f"/pages/{args.slug}")
|
|
147
|
+
if args.action == "create":
|
|
148
|
+
with open(args.spec_file) as f:
|
|
149
|
+
spec = json.load(f)
|
|
150
|
+
return client.request("POST", "/pages", json={"spec": spec})
|
|
151
|
+
|
|
152
|
+
if args.resource == "approvals":
|
|
153
|
+
if args.action == "list":
|
|
154
|
+
return client.request("GET", "/approvals")
|
|
155
|
+
if args.action == "decide":
|
|
156
|
+
return client.request("POST", f"/approvals/{args.id}/decide", json={"decision": args.decision})
|
|
157
|
+
|
|
158
|
+
if args.resource == "activity":
|
|
159
|
+
return client.request("GET", "/actions", params={"limit": args.limit})
|
|
160
|
+
|
|
161
|
+
if args.resource == "connections":
|
|
162
|
+
if args.action == "list":
|
|
163
|
+
return client.request("GET", "/mcp-connections")
|
|
164
|
+
if args.action == "connect":
|
|
165
|
+
body = {"name": args.name, "url": args.url, "allowed_tools": args.allowed_tools}
|
|
166
|
+
if args.auth_token:
|
|
167
|
+
body["auth_token"] = args.auth_token
|
|
168
|
+
return client.request("POST", "/mcp-connections", json=body)
|
|
169
|
+
if args.action == "disconnect":
|
|
170
|
+
return client.request("DELETE", f"/mcp-connections/{args.id}")
|
|
171
|
+
|
|
172
|
+
if args.resource == "call":
|
|
173
|
+
body = None
|
|
174
|
+
if args.data:
|
|
175
|
+
raw = args.data[1:] and open(args.data[1:]).read() if args.data.startswith("@") else args.data
|
|
176
|
+
body = json.loads(raw)
|
|
177
|
+
return client.request(args.method, args.path, json=body)
|
|
178
|
+
|
|
179
|
+
raise CliError(f"Unknown command: {args.resource}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def main(argv: list[str] | None = None) -> int:
|
|
183
|
+
args = build_parser().parse_args(argv)
|
|
184
|
+
if args.resource == "auth":
|
|
185
|
+
return _handle_auth(args)
|
|
186
|
+
try:
|
|
187
|
+
result = dispatch(args, _client(args))
|
|
188
|
+
_print(result)
|
|
189
|
+
return 0
|
|
190
|
+
except ApprovalRequired as e:
|
|
191
|
+
print("⚠ Approval required before this action can run.", file=sys.stderr)
|
|
192
|
+
if e.approval_id:
|
|
193
|
+
print(f" approval_id: {e.approval_id}", file=sys.stderr)
|
|
194
|
+
for step in e.remediation:
|
|
195
|
+
print(f" → {step}", file=sys.stderr)
|
|
196
|
+
if e.approval_id:
|
|
197
|
+
print(f" Once approved, retry with: --approval-id {e.approval_id}", file=sys.stderr)
|
|
198
|
+
return 2
|
|
199
|
+
except CliError as e:
|
|
200
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
201
|
+
return 1
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
raise SystemExit(main())
|