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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrx-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "MATRX CLI for routing Codex and Claude through Matrx",
5
5
  "homepage": "https://mtrx.so",
6
6
  "repository": {
@@ -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 or os.environ)
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 or os.environ)
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 _claude_uses_oauth(state):
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 _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")
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: {_claude_uses_oauth(state)}",
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)
@@ -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