meridian-governance-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.
- meridian_governance_cli-0.1.0/LICENSE +21 -0
- meridian_governance_cli-0.1.0/PKG-INFO +79 -0
- meridian_governance_cli-0.1.0/README.md +66 -0
- meridian_governance_cli-0.1.0/meridian_cli/__init__.py +3 -0
- meridian_governance_cli-0.1.0/meridian_cli/cli.py +332 -0
- meridian_governance_cli-0.1.0/meridian_cli/config.py +36 -0
- meridian_governance_cli-0.1.0/meridian_governance_cli.egg-info/PKG-INFO +79 -0
- meridian_governance_cli-0.1.0/meridian_governance_cli.egg-info/SOURCES.txt +13 -0
- meridian_governance_cli-0.1.0/meridian_governance_cli.egg-info/dependency_links.txt +1 -0
- meridian_governance_cli-0.1.0/meridian_governance_cli.egg-info/entry_points.txt +2 -0
- meridian_governance_cli-0.1.0/meridian_governance_cli.egg-info/requires.txt +2 -0
- meridian_governance_cli-0.1.0/meridian_governance_cli.egg-info/top_level.txt +1 -0
- meridian_governance_cli-0.1.0/pyproject.toml +18 -0
- meridian_governance_cli-0.1.0/setup.cfg +4 -0
- meridian_governance_cli-0.1.0/tests/test_config.py +58 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 knjpollard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meridian-governance-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for Meridian Governance Router — not the unrelated 'meridian-cli' package also on PyPI
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/knjpollard/meridian
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: meridian-client
|
|
11
|
+
Requires-Dist: requests>=2.28.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# meridian-governance-cli
|
|
15
|
+
|
|
16
|
+
Scriptable command-line client for [Meridian Governance Router](https://dev-meridian.mytrustworthy.ai/), over [`meridian-client`](https://pypi.org/project/meridian-client/). Installs as the `meridian` command.
|
|
17
|
+
|
|
18
|
+
Not to be confused with the unrelated `meridian-cli` package also on PyPI.
|
|
19
|
+
|
|
20
|
+
Not a chat REPL and not `mta-cli` with the serial numbers filed off —
|
|
21
|
+
Meridian doesn't have anything to chat with or a persona to pick. This is
|
|
22
|
+
closer in spirit to the `aws` or `stripe` CLIs: subcommands over an API,
|
|
23
|
+
scriptable, no interactive loop required.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install meridian-governance-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Get credentials
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
meridian signup # new org — prompts for org name, email, password
|
|
35
|
+
# or
|
|
36
|
+
meridian login # existing account — authenticates, mints a new key
|
|
37
|
+
meridian whoami # confirm what's saved
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Credentials live in `~/.meridian/config.json` (mode 600 — `api_key` is in
|
|
41
|
+
there in plaintext, same tradeoff `mta-cli` makes for its own token). Override
|
|
42
|
+
the location with `MERIDIAN_CLI_HOME` if you want more than one profile.
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
meridian resolve my_agent [--project-id ID] [--json]
|
|
48
|
+
meridian explain ROUTING_LOG_ID
|
|
49
|
+
meridian audit ROUTING_LOG_ID --input-tokens N --output-tokens N
|
|
50
|
+
|
|
51
|
+
meridian projects create --name NAME [--quality Q] [--cost C] [--risk R]
|
|
52
|
+
meridian projects list
|
|
53
|
+
|
|
54
|
+
meridian agents create PROJECT_ID AGENT_NAME [--pinned-model M] [--quality-override Q] [--allowed-hubs h1,h2]
|
|
55
|
+
meridian agents list PROJECT_ID
|
|
56
|
+
|
|
57
|
+
meridian keys list [--json]
|
|
58
|
+
meridian keys create [--name NAME]
|
|
59
|
+
meridian keys revoke KEY_ID
|
|
60
|
+
|
|
61
|
+
meridian catalog
|
|
62
|
+
meridian logout
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every data-returning command without a dedicated human-readable format
|
|
66
|
+
(`explain`, `audit`, `catalog`, `projects list`, `agents list`) prints raw
|
|
67
|
+
JSON — pipe into `jq`.
|
|
68
|
+
|
|
69
|
+
## What this deliberately doesn't do
|
|
70
|
+
|
|
71
|
+
- No chat, no persona selection, no device integrations — those are `mta-cli`
|
|
72
|
+
concerns (consumer AI Companion), not Meridian's. Meridian answers "which
|
|
73
|
+
model should this agent use"; it has nothing to converse with.
|
|
74
|
+
- No hub/model/deployment registration commands — those routes aren't
|
|
75
|
+
tenant-scoped today (console/house-key only), so a CLI command for them
|
|
76
|
+
would just 403 for every real user. Added if/when that changes.
|
|
77
|
+
- No interactive REPL. If you want one, it'd be a thin layer calling the same
|
|
78
|
+
functions `cli.py` already has — the commands are the primitive, not the
|
|
79
|
+
shell.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# meridian-governance-cli
|
|
2
|
+
|
|
3
|
+
Scriptable command-line client for [Meridian Governance Router](https://dev-meridian.mytrustworthy.ai/), over [`meridian-client`](https://pypi.org/project/meridian-client/). Installs as the `meridian` command.
|
|
4
|
+
|
|
5
|
+
Not to be confused with the unrelated `meridian-cli` package also on PyPI.
|
|
6
|
+
|
|
7
|
+
Not a chat REPL and not `mta-cli` with the serial numbers filed off —
|
|
8
|
+
Meridian doesn't have anything to chat with or a persona to pick. This is
|
|
9
|
+
closer in spirit to the `aws` or `stripe` CLIs: subcommands over an API,
|
|
10
|
+
scriptable, no interactive loop required.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install meridian-governance-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Get credentials
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
meridian signup # new org — prompts for org name, email, password
|
|
22
|
+
# or
|
|
23
|
+
meridian login # existing account — authenticates, mints a new key
|
|
24
|
+
meridian whoami # confirm what's saved
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Credentials live in `~/.meridian/config.json` (mode 600 — `api_key` is in
|
|
28
|
+
there in plaintext, same tradeoff `mta-cli` makes for its own token). Override
|
|
29
|
+
the location with `MERIDIAN_CLI_HOME` if you want more than one profile.
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
meridian resolve my_agent [--project-id ID] [--json]
|
|
35
|
+
meridian explain ROUTING_LOG_ID
|
|
36
|
+
meridian audit ROUTING_LOG_ID --input-tokens N --output-tokens N
|
|
37
|
+
|
|
38
|
+
meridian projects create --name NAME [--quality Q] [--cost C] [--risk R]
|
|
39
|
+
meridian projects list
|
|
40
|
+
|
|
41
|
+
meridian agents create PROJECT_ID AGENT_NAME [--pinned-model M] [--quality-override Q] [--allowed-hubs h1,h2]
|
|
42
|
+
meridian agents list PROJECT_ID
|
|
43
|
+
|
|
44
|
+
meridian keys list [--json]
|
|
45
|
+
meridian keys create [--name NAME]
|
|
46
|
+
meridian keys revoke KEY_ID
|
|
47
|
+
|
|
48
|
+
meridian catalog
|
|
49
|
+
meridian logout
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every data-returning command without a dedicated human-readable format
|
|
53
|
+
(`explain`, `audit`, `catalog`, `projects list`, `agents list`) prints raw
|
|
54
|
+
JSON — pipe into `jq`.
|
|
55
|
+
|
|
56
|
+
## What this deliberately doesn't do
|
|
57
|
+
|
|
58
|
+
- No chat, no persona selection, no device integrations — those are `mta-cli`
|
|
59
|
+
concerns (consumer AI Companion), not Meridian's. Meridian answers "which
|
|
60
|
+
model should this agent use"; it has nothing to converse with.
|
|
61
|
+
- No hub/model/deployment registration commands — those routes aren't
|
|
62
|
+
tenant-scoped today (console/house-key only), so a CLI command for them
|
|
63
|
+
would just 403 for every real user. Added if/when that changes.
|
|
64
|
+
- No interactive REPL. If you want one, it'd be a thin layer calling the same
|
|
65
|
+
functions `cli.py` already has — the commands are the primitive, not the
|
|
66
|
+
shell.
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""meridian-cli — a scriptable command-line client over meridian-client.
|
|
2
|
+
|
|
3
|
+
meridian signup
|
|
4
|
+
meridian login
|
|
5
|
+
meridian resolve my_agent
|
|
6
|
+
meridian explain 20076
|
|
7
|
+
meridian projects create --name support
|
|
8
|
+
meridian agents create 5 my_agent --pinned-model claude-opus-5
|
|
9
|
+
meridian keys list / create / revoke
|
|
10
|
+
meridian catalog
|
|
11
|
+
|
|
12
|
+
Not a chat REPL and not mta-cli with the serial numbers filed off — Meridian
|
|
13
|
+
doesn't have anything to chat with or a persona to pick. This is a thin,
|
|
14
|
+
scriptable layer over the SDK, closer in spirit to `aws` or `stripe` than to
|
|
15
|
+
mta-cli's interactive_loop.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import getpass
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from meridian import Meridian
|
|
24
|
+
from meridian.exceptions import MeridianError
|
|
25
|
+
|
|
26
|
+
from . import config as cfgmod
|
|
27
|
+
|
|
28
|
+
ERR = sys.stderr
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _client(require_auth: bool = True) -> Meridian:
|
|
32
|
+
cfg = cfgmod.load_config()
|
|
33
|
+
if require_auth and not cfg.get("api_key"):
|
|
34
|
+
print("No credentials saved. Run `meridian signup` or `meridian login` first.", file=ERR)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
return Meridian(cfg["base_url"], api_key=cfg.get("api_key"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _prompt(label: str, current: str = None) -> str:
|
|
40
|
+
if current:
|
|
41
|
+
return current
|
|
42
|
+
return input(f"{label}: ").strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _prompt_password(current: str = None) -> str:
|
|
46
|
+
if current:
|
|
47
|
+
return current
|
|
48
|
+
return getpass.getpass("Password: ")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _print(obj, as_json: bool) -> None:
|
|
52
|
+
if as_json:
|
|
53
|
+
print(json.dumps(obj, indent=2, default=str))
|
|
54
|
+
else:
|
|
55
|
+
print(obj)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run_guarded(fn):
|
|
59
|
+
try:
|
|
60
|
+
fn()
|
|
61
|
+
except MeridianError as e:
|
|
62
|
+
print(f"Error: {e}", file=ERR)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── signup / login / whoami / logout ──
|
|
67
|
+
|
|
68
|
+
def cmd_signup(args):
|
|
69
|
+
cfg = cfgmod.load_config()
|
|
70
|
+
base_url = args.base_url or cfg["base_url"]
|
|
71
|
+
org_name = _prompt("Organization name", args.org_name)
|
|
72
|
+
email = _prompt("Email", args.email)
|
|
73
|
+
password = _prompt_password(args.password)
|
|
74
|
+
m = Meridian.signup(base_url, org_name=org_name, email=email, password=password, name=args.name)
|
|
75
|
+
cfg.update({"base_url": base_url, "api_key": m.api_key, "org_id": m.org_id})
|
|
76
|
+
cfgmod.save_config(cfg)
|
|
77
|
+
print(f"Signed up. org_id={m.org_id}, tier={m.signup_response['subscription_tier']}")
|
|
78
|
+
print("Credentials saved to ~/.meridian/config.json")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_login(args):
|
|
82
|
+
cfg = cfgmod.load_config()
|
|
83
|
+
base_url = args.base_url or cfg["base_url"]
|
|
84
|
+
email = _prompt("Email", args.email)
|
|
85
|
+
password = _prompt_password(args.password)
|
|
86
|
+
import socket
|
|
87
|
+
key_name = args.key_name or f"meridian-cli on {socket.gethostname()}"
|
|
88
|
+
m = Meridian.login(base_url, email=email, password=password, key_name=key_name)
|
|
89
|
+
cfg.update({"base_url": base_url, "api_key": m.api_key, "org_id": m.org_id})
|
|
90
|
+
cfgmod.save_config(cfg)
|
|
91
|
+
print(f"Logged in. org_id={m.org_id}. Minted a new key ({key_name}) and saved it.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_whoami(args):
|
|
95
|
+
cfg = cfgmod.load_config()
|
|
96
|
+
key = cfg.get("api_key")
|
|
97
|
+
masked = (key[:14] + "...") if key else "(none)"
|
|
98
|
+
print(f"base_url: {cfg['base_url']}")
|
|
99
|
+
print(f"org_id: {cfg.get('org_id')}")
|
|
100
|
+
print(f"api_key: {masked}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_logout(args):
|
|
104
|
+
cfg = cfgmod.load_config()
|
|
105
|
+
cfg["api_key"] = None
|
|
106
|
+
cfg["org_id"] = None
|
|
107
|
+
cfgmod.save_config(cfg)
|
|
108
|
+
print("Cleared local credentials. The key itself is still valid on the server —")
|
|
109
|
+
print("run `meridian keys list` / `meridian keys revoke <id>` first if you want it dead too.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── resolve / explain / audit / catalog ──
|
|
113
|
+
|
|
114
|
+
def cmd_resolve(args):
|
|
115
|
+
def go():
|
|
116
|
+
m = _client()
|
|
117
|
+
d = m.resolve(args.agent_name, project_id=args.project_id)
|
|
118
|
+
if args.json:
|
|
119
|
+
print(json.dumps(d.__dict__, indent=2, default=str))
|
|
120
|
+
else:
|
|
121
|
+
print(f"model: {d.model}")
|
|
122
|
+
print(f"hub: {d.hub}")
|
|
123
|
+
print(f"source: {d.source}")
|
|
124
|
+
print(f"routing_log_id: {d.routing_log_id}")
|
|
125
|
+
if d.sliders:
|
|
126
|
+
print(f"sliders: {d.sliders}")
|
|
127
|
+
_run_guarded(go)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_explain(args):
|
|
131
|
+
def go():
|
|
132
|
+
m = _client()
|
|
133
|
+
_print(m.explain(args.routing_log_id), True)
|
|
134
|
+
_run_guarded(go)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_audit(args):
|
|
138
|
+
def go():
|
|
139
|
+
m = _client()
|
|
140
|
+
r = m.audit(args.routing_log_id, input_tokens=args.input_tokens,
|
|
141
|
+
output_tokens=args.output_tokens, latency_ms=args.latency_ms)
|
|
142
|
+
_print(r, True)
|
|
143
|
+
_run_guarded(go)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cmd_catalog(args):
|
|
147
|
+
def go():
|
|
148
|
+
m = _client()
|
|
149
|
+
_print(m.catalog(), True)
|
|
150
|
+
_run_guarded(go)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── projects ──
|
|
154
|
+
|
|
155
|
+
def cmd_projects_create(args):
|
|
156
|
+
def go():
|
|
157
|
+
m = _client()
|
|
158
|
+
cfg = cfgmod.load_config()
|
|
159
|
+
p = m.create_project(org_id=args.org_id or cfg.get("org_id"), name=args.name,
|
|
160
|
+
quality=args.quality, cost=args.cost, risk=args.risk,
|
|
161
|
+
classification=args.classification)
|
|
162
|
+
print(f"Created project {p.id} ({p.name})")
|
|
163
|
+
_run_guarded(go)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cmd_projects_list(args):
|
|
167
|
+
def go():
|
|
168
|
+
m = _client()
|
|
169
|
+
_print(m.projects(), True)
|
|
170
|
+
_run_guarded(go)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── agents ──
|
|
174
|
+
|
|
175
|
+
def cmd_agents_create(args):
|
|
176
|
+
def go():
|
|
177
|
+
m = _client()
|
|
178
|
+
kwargs = {}
|
|
179
|
+
if args.allowed_hubs: kwargs["allowed_hubs"] = args.allowed_hubs.split(",")
|
|
180
|
+
if args.denied_hubs: kwargs["denied_hubs"] = args.denied_hubs.split(",")
|
|
181
|
+
a = m.create_agent(args.project_id, args.agent_name,
|
|
182
|
+
quality_override=args.quality_override,
|
|
183
|
+
cost_override=args.cost_override,
|
|
184
|
+
risk_override=args.risk_override,
|
|
185
|
+
pinned_model=args.pinned_model, **kwargs)
|
|
186
|
+
print(f"Created agent '{a.agent_name}' in project {args.project_id}")
|
|
187
|
+
_run_guarded(go)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_agents_list(args):
|
|
191
|
+
def go():
|
|
192
|
+
m = _client()
|
|
193
|
+
_print(m.agents(args.project_id), True)
|
|
194
|
+
_run_guarded(go)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── keys ──
|
|
198
|
+
|
|
199
|
+
def cmd_keys_list(args):
|
|
200
|
+
def go():
|
|
201
|
+
m = _client()
|
|
202
|
+
keys = m.list_keys()
|
|
203
|
+
if args.json:
|
|
204
|
+
print(json.dumps([k.__dict__ for k in keys], indent=2, default=str))
|
|
205
|
+
return
|
|
206
|
+
if not keys:
|
|
207
|
+
print("No keys.")
|
|
208
|
+
return
|
|
209
|
+
for k in keys:
|
|
210
|
+
status = "active" if k.enabled else "revoked"
|
|
211
|
+
print(f" {k.id:>4} {k.key_prefix}... {k.name or '(unnamed)':<30} {status} last used: {k.last_used_at or 'never'}")
|
|
212
|
+
_run_guarded(go)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cmd_keys_create(args):
|
|
216
|
+
def go():
|
|
217
|
+
m = _client()
|
|
218
|
+
cfg = cfgmod.load_config()
|
|
219
|
+
data = m.create_key(org_id=args.org_id or cfg.get("org_id"), name=args.name)
|
|
220
|
+
print(data["api_key"])
|
|
221
|
+
print("Store this now — it will not be shown again.", file=ERR)
|
|
222
|
+
_run_guarded(go)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def cmd_keys_revoke(args):
|
|
226
|
+
def go():
|
|
227
|
+
m = _client()
|
|
228
|
+
m.revoke_key(args.key_id)
|
|
229
|
+
print(f"Revoked key {args.key_id}")
|
|
230
|
+
_run_guarded(go)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── argparse wiring ──
|
|
234
|
+
|
|
235
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
236
|
+
p = argparse.ArgumentParser(prog="meridian", description="Command-line client for Meridian Governance Router")
|
|
237
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
238
|
+
|
|
239
|
+
s = sub.add_parser("signup", help="Create a new org, user, and first key")
|
|
240
|
+
s.add_argument("--base-url")
|
|
241
|
+
s.add_argument("--org-name")
|
|
242
|
+
s.add_argument("--email")
|
|
243
|
+
s.add_argument("--password")
|
|
244
|
+
s.add_argument("--name", help="Your name (optional)")
|
|
245
|
+
s.set_defaults(func=cmd_signup)
|
|
246
|
+
|
|
247
|
+
s = sub.add_parser("login", help="Authenticate and mint a new key")
|
|
248
|
+
s.add_argument("--base-url")
|
|
249
|
+
s.add_argument("--email")
|
|
250
|
+
s.add_argument("--password")
|
|
251
|
+
s.add_argument("--key-name")
|
|
252
|
+
s.set_defaults(func=cmd_login)
|
|
253
|
+
|
|
254
|
+
s = sub.add_parser("whoami", help="Show the currently saved credentials")
|
|
255
|
+
s.set_defaults(func=cmd_whoami)
|
|
256
|
+
|
|
257
|
+
s = sub.add_parser("logout", help="Clear locally saved credentials")
|
|
258
|
+
s.set_defaults(func=cmd_logout)
|
|
259
|
+
|
|
260
|
+
s = sub.add_parser("resolve", help="Ask which model/hub an agent should use")
|
|
261
|
+
s.add_argument("agent_name")
|
|
262
|
+
s.add_argument("--project-id", type=int)
|
|
263
|
+
s.add_argument("--json", action="store_true")
|
|
264
|
+
s.set_defaults(func=cmd_resolve)
|
|
265
|
+
|
|
266
|
+
s = sub.add_parser("explain", help="Full record of a routing decision")
|
|
267
|
+
s.add_argument("routing_log_id", type=int)
|
|
268
|
+
s.set_defaults(func=cmd_explain)
|
|
269
|
+
|
|
270
|
+
s = sub.add_parser("audit", help="Report token usage for a routing decision")
|
|
271
|
+
s.add_argument("routing_log_id", type=int)
|
|
272
|
+
s.add_argument("--input-tokens", type=int, default=0)
|
|
273
|
+
s.add_argument("--output-tokens", type=int, default=0)
|
|
274
|
+
s.add_argument("--latency-ms", type=int)
|
|
275
|
+
s.set_defaults(func=cmd_audit)
|
|
276
|
+
|
|
277
|
+
s = sub.add_parser("catalog", help="List the model/deployment catalog")
|
|
278
|
+
s.set_defaults(func=cmd_catalog)
|
|
279
|
+
|
|
280
|
+
projects = sub.add_parser("projects", help="Manage projects")
|
|
281
|
+
psub = projects.add_subparsers(dest="projects_command", required=True)
|
|
282
|
+
s = psub.add_parser("create")
|
|
283
|
+
s.add_argument("--name", required=True)
|
|
284
|
+
s.add_argument("--org-id", type=int)
|
|
285
|
+
s.add_argument("--quality", type=int)
|
|
286
|
+
s.add_argument("--cost", type=int)
|
|
287
|
+
s.add_argument("--risk", type=int)
|
|
288
|
+
s.add_argument("--classification", default="general")
|
|
289
|
+
s.set_defaults(func=cmd_projects_create)
|
|
290
|
+
s = psub.add_parser("list")
|
|
291
|
+
s.set_defaults(func=cmd_projects_list)
|
|
292
|
+
|
|
293
|
+
agents = sub.add_parser("agents", help="Manage agents within a project")
|
|
294
|
+
asub = agents.add_subparsers(dest="agents_command", required=True)
|
|
295
|
+
s = asub.add_parser("create")
|
|
296
|
+
s.add_argument("project_id", type=int)
|
|
297
|
+
s.add_argument("agent_name")
|
|
298
|
+
s.add_argument("--pinned-model")
|
|
299
|
+
s.add_argument("--quality-override", type=int)
|
|
300
|
+
s.add_argument("--cost-override", type=int)
|
|
301
|
+
s.add_argument("--risk-override", type=int)
|
|
302
|
+
s.add_argument("--allowed-hubs", help="Comma-separated")
|
|
303
|
+
s.add_argument("--denied-hubs", help="Comma-separated")
|
|
304
|
+
s.set_defaults(func=cmd_agents_create)
|
|
305
|
+
s = asub.add_parser("list")
|
|
306
|
+
s.add_argument("project_id", type=int)
|
|
307
|
+
s.set_defaults(func=cmd_agents_list)
|
|
308
|
+
|
|
309
|
+
keys = sub.add_parser("keys", help="Manage your own Meridian API keys")
|
|
310
|
+
ksub = keys.add_subparsers(dest="keys_command", required=True)
|
|
311
|
+
s = ksub.add_parser("list")
|
|
312
|
+
s.add_argument("--json", action="store_true")
|
|
313
|
+
s.set_defaults(func=cmd_keys_list)
|
|
314
|
+
s = ksub.add_parser("create")
|
|
315
|
+
s.add_argument("--name")
|
|
316
|
+
s.add_argument("--org-id", type=int)
|
|
317
|
+
s.set_defaults(func=cmd_keys_create)
|
|
318
|
+
s = ksub.add_parser("revoke")
|
|
319
|
+
s.add_argument("key_id", type=int)
|
|
320
|
+
s.set_defaults(func=cmd_keys_revoke)
|
|
321
|
+
|
|
322
|
+
return p
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def main():
|
|
326
|
+
parser = build_parser()
|
|
327
|
+
args = parser.parse_args()
|
|
328
|
+
args.func(args)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Config persistence — ~/.meridian/config.json. Same shape as mta-cli's
|
|
2
|
+
load_config/save_config: defaults merged under whatever's saved, directory
|
|
3
|
+
created on first write, file permissions locked down because api_key lives
|
|
4
|
+
here in plaintext (the same tradeoff mta-cli makes for its own token)."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
CONFIG_DIR = Path(os.environ.get("MERIDIAN_CLI_HOME", Path.home() / ".meridian"))
|
|
12
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONFIG = {
|
|
15
|
+
"base_url": "https://dev-meridian.mytrustworthy.ai",
|
|
16
|
+
"api_key": None,
|
|
17
|
+
"org_id": None,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_config() -> dict:
|
|
22
|
+
if CONFIG_FILE.exists():
|
|
23
|
+
with open(CONFIG_FILE, encoding="utf-8") as f:
|
|
24
|
+
saved = json.load(f)
|
|
25
|
+
return {**DEFAULT_CONFIG, **saved}
|
|
26
|
+
return dict(DEFAULT_CONFIG)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def save_config(config: dict) -> None:
|
|
30
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
32
|
+
json.dump(config, f, indent=2)
|
|
33
|
+
try:
|
|
34
|
+
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600 — api_key lives here
|
|
35
|
+
except OSError:
|
|
36
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meridian-governance-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for Meridian Governance Router — not the unrelated 'meridian-cli' package also on PyPI
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/knjpollard/meridian
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: meridian-client
|
|
11
|
+
Requires-Dist: requests>=2.28.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# meridian-governance-cli
|
|
15
|
+
|
|
16
|
+
Scriptable command-line client for [Meridian Governance Router](https://dev-meridian.mytrustworthy.ai/), over [`meridian-client`](https://pypi.org/project/meridian-client/). Installs as the `meridian` command.
|
|
17
|
+
|
|
18
|
+
Not to be confused with the unrelated `meridian-cli` package also on PyPI.
|
|
19
|
+
|
|
20
|
+
Not a chat REPL and not `mta-cli` with the serial numbers filed off —
|
|
21
|
+
Meridian doesn't have anything to chat with or a persona to pick. This is
|
|
22
|
+
closer in spirit to the `aws` or `stripe` CLIs: subcommands over an API,
|
|
23
|
+
scriptable, no interactive loop required.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install meridian-governance-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Get credentials
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
meridian signup # new org — prompts for org name, email, password
|
|
35
|
+
# or
|
|
36
|
+
meridian login # existing account — authenticates, mints a new key
|
|
37
|
+
meridian whoami # confirm what's saved
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Credentials live in `~/.meridian/config.json` (mode 600 — `api_key` is in
|
|
41
|
+
there in plaintext, same tradeoff `mta-cli` makes for its own token). Override
|
|
42
|
+
the location with `MERIDIAN_CLI_HOME` if you want more than one profile.
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
meridian resolve my_agent [--project-id ID] [--json]
|
|
48
|
+
meridian explain ROUTING_LOG_ID
|
|
49
|
+
meridian audit ROUTING_LOG_ID --input-tokens N --output-tokens N
|
|
50
|
+
|
|
51
|
+
meridian projects create --name NAME [--quality Q] [--cost C] [--risk R]
|
|
52
|
+
meridian projects list
|
|
53
|
+
|
|
54
|
+
meridian agents create PROJECT_ID AGENT_NAME [--pinned-model M] [--quality-override Q] [--allowed-hubs h1,h2]
|
|
55
|
+
meridian agents list PROJECT_ID
|
|
56
|
+
|
|
57
|
+
meridian keys list [--json]
|
|
58
|
+
meridian keys create [--name NAME]
|
|
59
|
+
meridian keys revoke KEY_ID
|
|
60
|
+
|
|
61
|
+
meridian catalog
|
|
62
|
+
meridian logout
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every data-returning command without a dedicated human-readable format
|
|
66
|
+
(`explain`, `audit`, `catalog`, `projects list`, `agents list`) prints raw
|
|
67
|
+
JSON — pipe into `jq`.
|
|
68
|
+
|
|
69
|
+
## What this deliberately doesn't do
|
|
70
|
+
|
|
71
|
+
- No chat, no persona selection, no device integrations — those are `mta-cli`
|
|
72
|
+
concerns (consumer AI Companion), not Meridian's. Meridian answers "which
|
|
73
|
+
model should this agent use"; it has nothing to converse with.
|
|
74
|
+
- No hub/model/deployment registration commands — those routes aren't
|
|
75
|
+
tenant-scoped today (console/house-key only), so a CLI command for them
|
|
76
|
+
would just 403 for every real user. Added if/when that changes.
|
|
77
|
+
- No interactive REPL. If you want one, it'd be a thin layer calling the same
|
|
78
|
+
functions `cli.py` already has — the commands are the primitive, not the
|
|
79
|
+
shell.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
meridian_cli/__init__.py
|
|
5
|
+
meridian_cli/cli.py
|
|
6
|
+
meridian_cli/config.py
|
|
7
|
+
meridian_governance_cli.egg-info/PKG-INFO
|
|
8
|
+
meridian_governance_cli.egg-info/SOURCES.txt
|
|
9
|
+
meridian_governance_cli.egg-info/dependency_links.txt
|
|
10
|
+
meridian_governance_cli.egg-info/entry_points.txt
|
|
11
|
+
meridian_governance_cli.egg-info/requires.txt
|
|
12
|
+
meridian_governance_cli.egg-info/top_level.txt
|
|
13
|
+
tests/test_config.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
meridian_cli
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "meridian-governance-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Command-line client for Meridian Governance Router — not the unrelated 'meridian-cli' package also on PyPI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = ["meridian-client", "requests>=2.28.0"]
|
|
9
|
+
|
|
10
|
+
[project.urls]
|
|
11
|
+
Homepage = "https://github.com/knjpollard/meridian"
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
meridian = "meridian_cli.cli:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools>=68.0"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Config round-trip — no network, no server. Uses MERIDIAN_CLI_HOME to
|
|
2
|
+
point at a throwaway directory instead of the real ~/.meridian."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
10
|
+
|
|
11
|
+
FAILS, PASSES = [], []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check(name, cond, detail=""):
|
|
15
|
+
(PASSES if cond else FAILS).append(name)
|
|
16
|
+
print(f"{'PASS' if cond else 'FAIL'} {name}" + (f" — {detail}" if detail else ""))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
tmp = tempfile.mkdtemp()
|
|
20
|
+
os.environ["MERIDIAN_CLI_HOME"] = tmp
|
|
21
|
+
try:
|
|
22
|
+
import importlib
|
|
23
|
+
from meridian_cli import config as cfgmod
|
|
24
|
+
importlib.reload(cfgmod) # pick up MERIDIAN_CLI_HOME set above
|
|
25
|
+
|
|
26
|
+
c = cfgmod.load_config()
|
|
27
|
+
check("defaults load with no file present", c["api_key"] is None and "base_url" in c)
|
|
28
|
+
|
|
29
|
+
c["api_key"] = "mdn_live_test"
|
|
30
|
+
c["org_id"] = 42
|
|
31
|
+
cfgmod.save_config(c)
|
|
32
|
+
check("config file was actually created", os.path.exists(cfgmod.CONFIG_FILE))
|
|
33
|
+
|
|
34
|
+
mode = oct(os.stat(cfgmod.CONFIG_FILE).st_mode)[-3:]
|
|
35
|
+
check("config file is not world/group readable", mode == "600", mode)
|
|
36
|
+
|
|
37
|
+
c2 = cfgmod.load_config()
|
|
38
|
+
check("saved values round-trip", c2["api_key"] == "mdn_live_test" and c2["org_id"] == 42)
|
|
39
|
+
|
|
40
|
+
c2["api_key"] = None
|
|
41
|
+
cfgmod.save_config(c2)
|
|
42
|
+
c3 = cfgmod.load_config()
|
|
43
|
+
check("clearing a field persists", c3["api_key"] is None and c3["org_id"] == 42)
|
|
44
|
+
|
|
45
|
+
from meridian_cli.cli import build_parser
|
|
46
|
+
p = build_parser()
|
|
47
|
+
args = p.parse_args(["projects", "create", "--name", "x"])
|
|
48
|
+
check("subcommand parsing wires the right handler", args.func.__name__ == "cmd_projects_create")
|
|
49
|
+
args = p.parse_args(["keys", "revoke", "7"])
|
|
50
|
+
check("nested subcommand parsing works", args.key_id == 7 and args.func.__name__ == "cmd_keys_revoke")
|
|
51
|
+
|
|
52
|
+
finally:
|
|
53
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
54
|
+
|
|
55
|
+
print(f"\n{len(PASSES)} passed, {len(FAILS)} failed")
|
|
56
|
+
if FAILS:
|
|
57
|
+
print("FAILED: " + ", ".join(FAILS))
|
|
58
|
+
sys.exit(1 if FAILS else 0)
|