mtrx-cli 0.1.19 → 0.1.21
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 +95 -20
- package/src/matrx/cli/main.py +112 -8
package/package.json
CHANGED
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.21"
|
|
@@ -214,6 +214,11 @@ def prepare_routed_setup(
|
|
|
214
214
|
if route != "matrx":
|
|
215
215
|
return state, changed
|
|
216
216
|
|
|
217
|
+
if tool == "claude":
|
|
218
|
+
env = dict(os.environ if base_env is None else base_env)
|
|
219
|
+
if _sync_claude_subscription_to_matrx(state, env):
|
|
220
|
+
changed = True
|
|
221
|
+
|
|
217
222
|
return state, changed
|
|
218
223
|
|
|
219
224
|
|
|
@@ -432,6 +437,32 @@ def _workspace_cwd(env: dict[str, str]) -> str:
|
|
|
432
437
|
return (env.get("PWD") or os.getcwd()).strip()
|
|
433
438
|
|
|
434
439
|
|
|
440
|
+
def _capture_git_context(cwd: str | None = None) -> tuple[str, str]:
|
|
441
|
+
"""Return (branch, commit_sha) for the current git repo, or ('', '') on failure."""
|
|
442
|
+
root = cwd or os.getcwd()
|
|
443
|
+
branch = ""
|
|
444
|
+
commit = ""
|
|
445
|
+
try:
|
|
446
|
+
r = subprocess.run(
|
|
447
|
+
["git", "-C", root, "branch", "--show-current"],
|
|
448
|
+
capture_output=True, text=True, timeout=2, check=False,
|
|
449
|
+
)
|
|
450
|
+
if r.returncode == 0:
|
|
451
|
+
branch = r.stdout.strip()
|
|
452
|
+
except (OSError, subprocess.SubprocessError):
|
|
453
|
+
pass
|
|
454
|
+
try:
|
|
455
|
+
r = subprocess.run(
|
|
456
|
+
["git", "-C", root, "rev-parse", "--short", "HEAD"],
|
|
457
|
+
capture_output=True, text=True, timeout=2, check=False,
|
|
458
|
+
)
|
|
459
|
+
if r.returncode == 0:
|
|
460
|
+
commit = r.stdout.strip()
|
|
461
|
+
except (OSError, subprocess.SubprocessError):
|
|
462
|
+
pass
|
|
463
|
+
return branch, commit
|
|
464
|
+
|
|
465
|
+
|
|
435
466
|
def _resolve_matrx_context_overrides(
|
|
436
467
|
state: dict,
|
|
437
468
|
env: dict[str, str],
|
|
@@ -506,6 +537,11 @@ def _build_codex_env(
|
|
|
506
537
|
header_parts.append(f'"X-Matrx-Group" = "{group_id}"')
|
|
507
538
|
if project_id:
|
|
508
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}"')
|
|
509
545
|
if env_b64:
|
|
510
546
|
header_parts.append(f'"X-Matrx-Env" = "{env_b64}"')
|
|
511
547
|
headers_str = ", ".join(header_parts)
|
|
@@ -555,19 +591,36 @@ def _build_gemini_env(
|
|
|
555
591
|
if route == "matrx":
|
|
556
592
|
if not mx_key:
|
|
557
593
|
raise ValueError("No Matrx key available. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
|
|
558
|
-
|
|
594
|
+
|
|
559
595
|
# Clear existing Gemini config to force proxy usage
|
|
560
596
|
env.pop("MTRX_KEY", None)
|
|
561
|
-
|
|
597
|
+
|
|
598
|
+
group_id, project_id = _resolve_matrx_context_overrides(state, env)
|
|
599
|
+
session_id = str(uuid.uuid4())
|
|
600
|
+
runtime_agent_id = (
|
|
601
|
+
(orchestration or {}).get("agent_id")
|
|
602
|
+
or _runtime_agent_basename("gemini")[0]
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Build a project-aware proxy base URL so the proxy can identify the project
|
|
606
|
+
# even though Gemini CLI doesn't support custom request headers via env vars.
|
|
607
|
+
# We append matrx context as query params which the proxy reads.
|
|
608
|
+
proxy_base_with_ctx = proxy_base
|
|
609
|
+
ctx_params: list[str] = []
|
|
610
|
+
if project_id:
|
|
611
|
+
ctx_params.append(f"mtrx_project={project_id}")
|
|
612
|
+
if session_id:
|
|
613
|
+
ctx_params.append(f"mtrx_session={session_id}")
|
|
614
|
+
if runtime_agent_id:
|
|
615
|
+
ctx_params.append(f"mtrx_agent={runtime_agent_id}")
|
|
616
|
+
if ctx_params:
|
|
617
|
+
proxy_base_with_ctx = f"{proxy_base}?{'&'.join(ctx_params)}"
|
|
618
|
+
|
|
562
619
|
# Set Proxy Config
|
|
563
|
-
env["GOOGLE_GEMINI_BASE_URL"] =
|
|
564
|
-
env["GEMINI_API_ENDPOINT"] =
|
|
620
|
+
env["GOOGLE_GEMINI_BASE_URL"] = proxy_base_with_ctx
|
|
621
|
+
env["GEMINI_API_ENDPOINT"] = proxy_base_with_ctx
|
|
565
622
|
env["GOOGLE_API_KEY"] = mx_key
|
|
566
|
-
|
|
567
|
-
# Matrx-specific headers (if supported by the tool, or for our own tracking)
|
|
568
|
-
# Note: Standard Gemini CLI might not support custom headers via env vars easily.
|
|
569
|
-
# We rely on the Base URL routing to Matrx proxy which handles the logic.
|
|
570
|
-
|
|
623
|
+
|
|
571
624
|
return env, matrx_auth_source
|
|
572
625
|
|
|
573
626
|
# Direct route: clear any matrx-managed env vars
|
|
@@ -632,6 +685,11 @@ def _build_claude_env(
|
|
|
632
685
|
custom_headers += f"\nx-matrx-group: {group_id}"
|
|
633
686
|
if project_id:
|
|
634
687
|
custom_headers += f"\nx-matrx-project-id: {project_id}"
|
|
688
|
+
_git_branch, _git_commit = _capture_git_context(_workspace_cwd(env))
|
|
689
|
+
if _git_branch:
|
|
690
|
+
custom_headers += f"\nx-matrx-branch: {_git_branch}"
|
|
691
|
+
if _git_commit:
|
|
692
|
+
custom_headers += f"\nx-matrx-commit: {_git_commit}"
|
|
635
693
|
if env_b64:
|
|
636
694
|
custom_headers += f"\nx-matrx-env: {env_b64}"
|
|
637
695
|
env["ANTHROPIC_CUSTOM_HEADERS"] = custom_headers
|
|
@@ -709,14 +767,14 @@ def _build_orchestration_metadata(
|
|
|
709
767
|
}
|
|
710
768
|
|
|
711
769
|
|
|
712
|
-
def _best_effort_register_cli_agent(orchestration: dict) ->
|
|
770
|
+
def _best_effort_register_cli_agent(orchestration: dict) -> bool:
|
|
713
771
|
if not orchestration:
|
|
714
|
-
return
|
|
772
|
+
return False
|
|
715
773
|
base_url = (orchestration.get("base_url") or "").rstrip("/")
|
|
716
774
|
matrx_key = (orchestration.get("matrx_key") or "").strip()
|
|
717
775
|
agent_id = (orchestration.get("agent_id") or "").strip()
|
|
718
776
|
if not base_url or not matrx_key or not agent_id:
|
|
719
|
-
return
|
|
777
|
+
return False
|
|
720
778
|
|
|
721
779
|
payload = {
|
|
722
780
|
"agent_id": agent_id,
|
|
@@ -743,10 +801,11 @@ def _best_effort_register_cli_agent(orchestration: dict) -> None:
|
|
|
743
801
|
json=payload,
|
|
744
802
|
)
|
|
745
803
|
if response.status_code in {404, 401, 403}:
|
|
746
|
-
return
|
|
804
|
+
return False
|
|
747
805
|
response.raise_for_status()
|
|
806
|
+
return True
|
|
748
807
|
except httpx.HTTPError:
|
|
749
|
-
return
|
|
808
|
+
return False
|
|
750
809
|
|
|
751
810
|
|
|
752
811
|
def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
@@ -773,6 +832,14 @@ def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
|
773
832
|
f"{base_url}/v1/orchestration/agents/{agent_id}/heartbeat",
|
|
774
833
|
headers=headers,
|
|
775
834
|
)
|
|
835
|
+
if response.status_code == 404:
|
|
836
|
+
if not _best_effort_register_cli_agent(orchestration):
|
|
837
|
+
return
|
|
838
|
+
with httpx.Client(timeout=5) as client:
|
|
839
|
+
response = client.patch(
|
|
840
|
+
f"{base_url}/v1/orchestration/agents/{agent_id}/heartbeat",
|
|
841
|
+
headers=headers,
|
|
842
|
+
)
|
|
776
843
|
if response.status_code in {404, 401, 403}:
|
|
777
844
|
return
|
|
778
845
|
except httpx.HTTPError:
|
|
@@ -839,16 +906,18 @@ def _validate_gemini_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
839
906
|
return
|
|
840
907
|
|
|
841
908
|
expected_base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
842
|
-
|
|
909
|
+
|
|
843
910
|
base_url = (plan.env.get("GOOGLE_GEMINI_BASE_URL") or "").strip()
|
|
844
911
|
if not base_url:
|
|
845
|
-
|
|
846
|
-
base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
|
|
912
|
+
base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
|
|
847
913
|
|
|
848
914
|
if not base_url:
|
|
849
915
|
raise ValueError("Gemini Matrx route is missing GOOGLE_GEMINI_BASE_URL or GEMINI_API_ENDPOINT")
|
|
850
|
-
|
|
851
|
-
|
|
916
|
+
|
|
917
|
+
# Strip query params before comparing (project_id context may be appended as ?mtrx_project=...)
|
|
918
|
+
base_url_no_qs = base_url.split("?")[0].rstrip("/")
|
|
919
|
+
expected_no_qs = expected_base_url.rstrip("/")
|
|
920
|
+
if base_url_no_qs != expected_no_qs:
|
|
852
921
|
raise ValueError(
|
|
853
922
|
"Gemini Matrx route must use the Matrx /v1 base URL. "
|
|
854
923
|
f"Got: {base_url}"
|
|
@@ -921,13 +990,19 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
|
|
|
921
990
|
|
|
922
991
|
if plan.tool == "gemini":
|
|
923
992
|
base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
924
|
-
|
|
993
|
+
_, project_id = _resolve_matrx_context_overrides(state, dict(plan.env))
|
|
994
|
+
lines = [
|
|
925
995
|
"Launching gemini via Matrx",
|
|
926
996
|
f" base_url: {base_url}",
|
|
927
997
|
f" auth_source: {plan.auth_source}",
|
|
928
998
|
" runtime_route: env injection",
|
|
929
999
|
" persistent_route: disabled",
|
|
930
1000
|
]
|
|
1001
|
+
if project_id:
|
|
1002
|
+
lines.append(f" project_id: {project_id}")
|
|
1003
|
+
else:
|
|
1004
|
+
lines.append(" project_id: (from API key scope)")
|
|
1005
|
+
return lines
|
|
931
1006
|
|
|
932
1007
|
return []
|
|
933
1008
|
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -82,6 +82,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
82
82
|
from matrx.cli.bootstrap import run_init
|
|
83
83
|
run_init()
|
|
84
84
|
return 0
|
|
85
|
+
if args.command == "project":
|
|
86
|
+
return _cmd_project(args)
|
|
85
87
|
|
|
86
88
|
parser.print_help()
|
|
87
89
|
return 1
|
|
@@ -124,6 +126,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
124
126
|
gemini = subparsers.add_parser("gemini")
|
|
125
127
|
gemini.add_argument("--route", choices=["direct", "matrx"])
|
|
126
128
|
|
|
129
|
+
project = subparsers.add_parser("project", help="Manage Matrx projects")
|
|
130
|
+
project_sub = project.add_subparsers(dest="project_command")
|
|
131
|
+
project_sub.add_parser("list", help="List projects in your org")
|
|
132
|
+
project_sub.add_parser("current", help="Show active project for this workspace")
|
|
133
|
+
project_switch = project_sub.add_parser("switch", help="Bind this workspace to a project")
|
|
134
|
+
project_switch.add_argument("name", help="Project name, slug, or ID")
|
|
135
|
+
project_create = project_sub.add_parser("create", help="Create a new project")
|
|
136
|
+
project_create.add_argument("name", help="Project name")
|
|
137
|
+
project_create.add_argument("--description", help="Optional description")
|
|
138
|
+
project_sub.add_parser("init", help="Link this git repo to a Matrx project")
|
|
139
|
+
|
|
127
140
|
cursor = subparsers.add_parser("cursor")
|
|
128
141
|
cursor.add_argument("--route", choices=["direct", "matrx"])
|
|
129
142
|
cursor.add_argument("--status", action="store_true", help="Check proxy status")
|
|
@@ -453,10 +466,13 @@ def _cmd_login(args) -> int:
|
|
|
453
466
|
|
|
454
467
|
key = (args.key or "").strip()
|
|
455
468
|
if provider == "matrx":
|
|
469
|
+
url_changed = False
|
|
456
470
|
if args.base_url:
|
|
457
471
|
state["auth"]["matrx"]["base_url"] = args.base_url.strip()
|
|
472
|
+
url_changed = True
|
|
458
473
|
if args.app_url:
|
|
459
474
|
state["auth"]["matrx"]["app_url"] = args.app_url.strip()
|
|
475
|
+
url_changed = True
|
|
460
476
|
elif not state["auth"]["matrx"].get("app_url"):
|
|
461
477
|
state["auth"]["matrx"]["app_url"] = ensure_app_url(
|
|
462
478
|
None,
|
|
@@ -471,13 +487,14 @@ def _cmd_login(args) -> int:
|
|
|
471
487
|
except ValueError as exc:
|
|
472
488
|
print(str(exc), file=sys.stderr)
|
|
473
489
|
return 1
|
|
474
|
-
if changed:
|
|
490
|
+
if changed or url_changed:
|
|
475
491
|
path = save_state(state)
|
|
476
492
|
print(f"Saved {args.provider} credentials to {path}")
|
|
477
493
|
print(f"Matrx base URL: {ensure_v1_url(state['auth']['matrx']['base_url'])}")
|
|
478
494
|
return 0
|
|
479
|
-
|
|
480
|
-
|
|
495
|
+
if not url_changed:
|
|
496
|
+
print("Matrx login did not change the current state", file=sys.stderr)
|
|
497
|
+
return 1
|
|
481
498
|
|
|
482
499
|
if not key:
|
|
483
500
|
print("--key is required for this login command", file=sys.stderr)
|
|
@@ -558,16 +575,32 @@ def _restore_cursor_if_needed() -> None:
|
|
|
558
575
|
def _cmd_status() -> int:
|
|
559
576
|
state = load_state()
|
|
560
577
|
auth = state["auth"]
|
|
578
|
+
|
|
579
|
+
# Show active project for this workspace
|
|
580
|
+
workspace_binding = get_workspace_binding(state, cwd=os.environ.get("PWD") or os.getcwd()) or {}
|
|
581
|
+
active_project_id = (
|
|
582
|
+
(os.environ.get("MTRX_PROJECT_ID") or "").strip()
|
|
583
|
+
or (workspace_binding.get("project_id") or "").strip()
|
|
584
|
+
)
|
|
585
|
+
if active_project_id:
|
|
586
|
+
source = "env" if (os.environ.get("MTRX_PROJECT_ID") or "").strip() else "workspace"
|
|
587
|
+
print(f"Active project: {active_project_id} [{source}]")
|
|
588
|
+
else:
|
|
589
|
+
print("Active project: none (run: mtrx project switch <name>)")
|
|
590
|
+
|
|
561
591
|
print("Defaults:")
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
print(f" cursor: {_default_route_label(configured_route(state, 'cursor'))}")
|
|
592
|
+
for tool in ("codex", "claude", "gemini", "cursor"):
|
|
593
|
+
route = configured_route(state, tool) or "not set"
|
|
594
|
+
print(f" {tool}: {route}")
|
|
566
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"
|
|
567
600
|
print(
|
|
568
601
|
" matrx: "
|
|
569
602
|
f"{mask_secret(auth['matrx'].get('key'))} "
|
|
570
|
-
f"({
|
|
603
|
+
f"({effective_base}) [{base_source}]"
|
|
571
604
|
)
|
|
572
605
|
print(f" matrx app: {ensure_app_url(auth['matrx'].get('app_url'), base_url=auth['matrx'].get('base_url'))}")
|
|
573
606
|
print(f" openai: {mask_secret(auth['openai'].get('key'))}")
|
|
@@ -595,6 +628,24 @@ def _cmd_status() -> int:
|
|
|
595
628
|
proxy_running = is_proxy_running()
|
|
596
629
|
print(f" cursor proxy: {'running' if proxy_running else 'not running'}")
|
|
597
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
|
+
|
|
598
649
|
print("Executables:")
|
|
599
650
|
print(f" codex: {find_executable('codex') or 'not found'}")
|
|
600
651
|
print(f" claude: {find_executable('claude') or 'not found'}")
|
|
@@ -800,6 +851,35 @@ def _cmd_doctor() -> int:
|
|
|
800
851
|
else:
|
|
801
852
|
print(f"[warn] {tool} native config not configured")
|
|
802
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
|
+
|
|
803
883
|
return 1 if failures else 0
|
|
804
884
|
|
|
805
885
|
|
|
@@ -976,5 +1056,29 @@ def _default_route_label(route: str | None) -> str:
|
|
|
976
1056
|
return route or "unset"
|
|
977
1057
|
|
|
978
1058
|
|
|
1059
|
+
def _cmd_project(args) -> int:
|
|
1060
|
+
from matrx.cli.project_cmds import cmd_list, cmd_current, cmd_switch, cmd_create, cmd_init
|
|
1061
|
+
|
|
1062
|
+
sub = getattr(args, "project_command", None)
|
|
1063
|
+
if sub == "list":
|
|
1064
|
+
return cmd_list(args)
|
|
1065
|
+
if sub == "current":
|
|
1066
|
+
return cmd_current(args)
|
|
1067
|
+
if sub == "switch":
|
|
1068
|
+
return cmd_switch(args)
|
|
1069
|
+
if sub == "create":
|
|
1070
|
+
return cmd_create(args)
|
|
1071
|
+
if sub == "init":
|
|
1072
|
+
return cmd_init(args)
|
|
1073
|
+
|
|
1074
|
+
print("Usage: mtrx project <list|current|switch|create|init>", file=sys.stderr)
|
|
1075
|
+
print(" list List all projects in your org")
|
|
1076
|
+
print(" current Show active project for this workspace")
|
|
1077
|
+
print(" switch <name> Bind this workspace to a project")
|
|
1078
|
+
print(" create <name> Create a new project")
|
|
1079
|
+
print(" init Link this git repo to a Matrx project")
|
|
1080
|
+
return 1
|
|
1081
|
+
|
|
1082
|
+
|
|
979
1083
|
if __name__ == "__main__":
|
|
980
1084
|
raise SystemExit(main())
|