mtrx-cli 0.1.19 → 0.1.20
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 +85 -20
- package/src/matrx/cli/main.py +50 -4
package/package.json
CHANGED
|
@@ -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],
|
|
@@ -555,19 +586,36 @@ def _build_gemini_env(
|
|
|
555
586
|
if route == "matrx":
|
|
556
587
|
if not mx_key:
|
|
557
588
|
raise ValueError("No Matrx key available. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
|
|
558
|
-
|
|
589
|
+
|
|
559
590
|
# Clear existing Gemini config to force proxy usage
|
|
560
591
|
env.pop("MTRX_KEY", None)
|
|
561
|
-
|
|
592
|
+
|
|
593
|
+
group_id, project_id = _resolve_matrx_context_overrides(state, env)
|
|
594
|
+
session_id = str(uuid.uuid4())
|
|
595
|
+
runtime_agent_id = (
|
|
596
|
+
(orchestration or {}).get("agent_id")
|
|
597
|
+
or _runtime_agent_basename("gemini")[0]
|
|
598
|
+
)
|
|
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
|
+
ctx_params: list[str] = []
|
|
605
|
+
if project_id:
|
|
606
|
+
ctx_params.append(f"mtrx_project={project_id}")
|
|
607
|
+
if session_id:
|
|
608
|
+
ctx_params.append(f"mtrx_session={session_id}")
|
|
609
|
+
if runtime_agent_id:
|
|
610
|
+
ctx_params.append(f"mtrx_agent={runtime_agent_id}")
|
|
611
|
+
if ctx_params:
|
|
612
|
+
proxy_base_with_ctx = f"{proxy_base}?{'&'.join(ctx_params)}"
|
|
613
|
+
|
|
562
614
|
# Set Proxy Config
|
|
563
|
-
env["GOOGLE_GEMINI_BASE_URL"] =
|
|
564
|
-
env["GEMINI_API_ENDPOINT"] =
|
|
615
|
+
env["GOOGLE_GEMINI_BASE_URL"] = proxy_base_with_ctx
|
|
616
|
+
env["GEMINI_API_ENDPOINT"] = proxy_base_with_ctx
|
|
565
617
|
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
|
-
|
|
618
|
+
|
|
571
619
|
return env, matrx_auth_source
|
|
572
620
|
|
|
573
621
|
# Direct route: clear any matrx-managed env vars
|
|
@@ -709,14 +757,14 @@ def _build_orchestration_metadata(
|
|
|
709
757
|
}
|
|
710
758
|
|
|
711
759
|
|
|
712
|
-
def _best_effort_register_cli_agent(orchestration: dict) ->
|
|
760
|
+
def _best_effort_register_cli_agent(orchestration: dict) -> bool:
|
|
713
761
|
if not orchestration:
|
|
714
|
-
return
|
|
762
|
+
return False
|
|
715
763
|
base_url = (orchestration.get("base_url") or "").rstrip("/")
|
|
716
764
|
matrx_key = (orchestration.get("matrx_key") or "").strip()
|
|
717
765
|
agent_id = (orchestration.get("agent_id") or "").strip()
|
|
718
766
|
if not base_url or not matrx_key or not agent_id:
|
|
719
|
-
return
|
|
767
|
+
return False
|
|
720
768
|
|
|
721
769
|
payload = {
|
|
722
770
|
"agent_id": agent_id,
|
|
@@ -743,10 +791,11 @@ def _best_effort_register_cli_agent(orchestration: dict) -> None:
|
|
|
743
791
|
json=payload,
|
|
744
792
|
)
|
|
745
793
|
if response.status_code in {404, 401, 403}:
|
|
746
|
-
return
|
|
794
|
+
return False
|
|
747
795
|
response.raise_for_status()
|
|
796
|
+
return True
|
|
748
797
|
except httpx.HTTPError:
|
|
749
|
-
return
|
|
798
|
+
return False
|
|
750
799
|
|
|
751
800
|
|
|
752
801
|
def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
@@ -773,6 +822,14 @@ def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
|
773
822
|
f"{base_url}/v1/orchestration/agents/{agent_id}/heartbeat",
|
|
774
823
|
headers=headers,
|
|
775
824
|
)
|
|
825
|
+
if response.status_code == 404:
|
|
826
|
+
if not _best_effort_register_cli_agent(orchestration):
|
|
827
|
+
return
|
|
828
|
+
with httpx.Client(timeout=5) as client:
|
|
829
|
+
response = client.patch(
|
|
830
|
+
f"{base_url}/v1/orchestration/agents/{agent_id}/heartbeat",
|
|
831
|
+
headers=headers,
|
|
832
|
+
)
|
|
776
833
|
if response.status_code in {404, 401, 403}:
|
|
777
834
|
return
|
|
778
835
|
except httpx.HTTPError:
|
|
@@ -839,16 +896,18 @@ def _validate_gemini_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
839
896
|
return
|
|
840
897
|
|
|
841
898
|
expected_base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
842
|
-
|
|
899
|
+
|
|
843
900
|
base_url = (plan.env.get("GOOGLE_GEMINI_BASE_URL") or "").strip()
|
|
844
901
|
if not base_url:
|
|
845
|
-
|
|
846
|
-
base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
|
|
902
|
+
base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
|
|
847
903
|
|
|
848
904
|
if not base_url:
|
|
849
905
|
raise ValueError("Gemini Matrx route is missing GOOGLE_GEMINI_BASE_URL or GEMINI_API_ENDPOINT")
|
|
850
|
-
|
|
851
|
-
|
|
906
|
+
|
|
907
|
+
# Strip query params before comparing (project_id context may be appended as ?mtrx_project=...)
|
|
908
|
+
base_url_no_qs = base_url.split("?")[0].rstrip("/")
|
|
909
|
+
expected_no_qs = expected_base_url.rstrip("/")
|
|
910
|
+
if base_url_no_qs != expected_no_qs:
|
|
852
911
|
raise ValueError(
|
|
853
912
|
"Gemini Matrx route must use the Matrx /v1 base URL. "
|
|
854
913
|
f"Got: {base_url}"
|
|
@@ -921,13 +980,19 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
|
|
|
921
980
|
|
|
922
981
|
if plan.tool == "gemini":
|
|
923
982
|
base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
924
|
-
|
|
983
|
+
_, project_id = _resolve_matrx_context_overrides(state, dict(plan.env))
|
|
984
|
+
lines = [
|
|
925
985
|
"Launching gemini via Matrx",
|
|
926
986
|
f" base_url: {base_url}",
|
|
927
987
|
f" auth_source: {plan.auth_source}",
|
|
928
988
|
" runtime_route: env injection",
|
|
929
989
|
" persistent_route: disabled",
|
|
930
990
|
]
|
|
991
|
+
if project_id:
|
|
992
|
+
lines.append(f" project_id: {project_id}")
|
|
993
|
+
else:
|
|
994
|
+
lines.append(" project_id: (from API key scope)")
|
|
995
|
+
return lines
|
|
931
996
|
|
|
932
997
|
return []
|
|
933
998
|
|
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")
|
|
@@ -558,11 +571,20 @@ def _restore_cursor_if_needed() -> None:
|
|
|
558
571
|
def _cmd_status() -> int:
|
|
559
572
|
state = load_state()
|
|
560
573
|
auth = state["auth"]
|
|
574
|
+
|
|
575
|
+
# Show active project for this workspace
|
|
576
|
+
workspace_binding = get_workspace_binding(state, cwd=os.environ.get("PWD") or os.getcwd()) or {}
|
|
577
|
+
active_project_id = (
|
|
578
|
+
(os.environ.get("MTRX_PROJECT_ID") or "").strip()
|
|
579
|
+
or (workspace_binding.get("project_id") or "").strip()
|
|
580
|
+
)
|
|
581
|
+
if active_project_id:
|
|
582
|
+
source = "env" if (os.environ.get("MTRX_PROJECT_ID") or "").strip() else "workspace"
|
|
583
|
+
print(f"Active project: {active_project_id} [{source}]")
|
|
584
|
+
else:
|
|
585
|
+
print("Active project: none (run: mtrx project switch <name>)")
|
|
586
|
+
|
|
561
587
|
print("Defaults:")
|
|
562
|
-
print(f" codex: {_default_route_label(configured_route(state, 'codex'))}")
|
|
563
|
-
print(f" claude: {_default_route_label(configured_route(state, 'claude'))}")
|
|
564
|
-
print(f" gemini: {_default_route_label(configured_route(state, 'gemini'))}")
|
|
565
|
-
print(f" cursor: {_default_route_label(configured_route(state, 'cursor'))}")
|
|
566
588
|
print("Auth:")
|
|
567
589
|
print(
|
|
568
590
|
" matrx: "
|
|
@@ -976,5 +998,29 @@ def _default_route_label(route: str | None) -> str:
|
|
|
976
998
|
return route or "unset"
|
|
977
999
|
|
|
978
1000
|
|
|
1001
|
+
def _cmd_project(args) -> int:
|
|
1002
|
+
from matrx.cli.project_cmds import cmd_list, cmd_current, cmd_switch, cmd_create, cmd_init
|
|
1003
|
+
|
|
1004
|
+
sub = getattr(args, "project_command", None)
|
|
1005
|
+
if sub == "list":
|
|
1006
|
+
return cmd_list(args)
|
|
1007
|
+
if sub == "current":
|
|
1008
|
+
return cmd_current(args)
|
|
1009
|
+
if sub == "switch":
|
|
1010
|
+
return cmd_switch(args)
|
|
1011
|
+
if sub == "create":
|
|
1012
|
+
return cmd_create(args)
|
|
1013
|
+
if sub == "init":
|
|
1014
|
+
return cmd_init(args)
|
|
1015
|
+
|
|
1016
|
+
print("Usage: mtrx project <list|current|switch|create|init>", file=sys.stderr)
|
|
1017
|
+
print(" list List all projects in your org")
|
|
1018
|
+
print(" current Show active project for this workspace")
|
|
1019
|
+
print(" switch <name> Bind this workspace to a project")
|
|
1020
|
+
print(" create <name> Create a new project")
|
|
1021
|
+
print(" init Link this git repo to a Matrx project")
|
|
1022
|
+
return 1
|
|
1023
|
+
|
|
1024
|
+
|
|
979
1025
|
if __name__ == "__main__":
|
|
980
1026
|
raise SystemExit(main())
|