mtrx-cli 0.1.0 → 0.1.2
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.
- package/package.json +1 -1
- package/src/matrx/cli/launcher.py +152 -51
- package/src/matrx/cli/main.py +103 -0
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import base64
|
|
4
4
|
import hashlib
|
|
5
5
|
import datetime as dt
|
|
6
|
+
import httpx
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
8
9
|
import platform
|
|
@@ -123,7 +124,7 @@ def build_launch_plan(
|
|
|
123
124
|
raise ValueError(f"{tool} executable not found in PATH")
|
|
124
125
|
|
|
125
126
|
route = resolve_route(state, tool, route_override)
|
|
126
|
-
env = dict(base_env
|
|
127
|
+
env = dict(os.environ if base_env is None else base_env)
|
|
127
128
|
auth_source = ""
|
|
128
129
|
launch_args = list(passthrough_args or [])
|
|
129
130
|
|
|
@@ -164,11 +165,17 @@ def prepare_routed_setup(
|
|
|
164
165
|
"""
|
|
165
166
|
route = resolve_route(state, tool, route_override)
|
|
166
167
|
changed = False
|
|
167
|
-
env = dict(base_env
|
|
168
|
+
env = dict(os.environ if base_env is None else base_env)
|
|
168
169
|
if route == "matrx" and _ensure_matrx_auth(state, env=env):
|
|
169
170
|
changed = True
|
|
170
171
|
if route == "matrx" and _persist_workspace_binding_from_env(state, env):
|
|
171
172
|
changed = True
|
|
173
|
+
if route == "matrx" and tool == "claude":
|
|
174
|
+
mx_key, _ = _resolve_matrx_route_key(state, env)
|
|
175
|
+
if _approve_claude_custom_api_key(mx_key):
|
|
176
|
+
changed = True
|
|
177
|
+
if _sync_claude_subscription_to_matrx(state, env):
|
|
178
|
+
changed = True
|
|
172
179
|
|
|
173
180
|
if _sync_tool_route_config(state, tool=tool, route=route):
|
|
174
181
|
changed = True
|
|
@@ -200,6 +207,10 @@ def claude_settings_path() -> Path:
|
|
|
200
207
|
return Path.home() / ".claude" / "settings.json"
|
|
201
208
|
|
|
202
209
|
|
|
210
|
+
def claude_state_path() -> Path:
|
|
211
|
+
return Path.home() / ".claude.json"
|
|
212
|
+
|
|
213
|
+
|
|
203
214
|
def codex_config_path() -> Path:
|
|
204
215
|
return Path.home() / ".codex" / "config.toml"
|
|
205
216
|
|
|
@@ -247,6 +258,102 @@ def read_codex_access_token() -> str | None:
|
|
|
247
258
|
return token.strip()
|
|
248
259
|
|
|
249
260
|
|
|
261
|
+
def _read_claude_app_state() -> dict:
|
|
262
|
+
path = claude_state_path()
|
|
263
|
+
if not path.exists():
|
|
264
|
+
return {}
|
|
265
|
+
try:
|
|
266
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
267
|
+
except (json.JSONDecodeError, OSError):
|
|
268
|
+
return {}
|
|
269
|
+
return data if isinstance(data, dict) else {}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _write_claude_app_state(data: dict) -> None:
|
|
273
|
+
path = claude_state_path()
|
|
274
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _approve_claude_custom_api_key(raw_key: str) -> bool:
|
|
279
|
+
if not raw_key:
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
state = _read_claude_app_state()
|
|
283
|
+
custom = state.setdefault("customApiKeyResponses", {})
|
|
284
|
+
approved = custom.get("approved")
|
|
285
|
+
rejected = custom.get("rejected")
|
|
286
|
+
if not isinstance(approved, list):
|
|
287
|
+
approved = []
|
|
288
|
+
if not isinstance(rejected, list):
|
|
289
|
+
rejected = []
|
|
290
|
+
|
|
291
|
+
changed = False
|
|
292
|
+
if raw_key not in approved:
|
|
293
|
+
approved.append(raw_key)
|
|
294
|
+
changed = True
|
|
295
|
+
if raw_key in rejected:
|
|
296
|
+
rejected = [value for value in rejected if value != raw_key]
|
|
297
|
+
changed = True
|
|
298
|
+
|
|
299
|
+
if not changed:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
custom["approved"] = approved
|
|
303
|
+
custom["rejected"] = rejected
|
|
304
|
+
_write_claude_app_state(state)
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _resolve_claude_subscription_token(state: dict) -> str:
|
|
309
|
+
token = (read_claude_oauth_token() or "").strip()
|
|
310
|
+
if token:
|
|
311
|
+
return token
|
|
312
|
+
return (state.get("auth", {}).get("claude_code", {}).get("oauth_token") or "").strip()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _sync_claude_subscription_to_matrx(state: dict, env: dict[str, str]) -> bool:
|
|
316
|
+
token = _resolve_claude_subscription_token(state)
|
|
317
|
+
if not token:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
mx_key, _ = _resolve_matrx_route_key(state, env)
|
|
321
|
+
if not mx_key:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
auth_cfg = state.setdefault("auth", {}).setdefault("claude_code", {})
|
|
325
|
+
token_fp = _fingerprint_secret(token)
|
|
326
|
+
key_fp = _fingerprint_secret(mx_key)
|
|
327
|
+
base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
328
|
+
|
|
329
|
+
if (
|
|
330
|
+
auth_cfg.get("server_synced_token_fingerprint") == token_fp
|
|
331
|
+
and auth_cfg.get("server_synced_matrx_key_fingerprint") == key_fp
|
|
332
|
+
and auth_cfg.get("server_synced_base_url") == base_url
|
|
333
|
+
):
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
with httpx.Client(timeout=15) as client:
|
|
338
|
+
response = client.post(
|
|
339
|
+
f"{base_url.rstrip('/')}/v1/subscriptions/claude-code",
|
|
340
|
+
headers={
|
|
341
|
+
"X-Matrx-Key": mx_key,
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
},
|
|
344
|
+
json={"token": token},
|
|
345
|
+
)
|
|
346
|
+
response.raise_for_status()
|
|
347
|
+
except httpx.HTTPError:
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
auth_cfg["server_synced_token_fingerprint"] = token_fp
|
|
351
|
+
auth_cfg["server_synced_matrx_key_fingerprint"] = key_fp
|
|
352
|
+
auth_cfg["server_synced_base_url"] = base_url
|
|
353
|
+
auth_cfg["server_synced_at"] = dt.datetime.now(dt.timezone.utc).isoformat()
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
|
|
250
357
|
def _resolve_matrx_route_key(
|
|
251
358
|
state: dict,
|
|
252
359
|
env: dict[str, str],
|
|
@@ -372,8 +479,8 @@ def _build_claude_env(
|
|
|
372
479
|
) -> tuple[dict[str, str], str]:
|
|
373
480
|
matrx = state["auth"]["matrx"]
|
|
374
481
|
anthropic = state["auth"]["anthropic"]
|
|
375
|
-
|
|
376
|
-
proxy_base =
|
|
482
|
+
proxy_root = ensure_root_url(matrx.get("base_url"))
|
|
483
|
+
proxy_base = ensure_v1_url(matrx.get("base_url"))
|
|
377
484
|
mx_key, matrx_auth_source = _resolve_matrx_route_key(state, env)
|
|
378
485
|
direct_key = (anthropic.get("key") or "").strip()
|
|
379
486
|
|
|
@@ -381,10 +488,11 @@ def _build_claude_env(
|
|
|
381
488
|
if not mx_key:
|
|
382
489
|
raise ValueError("No Matrx key available. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
|
|
383
490
|
env.pop("MTRX_KEY", None)
|
|
384
|
-
env
|
|
491
|
+
env.pop("MATRX_CLAUDE_MODE", None)
|
|
492
|
+
env["MATRX_BASE_URL"] = proxy_root
|
|
385
493
|
env["MATRX_API_KEY"] = mx_key
|
|
386
|
-
# Claude Code gateway uses root URL, not /v1
|
|
387
494
|
env["ANTHROPIC_BASE_URL"] = proxy_base
|
|
495
|
+
env["ANTHROPIC_API_KEY"] = mx_key
|
|
388
496
|
group_id, project_id = _resolve_matrx_context_overrides(state, env)
|
|
389
497
|
session_id = str(uuid.uuid4())
|
|
390
498
|
# Evolutionary scaffolding: env snapshot for AI context injection
|
|
@@ -392,7 +500,6 @@ def _build_claude_env(
|
|
|
392
500
|
env_b64 = base64.b64encode(json.dumps(env_snap).encode()).decode() if env_snap else ""
|
|
393
501
|
custom_headers = "\n".join(
|
|
394
502
|
[
|
|
395
|
-
f"x-matrx-key: {mx_key}",
|
|
396
503
|
"x-matrx-agent-id: claude-cli",
|
|
397
504
|
"x-matrx-provider: claude_code",
|
|
398
505
|
f"x-matrx-session-id: {session_id}",
|
|
@@ -404,33 +511,34 @@ def _build_claude_env(
|
|
|
404
511
|
custom_headers += f"\nx-matrx-project-id: {project_id}"
|
|
405
512
|
if env_b64:
|
|
406
513
|
custom_headers += f"\nx-matrx-env: {env_b64}"
|
|
407
|
-
# Always send the matrx key via ANTHROPIC_CUSTOM_HEADERS so it arrives on
|
|
408
|
-
# the dedicated X-Matrx-Key header. This keeps the Authorization slot free
|
|
409
|
-
# for the real Anthropic credential (OAuth token or API key) so the proxy can
|
|
410
|
-
# forward it to Anthropic without confusing the matrx key with a provider key.
|
|
411
514
|
env["ANTHROPIC_CUSTOM_HEADERS"] = custom_headers
|
|
412
515
|
env.pop("ANTHROPIC_AUTH_TOKEN", None)
|
|
413
|
-
if _claude_uses_oauth(state):
|
|
414
|
-
# OAuth token flows through natively via Authorization: Bearer sk-ant-oat01-*
|
|
415
|
-
env.pop("ANTHROPIC_API_KEY", None)
|
|
416
|
-
else:
|
|
417
|
-
# Non-OAuth: if a saved Anthropic API key is available, set it so the SDK
|
|
418
|
-
# sends x-api-key to the proxy and the proxy can forward it upstream.
|
|
419
|
-
# If no key is saved, the proxy will fall back to the org vault.
|
|
420
|
-
if direct_key:
|
|
421
|
-
env["ANTHROPIC_API_KEY"] = direct_key
|
|
422
|
-
else:
|
|
423
|
-
env.pop("ANTHROPIC_API_KEY", None)
|
|
424
516
|
return env, matrx_auth_source
|
|
425
517
|
|
|
426
518
|
# Direct route: clear any matrx-managed env vars
|
|
427
519
|
env.pop("MTRX_KEY", None)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
520
|
+
env.pop("MATRX_CLAUDE_MODE", None)
|
|
521
|
+
env.pop("MATRX_BASE_URL", None)
|
|
522
|
+
env.pop("MATRX_API_KEY", None)
|
|
523
|
+
for candidate in {proxy_root, proxy_base}:
|
|
524
|
+
_clear_if_matches(env, "ANTHROPIC_BASE_URL", candidate)
|
|
525
|
+
|
|
526
|
+
anthropic_api_key = (env.get("ANTHROPIC_API_KEY") or "").strip()
|
|
527
|
+
if anthropic_api_key.startswith("mx_") or anthropic_api_key == mx_key:
|
|
528
|
+
env.pop("ANTHROPIC_API_KEY", None)
|
|
529
|
+
|
|
530
|
+
anthropic_auth_token = (env.get("ANTHROPIC_AUTH_TOKEN") or "").strip()
|
|
531
|
+
if (
|
|
532
|
+
anthropic_auth_token == mx_key
|
|
533
|
+
or anthropic_auth_token.startswith("mx_")
|
|
534
|
+
or anthropic_auth_token.lower().startswith("bearer mx_")
|
|
535
|
+
):
|
|
536
|
+
env.pop("ANTHROPIC_AUTH_TOKEN", None)
|
|
537
|
+
|
|
538
|
+
custom_headers = (env.get("ANTHROPIC_CUSTOM_HEADERS") or "").strip()
|
|
539
|
+
if "x-matrx-" in custom_headers.lower():
|
|
433
540
|
env.pop("ANTHROPIC_CUSTOM_HEADERS", None)
|
|
541
|
+
|
|
434
542
|
if claude_oauth_available():
|
|
435
543
|
return env, "local_claude_oauth"
|
|
436
544
|
if env.get("ANTHROPIC_API_KEY"):
|
|
@@ -450,11 +558,12 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
450
558
|
return
|
|
451
559
|
|
|
452
560
|
base_url = (plan.env.get("ANTHROPIC_BASE_URL") or "").strip()
|
|
561
|
+
expected_base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
453
562
|
if not base_url:
|
|
454
563
|
raise ValueError("Claude Matrx route is missing ANTHROPIC_BASE_URL")
|
|
455
|
-
if base_url
|
|
564
|
+
if base_url != expected_base_url:
|
|
456
565
|
raise ValueError(
|
|
457
|
-
"Claude Matrx route must use the
|
|
566
|
+
"Claude Matrx route must use the Matrx /v1 base URL. "
|
|
458
567
|
f"Got: {base_url}"
|
|
459
568
|
)
|
|
460
569
|
|
|
@@ -462,27 +571,24 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
462
571
|
if not mx_key.startswith("mx_"):
|
|
463
572
|
raise ValueError("Claude Matrx route is missing a valid MATRX_API_KEY")
|
|
464
573
|
|
|
465
|
-
|
|
466
|
-
|
|
574
|
+
anthropic_key = (plan.env.get("ANTHROPIC_API_KEY") or "").strip()
|
|
575
|
+
if anthropic_key != mx_key:
|
|
576
|
+
raise ValueError("Claude Matrx route must set ANTHROPIC_API_KEY to the same mx_ key as MATRX_API_KEY")
|
|
577
|
+
|
|
467
578
|
custom_headers = (plan.env.get("ANTHROPIC_CUSTOM_HEADERS") or "").strip()
|
|
468
|
-
if not custom_headers
|
|
469
|
-
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS
|
|
579
|
+
if not custom_headers:
|
|
580
|
+
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS")
|
|
470
581
|
lowered_headers = custom_headers.lower()
|
|
471
582
|
if "x-matrx-session-id:" not in lowered_headers:
|
|
472
583
|
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Session-Id")
|
|
473
584
|
if "x-matrx-provider: claude_code" not in lowered_headers:
|
|
474
585
|
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Provider=claude_code")
|
|
586
|
+
if "x-matrx-agent-id: claude-cli" not in lowered_headers:
|
|
587
|
+
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Agent-Id=claude-cli")
|
|
475
588
|
|
|
476
589
|
if plan.env.get("ANTHROPIC_AUTH_TOKEN"):
|
|
477
590
|
raise ValueError("Claude Matrx route should not set ANTHROPIC_AUTH_TOKEN")
|
|
478
591
|
|
|
479
|
-
if _claude_uses_oauth(state):
|
|
480
|
-
oauth_token = read_claude_oauth_token() or (state.get("auth", {}).get("claude_code", {}).get("oauth_token") or "").strip()
|
|
481
|
-
if not oauth_token:
|
|
482
|
-
raise ValueError("Claude OAuth was selected but no Claude OAuth token is available. Run: mtrx login claude-code --import")
|
|
483
|
-
if plan.env.get("ANTHROPIC_API_KEY"):
|
|
484
|
-
raise ValueError("Claude Matrx OAuth route should not set ANTHROPIC_API_KEY")
|
|
485
|
-
|
|
486
592
|
|
|
487
593
|
def _validate_codex_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
488
594
|
if plan.route != "matrx":
|
|
@@ -531,16 +637,18 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
|
|
|
531
637
|
]
|
|
532
638
|
|
|
533
639
|
if plan.tool == "claude":
|
|
534
|
-
base_url =
|
|
640
|
+
base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
535
641
|
custom_headers = (plan.env.get("ANTHROPIC_CUSTOM_HEADERS") or "").strip()
|
|
536
|
-
|
|
642
|
+
anthropic_key = (plan.env.get("ANTHROPIC_API_KEY") or "").strip()
|
|
537
643
|
return [
|
|
538
644
|
"Launching claude via Matrx",
|
|
539
645
|
f" base_url: {base_url or DEFAULT_MATRX_BASE_URL}",
|
|
540
646
|
f" auth_source: {plan.auth_source}",
|
|
541
|
-
f" oauth_mode: {_claude_uses_oauth(state)}",
|
|
542
647
|
f" custom_headers_present: {bool(custom_headers)}",
|
|
543
|
-
f"
|
|
648
|
+
f" proxy_key_present: {anthropic_key.startswith('mx_')}",
|
|
649
|
+
f" subscription_token_available: {bool(_resolve_claude_subscription_token(state))}",
|
|
650
|
+
" runtime_route: env injection",
|
|
651
|
+
" persistent_route: disabled",
|
|
544
652
|
]
|
|
545
653
|
|
|
546
654
|
return []
|
|
@@ -594,13 +702,6 @@ def _clear_if_matches(env: dict[str, str], key: str, expected: str) -> None:
|
|
|
594
702
|
env.pop(key, None)
|
|
595
703
|
|
|
596
704
|
|
|
597
|
-
def _claude_uses_oauth(state: dict) -> bool:
|
|
598
|
-
if read_claude_oauth_token():
|
|
599
|
-
return True
|
|
600
|
-
imported = (state.get("auth", {}).get("claude_code", {}).get("oauth_token") or "").strip()
|
|
601
|
-
return bool(imported)
|
|
602
|
-
|
|
603
|
-
|
|
604
705
|
def _sync_tool_route_config(state: dict, *, tool: str, route: str) -> bool:
|
|
605
706
|
if tool == "claude":
|
|
606
707
|
return _cleanup_claude_managed_config(state)
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -10,6 +10,8 @@ import threading
|
|
|
10
10
|
import urllib.parse
|
|
11
11
|
import webbrowser
|
|
12
12
|
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
13
15
|
from matrx.cli.launcher import (
|
|
14
16
|
prepare_routed_setup,
|
|
15
17
|
build_launch_plan,
|
|
@@ -52,6 +54,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
52
54
|
return _cmd_status()
|
|
53
55
|
if args.command == "doctor":
|
|
54
56
|
return _cmd_doctor()
|
|
57
|
+
if args.command == "personal":
|
|
58
|
+
return _cmd_personal(args)
|
|
55
59
|
if args.command in {"codex", "claude"}:
|
|
56
60
|
return _cmd_launch(args.command, args.route, remainder)
|
|
57
61
|
|
|
@@ -79,6 +83,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
79
83
|
subparsers.add_parser("status")
|
|
80
84
|
subparsers.add_parser("doctor")
|
|
81
85
|
|
|
86
|
+
personal = subparsers.add_parser("personal")
|
|
87
|
+
personal_subparsers = personal.add_subparsers(dest="personal_command")
|
|
88
|
+
optimize = personal_subparsers.add_parser("optimize")
|
|
89
|
+
optimize.add_argument("mode", choices=["on", "off", "status"])
|
|
90
|
+
|
|
82
91
|
codex = subparsers.add_parser("codex")
|
|
83
92
|
codex.add_argument("--route", choices=["direct", "matrx"])
|
|
84
93
|
|
|
@@ -379,6 +388,100 @@ def _cmd_status() -> int:
|
|
|
379
388
|
return 0
|
|
380
389
|
|
|
381
390
|
|
|
391
|
+
def _personal_matrx_key(state: dict) -> str:
|
|
392
|
+
return (state.get("auth", {}).get("matrx", {}).get("key") or "").strip()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _matrx_api_json(
|
|
396
|
+
state: dict,
|
|
397
|
+
*,
|
|
398
|
+
method: str,
|
|
399
|
+
path: str,
|
|
400
|
+
key: str,
|
|
401
|
+
json_body: dict | None = None,
|
|
402
|
+
) -> dict:
|
|
403
|
+
base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
404
|
+
url = f"{base_url.rstrip('/')}/v1{path}"
|
|
405
|
+
headers = {"X-Matrx-Key": key}
|
|
406
|
+
if json_body is not None:
|
|
407
|
+
headers["Content-Type"] = "application/json"
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
with httpx.Client(timeout=15) as client:
|
|
411
|
+
response = client.request(method, url, headers=headers, json=json_body)
|
|
412
|
+
except httpx.HTTPError as exc:
|
|
413
|
+
raise ValueError(f"Matrx API request failed: {exc}") from exc
|
|
414
|
+
|
|
415
|
+
if response.status_code >= 400:
|
|
416
|
+
detail = response.text.strip() or response.reason_phrase
|
|
417
|
+
raise ValueError(f"Matrx API error ({response.status_code}) for {path}: {detail}")
|
|
418
|
+
|
|
419
|
+
if response.status_code == 204 or not response.content:
|
|
420
|
+
return {}
|
|
421
|
+
return response.json()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _resolve_personal_policy_target(state: dict, *, key: str) -> tuple[dict, dict]:
|
|
425
|
+
context = _matrx_api_json(state, method="GET", path="/auth/context", key=key)
|
|
426
|
+
project_id = (context.get("project_id") or "").strip()
|
|
427
|
+
group_id = (context.get("group_id") or "").strip()
|
|
428
|
+
if not project_id:
|
|
429
|
+
raise ValueError("Saved Matrx key is not project-scoped; personal optimization is unavailable")
|
|
430
|
+
if not group_id:
|
|
431
|
+
raise ValueError("No default personal group found for the current Matrx project")
|
|
432
|
+
|
|
433
|
+
group = _matrx_api_json(state, method="GET", path=f"/groups/{group_id}", key=key)
|
|
434
|
+
policy_id = (group.get("policy_id") or "").strip()
|
|
435
|
+
if not policy_id:
|
|
436
|
+
raise ValueError("Default personal group has no optimization policy attached")
|
|
437
|
+
|
|
438
|
+
policy = _matrx_api_json(state, method="GET", path=f"/policies/{policy_id}", key=key)
|
|
439
|
+
return context, policy
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _cmd_personal_optimize(args) -> int:
|
|
443
|
+
state = load_state()
|
|
444
|
+
key = _personal_matrx_key(state)
|
|
445
|
+
if not key:
|
|
446
|
+
print("Personal Matrx login required. Run: mtrx login matrx", file=sys.stderr)
|
|
447
|
+
return 1
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
context, policy = _resolve_personal_policy_target(state, key=key)
|
|
451
|
+
mode = args.mode
|
|
452
|
+
if mode == "status":
|
|
453
|
+
print(f"Personal optimization: {policy.get('mode', 'unknown')}")
|
|
454
|
+
print(f" project: {context.get('project_id') or '-'}")
|
|
455
|
+
print(f" group: {context.get('group_id') or '-'}")
|
|
456
|
+
print(f" policy: {policy.get('id') or '-'}")
|
|
457
|
+
return 0
|
|
458
|
+
|
|
459
|
+
target_mode = "optimize" if mode == "on" else "observe"
|
|
460
|
+
updated = _matrx_api_json(
|
|
461
|
+
state,
|
|
462
|
+
method="PATCH",
|
|
463
|
+
path=f"/policies/{policy['id']}",
|
|
464
|
+
key=key,
|
|
465
|
+
json_body={"mode": target_mode},
|
|
466
|
+
)
|
|
467
|
+
except ValueError as exc:
|
|
468
|
+
print(str(exc), file=sys.stderr)
|
|
469
|
+
return 1
|
|
470
|
+
|
|
471
|
+
print(f"Personal optimization: {updated.get('mode', target_mode)}")
|
|
472
|
+
print(f" project: {context.get('project_id') or '-'}")
|
|
473
|
+
print(f" group: {context.get('group_id') or '-'}")
|
|
474
|
+
print(f" policy: {updated.get('id') or policy.get('id') or '-'}")
|
|
475
|
+
return 0
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _cmd_personal(args) -> int:
|
|
479
|
+
if args.personal_command == "optimize":
|
|
480
|
+
return _cmd_personal_optimize(args)
|
|
481
|
+
print("Use: mtrx personal optimize <on|off|status>", file=sys.stderr)
|
|
482
|
+
return 1
|
|
483
|
+
|
|
484
|
+
|
|
382
485
|
def _cmd_doctor() -> int:
|
|
383
486
|
state = load_state()
|
|
384
487
|
failures = 0
|