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 +1 -1
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/launcher.py +89 -40
- package/src/matrx/cli/main.py +62 -4
package/package.json
CHANGED
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
env["
|
|
616
|
-
env["GEMINI_API_ENDPOINT"] =
|
|
617
|
-
env["
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
|
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 =
|
|
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
|
|
944
|
+
"Gemini Matrx route must use the Matrx Gemini-native base URL. "
|
|
913
945
|
f"Got: {base_url}"
|
|
914
946
|
)
|
|
915
947
|
|
|
916
|
-
|
|
917
|
-
if
|
|
918
|
-
raise ValueError("Gemini Matrx route is missing a
|
|
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"
|
|
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
|
]
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -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
|
-
|
|
493
|
-
|
|
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"({
|
|
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
|
|