mtrx-cli 0.1.20 → 0.1.22

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.20",
3
+ "version": "0.1.22",
4
4
  "description": "MATRX CLI for routing Codex, Claude, and Cursor through Matrx",
5
5
  "homepage": "https://mtrx.so",
6
6
  "repository": {
@@ -1 +1 @@
1
- __version__ = "0.1.19"
1
+ __version__ = "0.1.22"
@@ -537,6 +537,11 @@ def _build_codex_env(
537
537
  header_parts.append(f'"X-Matrx-Group" = "{group_id}"')
538
538
  if project_id:
539
539
  header_parts.append(f'"X-Matrx-Project-Id" = "{project_id}"')
540
+ _git_branch, _git_commit = _capture_git_context(_workspace_cwd(env))
541
+ if _git_branch:
542
+ header_parts.append(f'"X-Matrx-Branch" = "{_git_branch}"')
543
+ if _git_commit:
544
+ header_parts.append(f'"X-Matrx-Commit" = "{_git_commit}"')
540
545
  if env_b64:
541
546
  header_parts.append(f'"X-Matrx-Env" = "{env_b64}"')
542
547
  headers_str = ", ".join(header_parts)
@@ -574,33 +579,21 @@ def _build_gemini_env(
574
579
  orchestration: dict | None = None,
575
580
  ) -> tuple[dict[str, str], str]:
576
581
  matrx = state["auth"]["matrx"]
577
- # Assuming we might store Gemini-specific keys in future, or use OpenAI key fallback
578
- # For now, we don't have a specific 'gemini' auth section in state.py, but we can assume
579
- # if direct route, we use env var.
582
+ proxy_root = ensure_root_url(matrx.get("base_url"))
580
583
  proxy_base = ensure_v1_url(matrx.get("base_url"))
581
584
  mx_key, matrx_auth_source = _resolve_matrx_route_key(state, env)
582
-
583
- # Check for direct key in env or potentially saved elsewhere
584
- direct_key = (env.get("GOOGLE_API_KEY") or "").strip()
585
+ direct_key = (env.get("GEMINI_API_KEY") or env.get("GOOGLE_API_KEY") or "").strip()
585
586
 
586
587
  if route == "matrx":
587
588
  if not mx_key:
588
589
  raise ValueError("No Matrx key available. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
589
-
590
- # Clear existing Gemini config to force proxy usage
591
590
  env.pop("MTRX_KEY", None)
592
-
593
591
  group_id, project_id = _resolve_matrx_context_overrides(state, env)
594
592
  session_id = str(uuid.uuid4())
595
593
  runtime_agent_id = (
596
594
  (orchestration or {}).get("agent_id")
597
595
  or _runtime_agent_basename("gemini")[0]
598
596
  )
599
-
600
- # Build a project-aware proxy base URL so the proxy can identify the project
601
- # even though Gemini CLI doesn't support custom request headers via env vars.
602
- # We append matrx context as query params which the proxy reads.
603
- proxy_base_with_ctx = proxy_base
604
597
  ctx_params: list[str] = []
605
598
  if project_id:
606
599
  ctx_params.append(f"mtrx_project={project_id}")
@@ -608,31 +601,65 @@ def _build_gemini_env(
608
601
  ctx_params.append(f"mtrx_session={session_id}")
609
602
  if runtime_agent_id:
610
603
  ctx_params.append(f"mtrx_agent={runtime_agent_id}")
611
- if ctx_params:
612
- proxy_base_with_ctx = f"{proxy_base}?{'&'.join(ctx_params)}"
604
+ git_branch, git_commit = _capture_git_context(_workspace_cwd(env))
605
+ if git_branch:
606
+ ctx_params.append(f"mtrx_branch={git_branch}")
607
+ if git_commit:
608
+ ctx_params.append(f"mtrx_commit={git_commit}")
609
+
610
+ query_suffix = f"?{'&'.join(ctx_params)}" if ctx_params else ""
611
+ env_snap = _capture_env_snapshot()
612
+ env_b64 = base64.b64encode(json.dumps(env_snap).encode()).decode() if env_snap else ""
613
+ custom_headers = [
614
+ f"x-matrx-key: {mx_key}",
615
+ f"x-matrx-agent-id: {runtime_agent_id}",
616
+ "x-matrx-provider: gemini_code",
617
+ f"x-matrx-session-id: {session_id}",
618
+ ]
619
+ if group_id:
620
+ custom_headers.append(f"x-matrx-group: {group_id}")
621
+ if project_id:
622
+ custom_headers.append(f"x-matrx-project-id: {project_id}")
623
+ if git_branch:
624
+ custom_headers.append(f"x-matrx-branch: {git_branch}")
625
+ if git_commit:
626
+ custom_headers.append(f"x-matrx-commit: {git_commit}")
627
+ if env_b64:
628
+ custom_headers.append(f"x-matrx-env: {env_b64}")
613
629
 
614
- # Set Proxy Config
615
- env["GOOGLE_GEMINI_BASE_URL"] = proxy_base_with_ctx
616
- env["GEMINI_API_ENDPOINT"] = proxy_base_with_ctx
617
- env["GOOGLE_API_KEY"] = mx_key
630
+ env["GOOGLE_GEMINI_BASE_URL"] = f"{proxy_base}/v1beta{query_suffix}"
631
+ env["GOOGLE_VERTEX_BASE_URL"] = f"{proxy_base}/v1beta{query_suffix}"
632
+ env["GEMINI_API_ENDPOINT"] = env["GOOGLE_GEMINI_BASE_URL"]
633
+ env["CODE_ASSIST_ENDPOINT"] = proxy_base
634
+ env["GEMINI_CLI_CUSTOM_HEADERS"] = ", ".join(custom_headers)
635
+ env["GEMINI_API_KEY_AUTH_MECHANISM"] = "bearer"
618
636
 
619
637
  return env, matrx_auth_source
620
638
 
621
639
  # Direct route: clear any matrx-managed env vars
622
640
  env.pop("MTRX_KEY", None)
623
-
624
- # Clear proxy overrides
625
- _clear_if_matches(env, "GOOGLE_GEMINI_BASE_URL", proxy_base)
626
- _clear_if_matches(env, "GEMINI_API_ENDPOINT", proxy_base)
627
-
628
- # Clear key if it was the Matrx key
629
- current_key = (env.get("GOOGLE_API_KEY") or "").strip()
630
- if current_key == mx_key or current_key.startswith("mx_"):
631
- env.pop("GOOGLE_API_KEY", None)
632
-
633
- if env.get("GOOGLE_API_KEY"):
641
+ for key in (
642
+ "GOOGLE_GEMINI_BASE_URL",
643
+ "GOOGLE_VERTEX_BASE_URL",
644
+ "GEMINI_API_ENDPOINT",
645
+ "CODE_ASSIST_ENDPOINT",
646
+ ):
647
+ value = (env.get(key) or "").strip()
648
+ if "matrx" in value.lower() or "mtrx.so" in value.lower():
649
+ env.pop(key, None)
650
+
651
+ custom_headers = (env.get("GEMINI_CLI_CUSTOM_HEADERS") or "").strip().lower()
652
+ if "x-matrx-" in custom_headers:
653
+ env.pop("GEMINI_CLI_CUSTOM_HEADERS", None)
654
+ if (env.get("GEMINI_API_KEY_AUTH_MECHANISM") or "").strip().lower() == "bearer":
655
+ env.pop("GEMINI_API_KEY_AUTH_MECHANISM", None)
656
+
657
+ if env.get("GEMINI_API_KEY") or env.get("GOOGLE_API_KEY"):
634
658
  return env, "existing_google_env"
635
-
659
+
660
+ if direct_key:
661
+ return env, "existing_gemini_auth"
662
+
636
663
  return env, "missing_auth"
637
664
 
638
665
 
@@ -680,6 +707,11 @@ def _build_claude_env(
680
707
  custom_headers += f"\nx-matrx-group: {group_id}"
681
708
  if project_id:
682
709
  custom_headers += f"\nx-matrx-project-id: {project_id}"
710
+ _git_branch, _git_commit = _capture_git_context(_workspace_cwd(env))
711
+ if _git_branch:
712
+ custom_headers += f"\nx-matrx-branch: {_git_branch}"
713
+ if _git_commit:
714
+ custom_headers += f"\nx-matrx-commit: {_git_commit}"
683
715
  if env_b64:
684
716
  custom_headers += f"\nx-matrx-env: {env_b64}"
685
717
  env["ANTHROPIC_CUSTOM_HEADERS"] = custom_headers
@@ -896,26 +928,40 @@ def _validate_gemini_launch_plan(plan: LaunchPlan, state: dict) -> None:
896
928
  return
897
929
 
898
930
  expected_base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
931
+ expected_gemini_base = f"{expected_base_url}/v1beta"
899
932
 
900
933
  base_url = (plan.env.get("GOOGLE_GEMINI_BASE_URL") or "").strip()
901
934
  if not base_url:
902
935
  base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
903
936
 
904
937
  if not base_url:
905
- raise ValueError("Gemini Matrx route is missing GOOGLE_GEMINI_BASE_URL or GEMINI_API_ENDPOINT")
938
+ raise ValueError("Gemini Matrx route is missing GOOGLE_GEMINI_BASE_URL")
906
939
 
907
- # Strip query params before comparing (project_id context may be appended as ?mtrx_project=...)
908
940
  base_url_no_qs = base_url.split("?")[0].rstrip("/")
909
- expected_no_qs = expected_base_url.rstrip("/")
941
+ expected_no_qs = expected_gemini_base.rstrip("/")
910
942
  if base_url_no_qs != expected_no_qs:
911
943
  raise ValueError(
912
- "Gemini Matrx route must use the Matrx /v1 base URL. "
944
+ "Gemini Matrx route must use the Matrx Gemini-native base URL. "
913
945
  f"Got: {base_url}"
914
946
  )
915
947
 
916
- mx_key = (plan.env.get("GOOGLE_API_KEY") or "").strip()
917
- if not mx_key.startswith("mx_"):
918
- raise ValueError("Gemini Matrx route is missing a valid GOOGLE_API_KEY (should be mx_...)")
948
+ vertex_base = (plan.env.get("GOOGLE_VERTEX_BASE_URL") or "").strip()
949
+ if vertex_base and vertex_base.split("?")[0].rstrip("/") != expected_no_qs:
950
+ raise ValueError("Gemini Matrx route is missing a Matrx GOOGLE_VERTEX_BASE_URL")
951
+
952
+ code_assist_endpoint = (plan.env.get("CODE_ASSIST_ENDPOINT") or "").strip().rstrip("/")
953
+ if code_assist_endpoint != expected_base_url.rstrip("/"):
954
+ raise ValueError("Gemini Matrx route is missing a Matrx CODE_ASSIST_ENDPOINT")
955
+
956
+ custom_headers = (plan.env.get("GEMINI_CLI_CUSTOM_HEADERS") or "").strip().lower()
957
+ if "x-matrx-key:" not in custom_headers:
958
+ raise ValueError("Gemini Matrx route is missing GEMINI_CLI_CUSTOM_HEADERS with X-Matrx-Key")
959
+ if "x-matrx-provider: gemini_code" not in custom_headers:
960
+ raise ValueError("Gemini Matrx route is missing GEMINI_CLI_CUSTOM_HEADERS with X-Matrx-Provider=gemini_code")
961
+ if "x-matrx-session-id:" not in custom_headers:
962
+ raise ValueError("Gemini Matrx route is missing GEMINI_CLI_CUSTOM_HEADERS with X-Matrx-Session-Id")
963
+ if "x-matrx-agent-id:" not in custom_headers:
964
+ raise ValueError("Gemini Matrx route is missing GEMINI_CLI_CUSTOM_HEADERS with X-Matrx-Agent-Id")
919
965
 
920
966
 
921
967
  def _validate_codex_launch_plan(plan: LaunchPlan, state: dict) -> None:
@@ -981,10 +1027,13 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
981
1027
  if plan.tool == "gemini":
982
1028
  base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
983
1029
  _, project_id = _resolve_matrx_context_overrides(state, dict(plan.env))
1030
+ custom_headers = (plan.env.get("GEMINI_CLI_CUSTOM_HEADERS") or "").strip()
984
1031
  lines = [
985
1032
  "Launching gemini via Matrx",
986
- f" base_url: {base_url}",
1033
+ f" gemini_base_url: {plan.env.get('GOOGLE_GEMINI_BASE_URL') or ''}",
1034
+ f" code_assist_endpoint: {plan.env.get('CODE_ASSIST_ENDPOINT') or ''}",
987
1035
  f" auth_source: {plan.auth_source}",
1036
+ f" custom_headers_present: {bool(custom_headers)}",
988
1037
  " runtime_route: env injection",
989
1038
  " persistent_route: disabled",
990
1039
  ]
@@ -466,10 +466,13 @@ def _cmd_login(args) -> int:
466
466
 
467
467
  key = (args.key or "").strip()
468
468
  if provider == "matrx":
469
+ url_changed = False
469
470
  if args.base_url:
470
471
  state["auth"]["matrx"]["base_url"] = args.base_url.strip()
472
+ url_changed = True
471
473
  if args.app_url:
472
474
  state["auth"]["matrx"]["app_url"] = args.app_url.strip()
475
+ url_changed = True
473
476
  elif not state["auth"]["matrx"].get("app_url"):
474
477
  state["auth"]["matrx"]["app_url"] = ensure_app_url(
475
478
  None,
@@ -484,13 +487,14 @@ def _cmd_login(args) -> int:
484
487
  except ValueError as exc:
485
488
  print(str(exc), file=sys.stderr)
486
489
  return 1
487
- if changed:
490
+ if changed or url_changed:
488
491
  path = save_state(state)
489
492
  print(f"Saved {args.provider} credentials to {path}")
490
493
  print(f"Matrx base URL: {ensure_v1_url(state['auth']['matrx']['base_url'])}")
491
494
  return 0
492
- print("Matrx login did not change the current state", file=sys.stderr)
493
- return 1
495
+ if not url_changed:
496
+ print("Matrx login did not change the current state", file=sys.stderr)
497
+ return 1
494
498
 
495
499
  if not key:
496
500
  print("--key is required for this login command", file=sys.stderr)
@@ -585,11 +589,18 @@ def _cmd_status() -> int:
585
589
  print("Active project: none (run: mtrx project switch <name>)")
586
590
 
587
591
  print("Defaults:")
592
+ for tool in ("codex", "claude", "gemini", "cursor"):
593
+ route = configured_route(state, tool) or "not set"
594
+ print(f" {tool}: {route}")
588
595
  print("Auth:")
596
+ config_base = auth["matrx"].get("base_url")
597
+ env_base = (os.environ.get("MATRX_BASE_URL") or "").strip()
598
+ effective_base = ensure_v1_url(env_base or config_base)
599
+ base_source = "MATRX_BASE_URL env" if env_base else "config"
589
600
  print(
590
601
  " matrx: "
591
602
  f"{mask_secret(auth['matrx'].get('key'))} "
592
- f"({ensure_v1_url(auth['matrx'].get('base_url'))})"
603
+ f"({effective_base}) [{base_source}]"
593
604
  )
594
605
  print(f" matrx app: {ensure_app_url(auth['matrx'].get('app_url'), base_url=auth['matrx'].get('base_url'))}")
595
606
  print(f" openai: {mask_secret(auth['openai'].get('key'))}")
@@ -617,6 +628,24 @@ def _cmd_status() -> int:
617
628
  proxy_running = is_proxy_running()
618
629
  print(f" cursor proxy: {'running' if proxy_running else 'not running'}")
619
630
 
631
+ # Matrx API proxy health (when base_url configured)
632
+ base_url = auth["matrx"].get("base_url")
633
+ if base_url:
634
+ try:
635
+ health_url = f"{ensure_root_url(base_url).rstrip('/')}/health/wiring"
636
+ with httpx.Client(timeout=5) as client:
637
+ resp = client.get(health_url)
638
+ if resp.status_code == 200:
639
+ data = resp.json()
640
+ wiring = "ok" if data.get("proxy_wiring_ok") else "incomplete"
641
+ kv = data.get("key_vault", {})
642
+ enc = "configured" if kv.get("encryption_configured") else "not configured"
643
+ print(f" matrx proxy: reachable (wiring={wiring}, key_vault={enc})")
644
+ else:
645
+ print(f" matrx proxy: HTTP {resp.status_code}")
646
+ except Exception as exc:
647
+ print(f" matrx proxy: unreachable ({exc})")
648
+
620
649
  print("Executables:")
621
650
  print(f" codex: {find_executable('codex') or 'not found'}")
622
651
  print(f" claude: {find_executable('claude') or 'not found'}")
@@ -822,6 +851,35 @@ def _cmd_doctor() -> int:
822
851
  else:
823
852
  print(f"[warn] {tool} native config not configured")
824
853
 
854
+ # Proxy health check — when using matrx route, verify server is reachable
855
+ base_url = state.get("auth", {}).get("matrx", {}).get("base_url")
856
+ mx_key = matrx_key or workspace_matrx_key or env_matrx_key
857
+ if base_url and mx_key:
858
+ try:
859
+ health_url = f"{ensure_root_url(base_url).rstrip('/')}/health/wiring"
860
+ with httpx.Client(timeout=10) as client:
861
+ resp = client.get(health_url)
862
+ if resp.status_code == 200:
863
+ data = resp.json()
864
+ if data.get("proxy_wiring_ok"):
865
+ print("[ok] Matrx proxy reachable and wired")
866
+ else:
867
+ comps = data.get("components", {})
868
+ missing = [k for k, v in comps.items() if not v]
869
+ print(f"[warn] Matrx proxy incomplete: missing {missing}")
870
+ kv = data.get("key_vault", {})
871
+ if not kv.get("encryption_configured"):
872
+ print(
873
+ "[warn] Key vault encryption not configured on server — "
874
+ "provider keys may fail to decrypt; set KEY_VAULT_ENCRYPTION_KEY"
875
+ )
876
+ else:
877
+ print(f"[warn] Matrx proxy health check failed: {resp.status_code}")
878
+ except httpx.HTTPError as exc:
879
+ print(f"[warn] Matrx proxy unreachable: {exc}")
880
+ except Exception as exc:
881
+ print(f"[warn] Matrx proxy check failed: {exc}")
882
+
825
883
  return 1 if failures else 0
826
884
 
827
885