mtrx-cli 0.1.0 → 0.1.1
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 +15 -8
- package/src/matrx/cli/main.py +103 -0
package/package.json
CHANGED
|
@@ -123,7 +123,7 @@ def build_launch_plan(
|
|
|
123
123
|
raise ValueError(f"{tool} executable not found in PATH")
|
|
124
124
|
|
|
125
125
|
route = resolve_route(state, tool, route_override)
|
|
126
|
-
env = dict(base_env
|
|
126
|
+
env = dict(os.environ if base_env is None else base_env)
|
|
127
127
|
auth_source = ""
|
|
128
128
|
launch_args = list(passthrough_args or [])
|
|
129
129
|
|
|
@@ -164,7 +164,7 @@ def prepare_routed_setup(
|
|
|
164
164
|
"""
|
|
165
165
|
route = resolve_route(state, tool, route_override)
|
|
166
166
|
changed = False
|
|
167
|
-
env = dict(base_env
|
|
167
|
+
env = dict(os.environ if base_env is None else base_env)
|
|
168
168
|
if route == "matrx" and _ensure_matrx_auth(state, env=env):
|
|
169
169
|
changed = True
|
|
170
170
|
if route == "matrx" and _persist_workspace_binding_from_env(state, env):
|
|
@@ -376,6 +376,7 @@ def _build_claude_env(
|
|
|
376
376
|
proxy_base = ensure_root_url(matrx.get("base_url"))
|
|
377
377
|
mx_key, matrx_auth_source = _resolve_matrx_route_key(state, env)
|
|
378
378
|
direct_key = (anthropic.get("key") or "").strip()
|
|
379
|
+
oauth_mode = _claude_effective_oauth_mode(state, env)
|
|
379
380
|
|
|
380
381
|
if route == "matrx":
|
|
381
382
|
if not mx_key:
|
|
@@ -410,7 +411,7 @@ def _build_claude_env(
|
|
|
410
411
|
# forward it to Anthropic without confusing the matrx key with a provider key.
|
|
411
412
|
env["ANTHROPIC_CUSTOM_HEADERS"] = custom_headers
|
|
412
413
|
env.pop("ANTHROPIC_AUTH_TOKEN", None)
|
|
413
|
-
if
|
|
414
|
+
if oauth_mode:
|
|
414
415
|
# OAuth token flows through natively via Authorization: Bearer sk-ant-oat01-*
|
|
415
416
|
env.pop("ANTHROPIC_API_KEY", None)
|
|
416
417
|
else:
|
|
@@ -476,10 +477,7 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
476
477
|
if plan.env.get("ANTHROPIC_AUTH_TOKEN"):
|
|
477
478
|
raise ValueError("Claude Matrx route should not set ANTHROPIC_AUTH_TOKEN")
|
|
478
479
|
|
|
479
|
-
if
|
|
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")
|
|
480
|
+
if _claude_effective_oauth_mode(state, plan.env):
|
|
483
481
|
if plan.env.get("ANTHROPIC_API_KEY"):
|
|
484
482
|
raise ValueError("Claude Matrx OAuth route should not set ANTHROPIC_API_KEY")
|
|
485
483
|
|
|
@@ -538,7 +536,7 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
|
|
|
538
536
|
"Launching claude via Matrx",
|
|
539
537
|
f" base_url: {base_url or DEFAULT_MATRX_BASE_URL}",
|
|
540
538
|
f" auth_source: {plan.auth_source}",
|
|
541
|
-
f" oauth_mode: {
|
|
539
|
+
f" oauth_mode: {_claude_effective_oauth_mode(state, plan.env)}",
|
|
542
540
|
f" custom_headers_present: {bool(custom_headers)}",
|
|
543
541
|
f" api_key_present: {api_key_present}",
|
|
544
542
|
]
|
|
@@ -601,6 +599,15 @@ def _claude_uses_oauth(state: dict) -> bool:
|
|
|
601
599
|
return bool(imported)
|
|
602
600
|
|
|
603
601
|
|
|
602
|
+
def _claude_effective_oauth_mode(state: dict, env: dict[str, str] | None = None) -> bool:
|
|
603
|
+
requested = ((env or {}).get("MATRX_CLAUDE_MODE") or "").strip().lower()
|
|
604
|
+
if requested == "oauth":
|
|
605
|
+
return True
|
|
606
|
+
if requested == "api-key":
|
|
607
|
+
return False
|
|
608
|
+
return _claude_uses_oauth(state)
|
|
609
|
+
|
|
610
|
+
|
|
604
611
|
def _sync_tool_route_config(state: dict, *, tool: str, route: str) -> bool:
|
|
605
612
|
if tool == "claude":
|
|
606
613
|
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
|