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.
@@ -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,3 @@
1
+ """Meridian CLI — scriptable command-line client over meridian-client."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,2 @@
1
+ [console_scripts]
2
+ meridian = meridian_cli.cli:main
@@ -0,0 +1,2 @@
1
+ meridian-client
2
+ requests>=2.28.0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)