mtrx-cli 0.1.18 → 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/__init__.py +1 -1
- package/src/matrx/cli/launcher.py +92 -24
- package/src/matrx/cli/main.py +55 -4
package/package.json
CHANGED
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.19"
|
|
@@ -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],
|
|
@@ -475,13 +506,16 @@ def _build_codex_env(
|
|
|
475
506
|
proxy_base = ensure_v1_url(matrx.get("base_url"))
|
|
476
507
|
mx_key, matrx_auth_source = _resolve_matrx_route_key(state, env)
|
|
477
508
|
direct_key = (openai.get("key") or "").strip()
|
|
509
|
+
env_openai_key = (env.get("OPENAI_API_KEY") or "").strip()
|
|
478
510
|
|
|
479
511
|
if route == "matrx":
|
|
480
512
|
if not mx_key:
|
|
481
513
|
raise ValueError("No Matrx key available. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
|
|
482
|
-
|
|
483
|
-
if not
|
|
484
|
-
raise ValueError(
|
|
514
|
+
provider_bearer = env_openai_key or direct_key or read_codex_access_token()
|
|
515
|
+
if not provider_bearer:
|
|
516
|
+
raise ValueError(
|
|
517
|
+
"Codex login required. Run: codex login or configure an OpenAI API key."
|
|
518
|
+
)
|
|
485
519
|
for key in MATRX_ENV_KEYS:
|
|
486
520
|
env.pop(key, None)
|
|
487
521
|
env_snap = _capture_env_snapshot()
|
|
@@ -493,7 +527,7 @@ def _build_codex_env(
|
|
|
493
527
|
or _runtime_agent_basename("codex")[0]
|
|
494
528
|
)
|
|
495
529
|
header_parts = [
|
|
496
|
-
f'"Authorization" = "Bearer {
|
|
530
|
+
f'"Authorization" = "Bearer {provider_bearer}"',
|
|
497
531
|
f'"X-Matrx-Key" = "{mx_key}"',
|
|
498
532
|
f'"X-Matrx-Agent-Id" = "{runtime_agent_id}"',
|
|
499
533
|
'"X-Matrx-Provider" = "codex"',
|
|
@@ -552,19 +586,36 @@ def _build_gemini_env(
|
|
|
552
586
|
if route == "matrx":
|
|
553
587
|
if not mx_key:
|
|
554
588
|
raise ValueError("No Matrx key available. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
|
|
555
|
-
|
|
589
|
+
|
|
556
590
|
# Clear existing Gemini config to force proxy usage
|
|
557
591
|
env.pop("MTRX_KEY", None)
|
|
558
|
-
|
|
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
|
+
|
|
559
614
|
# Set Proxy Config
|
|
560
|
-
env["GOOGLE_GEMINI_BASE_URL"] =
|
|
561
|
-
env["GEMINI_API_ENDPOINT"] =
|
|
615
|
+
env["GOOGLE_GEMINI_BASE_URL"] = proxy_base_with_ctx
|
|
616
|
+
env["GEMINI_API_ENDPOINT"] = proxy_base_with_ctx
|
|
562
617
|
env["GOOGLE_API_KEY"] = mx_key
|
|
563
|
-
|
|
564
|
-
# Matrx-specific headers (if supported by the tool, or for our own tracking)
|
|
565
|
-
# Note: Standard Gemini CLI might not support custom headers via env vars easily.
|
|
566
|
-
# We rely on the Base URL routing to Matrx proxy which handles the logic.
|
|
567
|
-
|
|
618
|
+
|
|
568
619
|
return env, matrx_auth_source
|
|
569
620
|
|
|
570
621
|
# Direct route: clear any matrx-managed env vars
|
|
@@ -706,14 +757,14 @@ def _build_orchestration_metadata(
|
|
|
706
757
|
}
|
|
707
758
|
|
|
708
759
|
|
|
709
|
-
def _best_effort_register_cli_agent(orchestration: dict) ->
|
|
760
|
+
def _best_effort_register_cli_agent(orchestration: dict) -> bool:
|
|
710
761
|
if not orchestration:
|
|
711
|
-
return
|
|
762
|
+
return False
|
|
712
763
|
base_url = (orchestration.get("base_url") or "").rstrip("/")
|
|
713
764
|
matrx_key = (orchestration.get("matrx_key") or "").strip()
|
|
714
765
|
agent_id = (orchestration.get("agent_id") or "").strip()
|
|
715
766
|
if not base_url or not matrx_key or not agent_id:
|
|
716
|
-
return
|
|
767
|
+
return False
|
|
717
768
|
|
|
718
769
|
payload = {
|
|
719
770
|
"agent_id": agent_id,
|
|
@@ -740,10 +791,11 @@ def _best_effort_register_cli_agent(orchestration: dict) -> None:
|
|
|
740
791
|
json=payload,
|
|
741
792
|
)
|
|
742
793
|
if response.status_code in {404, 401, 403}:
|
|
743
|
-
return
|
|
794
|
+
return False
|
|
744
795
|
response.raise_for_status()
|
|
796
|
+
return True
|
|
745
797
|
except httpx.HTTPError:
|
|
746
|
-
return
|
|
798
|
+
return False
|
|
747
799
|
|
|
748
800
|
|
|
749
801
|
def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
@@ -770,6 +822,14 @@ def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
|
770
822
|
f"{base_url}/v1/orchestration/agents/{agent_id}/heartbeat",
|
|
771
823
|
headers=headers,
|
|
772
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
|
+
)
|
|
773
833
|
if response.status_code in {404, 401, 403}:
|
|
774
834
|
return
|
|
775
835
|
except httpx.HTTPError:
|
|
@@ -836,16 +896,18 @@ def _validate_gemini_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
836
896
|
return
|
|
837
897
|
|
|
838
898
|
expected_base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
839
|
-
|
|
899
|
+
|
|
840
900
|
base_url = (plan.env.get("GOOGLE_GEMINI_BASE_URL") or "").strip()
|
|
841
901
|
if not base_url:
|
|
842
|
-
|
|
843
|
-
base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
|
|
902
|
+
base_url = (plan.env.get("GEMINI_API_ENDPOINT") or "").strip()
|
|
844
903
|
|
|
845
904
|
if not base_url:
|
|
846
905
|
raise ValueError("Gemini Matrx route is missing GOOGLE_GEMINI_BASE_URL or GEMINI_API_ENDPOINT")
|
|
847
|
-
|
|
848
|
-
|
|
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:
|
|
849
911
|
raise ValueError(
|
|
850
912
|
"Gemini Matrx route must use the Matrx /v1 base URL. "
|
|
851
913
|
f"Got: {base_url}"
|
|
@@ -918,13 +980,19 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
|
|
|
918
980
|
|
|
919
981
|
if plan.tool == "gemini":
|
|
920
982
|
base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
921
|
-
|
|
983
|
+
_, project_id = _resolve_matrx_context_overrides(state, dict(plan.env))
|
|
984
|
+
lines = [
|
|
922
985
|
"Launching gemini via Matrx",
|
|
923
986
|
f" base_url: {base_url}",
|
|
924
987
|
f" auth_source: {plan.auth_source}",
|
|
925
988
|
" runtime_route: env injection",
|
|
926
989
|
" persistent_route: disabled",
|
|
927
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
|
|
928
996
|
|
|
929
997
|
return []
|
|
930
998
|
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -78,6 +78,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
78
78
|
return _cmd_launch(args.command, args.route, remainder)
|
|
79
79
|
if args.command == "cursor":
|
|
80
80
|
return _cmd_cursor(args)
|
|
81
|
+
if args.command == "init":
|
|
82
|
+
from matrx.cli.bootstrap import run_init
|
|
83
|
+
run_init()
|
|
84
|
+
return 0
|
|
85
|
+
if args.command == "project":
|
|
86
|
+
return _cmd_project(args)
|
|
81
87
|
|
|
82
88
|
parser.print_help()
|
|
83
89
|
return 1
|
|
@@ -104,6 +110,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
104
110
|
subparsers.add_parser("version")
|
|
105
111
|
subparsers.add_parser("status")
|
|
106
112
|
subparsers.add_parser("doctor")
|
|
113
|
+
subparsers.add_parser("init", help="Initialize Matrx for an existing project (seeds system registry)")
|
|
107
114
|
|
|
108
115
|
personal = subparsers.add_parser("personal")
|
|
109
116
|
personal_subparsers = personal.add_subparsers(dest="personal_command")
|
|
@@ -119,6 +126,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
119
126
|
gemini = subparsers.add_parser("gemini")
|
|
120
127
|
gemini.add_argument("--route", choices=["direct", "matrx"])
|
|
121
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
|
+
|
|
122
140
|
cursor = subparsers.add_parser("cursor")
|
|
123
141
|
cursor.add_argument("--route", choices=["direct", "matrx"])
|
|
124
142
|
cursor.add_argument("--status", action="store_true", help="Check proxy status")
|
|
@@ -553,11 +571,20 @@ def _restore_cursor_if_needed() -> None:
|
|
|
553
571
|
def _cmd_status() -> int:
|
|
554
572
|
state = load_state()
|
|
555
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
|
+
|
|
556
587
|
print("Defaults:")
|
|
557
|
-
print(f" codex: {_default_route_label(configured_route(state, 'codex'))}")
|
|
558
|
-
print(f" claude: {_default_route_label(configured_route(state, 'claude'))}")
|
|
559
|
-
print(f" gemini: {_default_route_label(configured_route(state, 'gemini'))}")
|
|
560
|
-
print(f" cursor: {_default_route_label(configured_route(state, 'cursor'))}")
|
|
561
588
|
print("Auth:")
|
|
562
589
|
print(
|
|
563
590
|
" matrx: "
|
|
@@ -971,5 +998,29 @@ def _default_route_label(route: str | None) -> str:
|
|
|
971
998
|
return route or "unset"
|
|
972
999
|
|
|
973
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
|
+
|
|
974
1025
|
if __name__ == "__main__":
|
|
975
1026
|
raise SystemExit(main())
|