mtrx-cli 0.1.25 → 0.1.27
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/cursor_ca.py +257 -39
- package/src/matrx/cli/cursor_config.py +14 -1
- package/src/matrx/cli/cursor_daemon.py +4 -0
- package/src/matrx/cli/cursor_launcher.py +3 -1
- package/src/matrx/cli/cursor_proxy.py +412 -166
- package/src/matrx/cli/cursor_reroute.py +376 -17
- package/src/matrx/cli/launcher.py +47 -1
- package/src/matrx/cli/main.py +384 -59
- package/src/matrx/cli/state.py +21 -0
package/src/matrx/cli/main.py
CHANGED
|
@@ -84,6 +84,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
84
84
|
return 0
|
|
85
85
|
if args.command == "project":
|
|
86
86
|
return _cmd_project(args)
|
|
87
|
+
if args.command == "agents":
|
|
88
|
+
return _cmd_agents(args)
|
|
87
89
|
|
|
88
90
|
parser.print_help()
|
|
89
91
|
return 1
|
|
@@ -147,6 +149,53 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
147
149
|
help="Launch Cursor after applying the current Matrx settings",
|
|
148
150
|
)
|
|
149
151
|
|
|
152
|
+
# ── mtrx agents ──────────────────────────────────────────────────────────
|
|
153
|
+
agents = subparsers.add_parser("agents", help="Real-time multi-agent coordination")
|
|
154
|
+
agents_sub = agents.add_subparsers(dest="agents_command")
|
|
155
|
+
|
|
156
|
+
agents_list = agents_sub.add_parser("list", help="Show agents active in the group")
|
|
157
|
+
agents_list.add_argument("--group", metavar="GROUP_ID", help="Group to query")
|
|
158
|
+
agents_list.add_argument(
|
|
159
|
+
"--history",
|
|
160
|
+
action="store_true",
|
|
161
|
+
help="Also include recent completed activities",
|
|
162
|
+
)
|
|
163
|
+
agents_list.add_argument(
|
|
164
|
+
"--limit",
|
|
165
|
+
type=int,
|
|
166
|
+
default=10,
|
|
167
|
+
help="Max history entries to return (default: 10)",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
agents_push = agents_sub.add_parser(
|
|
171
|
+
"push", help="Push a task into another agent's inbox"
|
|
172
|
+
)
|
|
173
|
+
agents_push.add_argument("target", help="Target agent ID")
|
|
174
|
+
agents_push.add_argument(
|
|
175
|
+
"prompt_parts",
|
|
176
|
+
nargs="+",
|
|
177
|
+
metavar="WORD",
|
|
178
|
+
help="Task prompt (all remaining words)",
|
|
179
|
+
)
|
|
180
|
+
agents_push.add_argument("--group", metavar="GROUP_ID", help="Override group")
|
|
181
|
+
agents_push.add_argument(
|
|
182
|
+
"--from-agent",
|
|
183
|
+
dest="from_agent",
|
|
184
|
+
metavar="AGENT_ID",
|
|
185
|
+
help="Sender identity (defaults to 'cli')",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
agents_watch = agents_sub.add_parser(
|
|
189
|
+
"watch", help="Stream live group events to the terminal"
|
|
190
|
+
)
|
|
191
|
+
agents_watch.add_argument("--group", metavar="GROUP_ID", help="Group to watch")
|
|
192
|
+
|
|
193
|
+
agents_status = agents_sub.add_parser(
|
|
194
|
+
"status", help="Show this workspace's group/agent binding"
|
|
195
|
+
)
|
|
196
|
+
agents_status.add_argument("--group", metavar="GROUP_ID", help="Override group")
|
|
197
|
+
agents_status.add_argument("--agent", metavar="AGENT_ID", help="Override agent")
|
|
198
|
+
|
|
150
199
|
return parser
|
|
151
200
|
|
|
152
201
|
|
|
@@ -367,8 +416,115 @@ def _run_matrx_browser_login(
|
|
|
367
416
|
return result
|
|
368
417
|
|
|
369
418
|
|
|
419
|
+
def _matrx_skip_remote_key_verify() -> bool:
|
|
420
|
+
return (os.environ.get("MTRX_SKIP_KEY_VERIFY") or "").strip().lower() in {
|
|
421
|
+
"1",
|
|
422
|
+
"true",
|
|
423
|
+
"yes",
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _verify_matrx_key_with_server(state: dict, key: str) -> str:
|
|
428
|
+
"""
|
|
429
|
+
Ping the configured Matrx API with the key.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
"valid" — server accepted the key (HTTP 200).
|
|
433
|
+
"invalid" — 401/403 (revoked, wrong deployment, etc.).
|
|
434
|
+
"unreachable" — network error or unexpected status (caller may proceed with a warning).
|
|
435
|
+
"""
|
|
436
|
+
if _matrx_skip_remote_key_verify():
|
|
437
|
+
return "valid"
|
|
438
|
+
if not key.startswith("mx_"):
|
|
439
|
+
return "valid"
|
|
440
|
+
|
|
441
|
+
base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
442
|
+
url = f"{base_url.rstrip('/')}/v1/auth/context"
|
|
443
|
+
try:
|
|
444
|
+
with httpx.Client(timeout=12) as client:
|
|
445
|
+
resp = client.get(url, headers={"X-Matrx-Key": key})
|
|
446
|
+
except httpx.HTTPError as exc:
|
|
447
|
+
print(
|
|
448
|
+
f"[warn] Could not reach Matrx at {base_url} to verify your API key ({exc}). "
|
|
449
|
+
"Continuing; if requests fail with 403, run: mtrx login matrx --web",
|
|
450
|
+
file=sys.stderr,
|
|
451
|
+
)
|
|
452
|
+
return "unreachable"
|
|
453
|
+
|
|
454
|
+
if resp.status_code == 200:
|
|
455
|
+
return "valid"
|
|
456
|
+
if resp.status_code in (401, 403):
|
|
457
|
+
return "invalid"
|
|
458
|
+
|
|
459
|
+
print(
|
|
460
|
+
f"[warn] Matrx key check returned HTTP {resp.status_code}; continuing. "
|
|
461
|
+
"If tools fail, run: mtrx login matrx --web",
|
|
462
|
+
file=sys.stderr,
|
|
463
|
+
)
|
|
464
|
+
return "unreachable"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _handle_rejected_matrx_key(state: dict, key: str) -> None:
|
|
468
|
+
"""
|
|
469
|
+
React to a server-rejected key. Clears saved config when appropriate, or raises
|
|
470
|
+
with instructions when the key comes from MTRX_KEY / workspace binding.
|
|
471
|
+
"""
|
|
472
|
+
auth_cfg = state.setdefault("auth", {}).setdefault("matrx", {})
|
|
473
|
+
env_key = normalize_matrx_key(os.environ.get("MTRX_KEY"))
|
|
474
|
+
workspace_binding = get_workspace_binding(
|
|
475
|
+
state, cwd=os.environ.get("PWD") or os.getcwd()
|
|
476
|
+
) or {}
|
|
477
|
+
ws_key = normalize_matrx_key(workspace_binding.get("matrx_key"))
|
|
478
|
+
saved = normalize_matrx_key(auth_cfg.get("key"))
|
|
479
|
+
|
|
480
|
+
print(
|
|
481
|
+
"Matrx rejected this API key (wrong server, revoked, expired, or not in this deployment).",
|
|
482
|
+
file=sys.stderr,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if env_key == key:
|
|
486
|
+
raise ValueError(
|
|
487
|
+
"MTRX_KEY in your environment is not valid for this Matrx server. "
|
|
488
|
+
"Unset it or set a fresh key, then run:\n"
|
|
489
|
+
" mtrx login matrx --web\n"
|
|
490
|
+
" # or: mtrx login matrx --key mx_..."
|
|
491
|
+
)
|
|
492
|
+
if ws_key == key:
|
|
493
|
+
raise ValueError(
|
|
494
|
+
"The Matrx key stored for this workspace is invalid for the configured server. "
|
|
495
|
+
"Run:\n"
|
|
496
|
+
" mtrx project switch <name>\n"
|
|
497
|
+
"after signing in with: mtrx login matrx --web"
|
|
498
|
+
)
|
|
499
|
+
if saved == key:
|
|
500
|
+
auth_cfg["key"] = None
|
|
501
|
+
print(
|
|
502
|
+
"Cleared the saved Matrx key from ~/.config/mtrx (or MTRX_CONFIG_DIR).",
|
|
503
|
+
file=sys.stderr,
|
|
504
|
+
)
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
raise ValueError(
|
|
508
|
+
"Matrx API key was rejected. Refresh it with:\n"
|
|
509
|
+
" mtrx login matrx --web\n"
|
|
510
|
+
" # or: mtrx login matrx --key mx_..."
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
370
514
|
def _complete_matrx_login(state: dict, *, force: bool = False) -> tuple[dict, bool]:
|
|
371
515
|
auth_cfg = state.setdefault("auth", {}).setdefault("matrx", {})
|
|
516
|
+
if not force and _has_matrx_login(state, env=os.environ):
|
|
517
|
+
key = _resolved_matrx_key(state, os.environ)
|
|
518
|
+
if key.startswith("mx_"):
|
|
519
|
+
outcome = _verify_matrx_key_with_server(state, key)
|
|
520
|
+
if outcome == "invalid":
|
|
521
|
+
_handle_rejected_matrx_key(state, key)
|
|
522
|
+
elif outcome == "valid":
|
|
523
|
+
return state, False
|
|
524
|
+
# unreachable: already warned; treat as OK for offline / flaky networks
|
|
525
|
+
else:
|
|
526
|
+
return state, False
|
|
527
|
+
|
|
372
528
|
if not force and _has_matrx_login(state, env=os.environ):
|
|
373
529
|
return state, False
|
|
374
530
|
if not _is_interactive_terminal():
|
|
@@ -428,14 +584,20 @@ def _maybe_promote_direct_route(state: dict, tool: str, route: str | None) -> tu
|
|
|
428
584
|
return route, False
|
|
429
585
|
if configured_route(state, tool) != "direct":
|
|
430
586
|
return route, False
|
|
431
|
-
if
|
|
587
|
+
if state.get("explicit_direct_route", {}).get(tool):
|
|
432
588
|
return route, False
|
|
589
|
+
# Matrx credentials present: align saved default with `mtrx <tool>` (same as `mtrx use <tool> matrx`).
|
|
590
|
+
if _has_matrx_login(state, env=os.environ):
|
|
591
|
+
state.setdefault("defaults", {})[tool] = "matrx"
|
|
592
|
+
state.setdefault("explicit_direct_route", {})[tool] = False
|
|
593
|
+
return "matrx", True
|
|
433
594
|
if not _is_interactive_terminal():
|
|
434
595
|
return route, False
|
|
435
596
|
|
|
436
597
|
print(f"`mtrx {tool}` is currently configured to use the direct route.")
|
|
437
598
|
if _prompt_yes_no(f"Switch {tool} to the Matrx route and continue with Matrx sign-in?", default=True):
|
|
438
599
|
state.setdefault("defaults", {})[tool] = "matrx"
|
|
600
|
+
state.setdefault("explicit_direct_route", {})[tool] = False
|
|
439
601
|
return "matrx", True
|
|
440
602
|
return route, False
|
|
441
603
|
|
|
@@ -514,10 +676,19 @@ def _cmd_login(args) -> int:
|
|
|
514
676
|
|
|
515
677
|
def _cmd_use(args) -> int:
|
|
516
678
|
state = load_state()
|
|
517
|
-
|
|
518
|
-
|
|
679
|
+
try:
|
|
680
|
+
state, login_changed = _complete_matrx_login(state)
|
|
681
|
+
except ValueError as exc:
|
|
682
|
+
print(str(exc), file=sys.stderr)
|
|
519
683
|
return 1
|
|
684
|
+
if login_changed:
|
|
685
|
+
save_state(state)
|
|
520
686
|
state["defaults"][args.tool] = args.route
|
|
687
|
+
explicit = state.setdefault("explicit_direct_route", {})
|
|
688
|
+
if args.route == "direct":
|
|
689
|
+
explicit[args.tool] = True
|
|
690
|
+
else:
|
|
691
|
+
explicit[args.tool] = False
|
|
521
692
|
path = save_state(state)
|
|
522
693
|
print(f"Default route for {args.tool}: {args.route}")
|
|
523
694
|
print(f"Saved to {path}")
|
|
@@ -716,9 +887,16 @@ def _resolve_personal_policy_target(state: dict, *, key: str) -> tuple[dict, dic
|
|
|
716
887
|
|
|
717
888
|
def _cmd_personal_optimize(args) -> int:
|
|
718
889
|
state = load_state()
|
|
890
|
+
try:
|
|
891
|
+
state, login_changed = _complete_matrx_login(state)
|
|
892
|
+
except ValueError as exc:
|
|
893
|
+
print(str(exc), file=sys.stderr)
|
|
894
|
+
return 1
|
|
895
|
+
if login_changed:
|
|
896
|
+
save_state(state)
|
|
719
897
|
key = _personal_matrx_key(state)
|
|
720
898
|
if not key:
|
|
721
|
-
print("
|
|
899
|
+
print("No Matrx key available. Run: mtrx login matrx --key mx_...", file=sys.stderr)
|
|
722
900
|
return 1
|
|
723
901
|
|
|
724
902
|
try:
|
|
@@ -909,6 +1087,11 @@ def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
|
909
1087
|
)
|
|
910
1088
|
if initialized or changed or auth_changed or promoted:
|
|
911
1089
|
save_state(state)
|
|
1090
|
+
if promoted:
|
|
1091
|
+
print(
|
|
1092
|
+
f"Default route for {tool} was direct — updated to matrx. "
|
|
1093
|
+
f"Use `mtrx use {tool} direct` if you want to stay on the direct route.",
|
|
1094
|
+
)
|
|
912
1095
|
if initialized:
|
|
913
1096
|
print(
|
|
914
1097
|
f"First-time setup: default route for {tool} set to matrx. "
|
|
@@ -931,33 +1114,60 @@ def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
|
931
1114
|
|
|
932
1115
|
|
|
933
1116
|
def _cmd_cursor(args) -> int:
|
|
1117
|
+
import json as _json
|
|
1118
|
+
import subprocess
|
|
1119
|
+
import time
|
|
1120
|
+
|
|
1121
|
+
from matrx.cli.cursor_ca import (
|
|
1122
|
+
ca_cert_path,
|
|
1123
|
+
ca_exists,
|
|
1124
|
+
generate_ca,
|
|
1125
|
+
is_ca_trusted,
|
|
1126
|
+
trust_ca_system,
|
|
1127
|
+
windows_trust_status,
|
|
1128
|
+
)
|
|
934
1129
|
from matrx.cli.cursor_hooks import install_mtrx_hooks, is_mtrx_hooks_installed
|
|
935
|
-
from matrx.cli.
|
|
936
|
-
from matrx.cli.
|
|
1130
|
+
from matrx.cli.cursor_launcher import find_cursor_executable, launch_cursor_with_proxy
|
|
1131
|
+
from matrx.cli.cursor_proxy import DEFAULT_PORT, PROXY_HOST
|
|
1132
|
+
from matrx.cli.cursor_service import get_proxy_status, install_service, is_proxy_running, uninstall_service
|
|
937
1133
|
|
|
938
1134
|
route = args.route
|
|
939
1135
|
|
|
940
|
-
# --status
|
|
1136
|
+
# --status
|
|
941
1137
|
if args.status:
|
|
942
|
-
|
|
1138
|
+
proxy_prev_path = config_dir() / "cursor-proxy-previous-settings.json"
|
|
943
1139
|
hooks_installed = is_mtrx_hooks_installed()
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
legacy_proxy_prev_path = config_dir() / "cursor-proxy-previous-settings.json"
|
|
947
|
-
configured = prev_path.exists()
|
|
948
|
-
legacy_proxy_active = is_proxy_running() or legacy_proxy_prev_path.exists()
|
|
1140
|
+
proxy_running = is_proxy_running()
|
|
1141
|
+
trust_status = windows_trust_status() if os.name == "nt" and ca_exists() else None
|
|
949
1142
|
print("MTRX Cursor integration:")
|
|
950
|
-
print(f" mode: {'
|
|
1143
|
+
print(f" mode: {'MITM proxy (all models)' if proxy_prev_path.exists() else 'not configured'}")
|
|
1144
|
+
if proxy_running:
|
|
1145
|
+
status = get_proxy_status()
|
|
1146
|
+
req_count = status.get("requests", "?") if status else "?"
|
|
1147
|
+
print(f" proxy: running ({PROXY_HOST}:{DEFAULT_PORT}, {req_count} requests)")
|
|
1148
|
+
else:
|
|
1149
|
+
print(f" proxy: not running")
|
|
1150
|
+
print(f" ca cert: {'generated' if ca_exists() else 'not generated'} — {ca_cert_path()}")
|
|
1151
|
+
if trust_status is not None:
|
|
1152
|
+
print(
|
|
1153
|
+
" trust: "
|
|
1154
|
+
f"user_root={'yes' if trust_status['user_root'] else 'no'}, "
|
|
1155
|
+
f"machine_root={'yes' if trust_status['machine_root'] else 'no'}, "
|
|
1156
|
+
f"fully_trusted={'yes' if trust_status['fully_trusted'] else 'no'}"
|
|
1157
|
+
)
|
|
1158
|
+
elif ca_exists():
|
|
1159
|
+
print(f" trust: {'trusted' if is_ca_trusted() else 'not trusted'}")
|
|
1160
|
+
print(f" proxy settings: {'written' if proxy_prev_path.exists() else 'not written'}")
|
|
1161
|
+
print(" launch env: `mtrx cursor --launch` sets NODE_EXTRA_CA_CERTS for the launched Cursor process")
|
|
951
1162
|
print(f" hooks: {'active (sessionEnd, stop → telemetry)' if hooks_installed else 'not installed'}")
|
|
952
|
-
print(f" legacy MITM proxy: {'active' if legacy_proxy_active else 'not active'}")
|
|
953
|
-
if configured:
|
|
954
|
-
print(f" matrx: {base_url}")
|
|
955
1163
|
return 0
|
|
956
1164
|
|
|
957
|
-
# --stop
|
|
1165
|
+
# --stop
|
|
958
1166
|
if args.stop:
|
|
959
1167
|
_restore_cursor_if_needed()
|
|
960
|
-
print("Cursor
|
|
1168
|
+
print("MTRX Cursor proxy stopped.")
|
|
1169
|
+
if cursor_is_running():
|
|
1170
|
+
print(" Restart Cursor for settings to take effect.")
|
|
961
1171
|
return 0
|
|
962
1172
|
|
|
963
1173
|
state = load_state()
|
|
@@ -971,7 +1181,7 @@ def _cmd_cursor(args) -> int:
|
|
|
971
1181
|
print(" Restart Cursor for settings to take effect.")
|
|
972
1182
|
return 0
|
|
973
1183
|
|
|
974
|
-
# --- matrx route:
|
|
1184
|
+
# --- matrx route: MITM proxy (intercepts all Cursor traffic, any model) ---
|
|
975
1185
|
|
|
976
1186
|
try:
|
|
977
1187
|
state, login_changed = _complete_matrx_login(state)
|
|
@@ -995,70 +1205,135 @@ def _cmd_cursor(args) -> int:
|
|
|
995
1205
|
matrx_base_url = ensure_root_url(
|
|
996
1206
|
state.get("auth", {}).get("matrx", {}).get("base_url")
|
|
997
1207
|
)
|
|
998
|
-
matrx_proxy_url = ensure_v1_url(matrx_base_url)
|
|
999
1208
|
|
|
1000
1209
|
if initialized or login_changed or promoted:
|
|
1001
1210
|
save_state(state)
|
|
1211
|
+
if promoted:
|
|
1212
|
+
print(
|
|
1213
|
+
"Default route for cursor was direct — updated to matrx. "
|
|
1214
|
+
"Use `mtrx use cursor direct` if you want to stay on the direct route.",
|
|
1215
|
+
)
|
|
1002
1216
|
if initialized:
|
|
1003
1217
|
print(
|
|
1004
1218
|
"First-time setup: default route for cursor set to matrx. "
|
|
1005
1219
|
"Use `mtrx use cursor direct` to opt out.",
|
|
1006
1220
|
)
|
|
1007
1221
|
|
|
1008
|
-
#
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1222
|
+
# 1. Generate CA cert if not present
|
|
1223
|
+
if not ca_exists():
|
|
1224
|
+
generate_ca()
|
|
1225
|
+
print("MTRX CA certificate generated.")
|
|
1226
|
+
cert_path = ca_cert_path()
|
|
1227
|
+
|
|
1228
|
+
# 2. Attempt system-wide CA trust (requires elevation on Windows/Mac)
|
|
1229
|
+
trusted_before = is_ca_trusted()
|
|
1230
|
+
trust_ok = trusted_before or trust_ca_system(cert_path)
|
|
1231
|
+
trusted_after = trusted_before or is_ca_trusted()
|
|
1232
|
+
if os.name == "nt":
|
|
1233
|
+
trust_status = windows_trust_status(cert_path)
|
|
1234
|
+
print(
|
|
1235
|
+
"Windows trust status:"
|
|
1236
|
+
f" user_root={'yes' if trust_status['user_root'] else 'no'},"
|
|
1237
|
+
f" machine_root={'yes' if trust_status['machine_root'] else 'no'}"
|
|
1238
|
+
)
|
|
1239
|
+
if not trust_status["user_root"]:
|
|
1240
|
+
print("[warn] CurrentUser Root store is missing the MTRX CA.")
|
|
1241
|
+
print(f" Run without elevation: certutil -user -addstore Root \"{cert_path}\"")
|
|
1242
|
+
if not trust_status["machine_root"]:
|
|
1243
|
+
print("[warn] LocalMachine Root store is missing the MTRX CA.")
|
|
1244
|
+
print(f" Run as Admin: certutil -addstore Root \"{cert_path}\"")
|
|
1245
|
+
if not (trust_ok or trusted_after or trust_status["user_root"]):
|
|
1246
|
+
print("[warn] The MTRX CA is not trusted by a Windows root store yet.")
|
|
1247
|
+
print(" Cursor's native network stack may still reject intercepted TLS connections.")
|
|
1248
|
+
elif trust_status["user_root"] and not trust_status["machine_root"]:
|
|
1249
|
+
print("[warn] Current-user trust is present, but machine-wide trust is still missing.")
|
|
1250
|
+
print(" Some Cursor native network paths may still reject intercepted TLS connections.")
|
|
1251
|
+
print(" `mtrx cursor --launch` sets NODE_EXTRA_CA_CERTS and disables QUIC for the launched Cursor process.")
|
|
1252
|
+
elif not trust_ok and not trusted_after:
|
|
1253
|
+
print("[warn] The MTRX CA is not trusted by the OS certificate store yet.")
|
|
1254
|
+
print(" Cursor's native network stack may still reject intercepted TLS connections.")
|
|
1255
|
+
print(" On Mac:")
|
|
1256
|
+
print(
|
|
1257
|
+
f" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain \"{cert_path}\""
|
|
1258
|
+
)
|
|
1259
|
+
print(" `mtrx cursor --launch` still sets NODE_EXTRA_CA_CERTS for Node-based requests only.")
|
|
1012
1260
|
|
|
1013
|
-
#
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
if previous is not None:
|
|
1017
|
-
prev_path.write_text(__import__("json").dumps(previous), encoding="utf-8")
|
|
1261
|
+
# 3. Start proxy daemon
|
|
1262
|
+
if is_proxy_running():
|
|
1263
|
+
print(f"MTRX proxy already running on {PROXY_HOST}:{DEFAULT_PORT}.")
|
|
1018
1264
|
else:
|
|
1019
|
-
print(
|
|
1020
|
-
|
|
1021
|
-
|
|
1265
|
+
print("Starting MTRX proxy...", end=" ", flush=True)
|
|
1266
|
+
ok = install_service(
|
|
1267
|
+
matrx_key=mx_key,
|
|
1268
|
+
matrx_base_url=matrx_base_url,
|
|
1022
1269
|
)
|
|
1023
|
-
|
|
1270
|
+
if not ok:
|
|
1271
|
+
# Poll a bit longer before giving up
|
|
1272
|
+
for _ in range(10):
|
|
1273
|
+
time.sleep(0.5)
|
|
1274
|
+
if is_proxy_running():
|
|
1275
|
+
ok = True
|
|
1276
|
+
break
|
|
1277
|
+
if ok:
|
|
1278
|
+
print("done.")
|
|
1279
|
+
else:
|
|
1280
|
+
print("failed.")
|
|
1281
|
+
print("[error] Proxy did not start. Check logs:", file=sys.stderr)
|
|
1282
|
+
print(f" {config_dir() / 'logs' / 'cursor-proxy.err.log'}", file=sys.stderr)
|
|
1283
|
+
return 1
|
|
1024
1284
|
|
|
1025
|
-
#
|
|
1285
|
+
# 4. Write http.proxy + NODE_EXTRA_CA_CERTS to Cursor's settings.json (idempotent)
|
|
1286
|
+
proxy_prev_path = config_dir() / "cursor-proxy-previous-settings.json"
|
|
1287
|
+
if not proxy_prev_path.exists():
|
|
1288
|
+
previous = configure_cursor_proxy_settings(
|
|
1289
|
+
proxy_url=f"http://{PROXY_HOST}:{DEFAULT_PORT}",
|
|
1290
|
+
ca_cert_path=str(cert_path),
|
|
1291
|
+
)
|
|
1292
|
+
proxy_prev_path.write_text(_json.dumps(previous), encoding="utf-8")
|
|
1293
|
+
|
|
1294
|
+
# 5. Install hooks for session telemetry
|
|
1026
1295
|
install_mtrx_hooks(mx_key, matrx_base_url)
|
|
1027
1296
|
|
|
1028
|
-
# Optional: launch Cursor
|
|
1297
|
+
# 6. Optional: launch Cursor with NODE_EXTRA_CA_CERTS set on the process
|
|
1029
1298
|
if getattr(args, "launch", False):
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
except Exception:
|
|
1037
|
-
print("[warn] Could not launch Cursor.", file=sys.stderr)
|
|
1299
|
+
launched = launch_cursor_with_proxy(
|
|
1300
|
+
proxy_url=f"http://{PROXY_HOST}:{DEFAULT_PORT}",
|
|
1301
|
+
ca_cert_path=str(cert_path),
|
|
1302
|
+
)
|
|
1303
|
+
if launched:
|
|
1304
|
+
print("Launched Cursor with MTRX proxy environment.")
|
|
1038
1305
|
else:
|
|
1039
|
-
print("[warn] Could not find Cursor executable.", file=sys.stderr)
|
|
1306
|
+
print("[warn] Could not find Cursor executable. Open Cursor manually.", file=sys.stderr)
|
|
1040
1307
|
|
|
1041
1308
|
print()
|
|
1042
|
-
print("Cursor configured for MTRX —
|
|
1043
|
-
print(f"
|
|
1309
|
+
print("Cursor configured for MTRX — all traffic routed through MITM proxy.")
|
|
1310
|
+
print(f" proxy: {PROXY_HOST}:{DEFAULT_PORT}")
|
|
1311
|
+
print(f" ca cert: {cert_path}")
|
|
1044
1312
|
print()
|
|
1045
|
-
print(" Works with
|
|
1046
|
-
|
|
1313
|
+
print(" Works with all Cursor Pro models: Claude, GPT, Gemini, cursor-fast, and more.")
|
|
1314
|
+
if not getattr(args, "launch", False):
|
|
1315
|
+
print(" Restart Cursor for settings to take effect.")
|
|
1316
|
+
print(" Tip: use `mtrx cursor --launch` to open Cursor with the proxy pre-wired.")
|
|
1047
1317
|
print(" Check status: mtrx cursor --status")
|
|
1048
|
-
print(" To disable: mtrx
|
|
1318
|
+
print(" To disable: mtrx cursor --stop")
|
|
1049
1319
|
return 0
|
|
1050
1320
|
|
|
1051
1321
|
|
|
1052
|
-
def
|
|
1322
|
+
def _resolved_matrx_key(state: dict, env: dict[str, str] | None = None) -> str:
|
|
1323
|
+
"""Matrx API key from MTRX_KEY, workspace binding, or saved login (first match)."""
|
|
1053
1324
|
env = env or {}
|
|
1054
1325
|
env_key = normalize_matrx_key(env.get("MTRX_KEY"))
|
|
1055
1326
|
if env_key:
|
|
1056
|
-
return
|
|
1327
|
+
return env_key
|
|
1057
1328
|
workspace_binding = get_workspace_binding(state, cwd=env.get("PWD") or os.getcwd()) or {}
|
|
1058
1329
|
workspace_key = normalize_matrx_key(workspace_binding.get("matrx_key"))
|
|
1059
1330
|
if workspace_key:
|
|
1060
|
-
return
|
|
1061
|
-
return
|
|
1331
|
+
return workspace_key
|
|
1332
|
+
return normalize_matrx_key(state.get("auth", {}).get("matrx", {}).get("key")) or ""
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def _has_matrx_login(state: dict, env: dict[str, str] | None = None) -> bool:
|
|
1336
|
+
return bool(_resolved_matrx_key(state, env))
|
|
1062
1337
|
|
|
1063
1338
|
|
|
1064
1339
|
def _default_route_label(route: str | None) -> str:
|
|
@@ -1069,6 +1344,27 @@ def _cmd_project(args) -> int:
|
|
|
1069
1344
|
from matrx.cli.project_cmds import cmd_list, cmd_current, cmd_switch, cmd_create, cmd_init
|
|
1070
1345
|
|
|
1071
1346
|
sub = getattr(args, "project_command", None)
|
|
1347
|
+
if sub is None:
|
|
1348
|
+
print("Usage: mtrx project <list|current|switch|create|init>", file=sys.stderr)
|
|
1349
|
+
print(" list List all projects in your org")
|
|
1350
|
+
print(" current Show active project for this workspace")
|
|
1351
|
+
print(" switch <name> Bind this workspace to a project")
|
|
1352
|
+
print(" create <name> Create a new project")
|
|
1353
|
+
print(" init Link this git repo to a Matrx project")
|
|
1354
|
+
return 1
|
|
1355
|
+
|
|
1356
|
+
# Ensure Matrx auth before any project subcommand; trigger interactive
|
|
1357
|
+
# login if needed so the user isn't forced to run `mtrx login` separately.
|
|
1358
|
+
state = load_state()
|
|
1359
|
+
try:
|
|
1360
|
+
state, login_changed = _complete_matrx_login(state)
|
|
1361
|
+
except ValueError as exc:
|
|
1362
|
+
print(str(exc), file=sys.stderr)
|
|
1363
|
+
return 1
|
|
1364
|
+
if login_changed:
|
|
1365
|
+
# Persist immediately so sub-commands that call load_state() find the key.
|
|
1366
|
+
save_state(state)
|
|
1367
|
+
|
|
1072
1368
|
if sub == "list":
|
|
1073
1369
|
return cmd_list(args)
|
|
1074
1370
|
if sub == "current":
|
|
@@ -1081,11 +1377,40 @@ def _cmd_project(args) -> int:
|
|
|
1081
1377
|
return cmd_init(args)
|
|
1082
1378
|
|
|
1083
1379
|
print("Usage: mtrx project <list|current|switch|create|init>", file=sys.stderr)
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1380
|
+
return 1
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def _cmd_agents(args) -> int:
|
|
1384
|
+
from matrx.cli.agent_cmds import cmd_list, cmd_push, cmd_watch, cmd_status
|
|
1385
|
+
|
|
1386
|
+
sub = getattr(args, "agents_command", None)
|
|
1387
|
+
if sub is None:
|
|
1388
|
+
print("Usage: mtrx agents <list|push|watch|status>", file=sys.stderr)
|
|
1389
|
+
print(" list [--group ID] [--history] Show agents active in the group")
|
|
1390
|
+
print(" push <target> <prompt> [--group ID] Push a task into an agent's inbox")
|
|
1391
|
+
print(" watch [--group ID] Stream live group events")
|
|
1392
|
+
print(" status [--group ID] Show workspace group/agent identity")
|
|
1393
|
+
return 1
|
|
1394
|
+
|
|
1395
|
+
state = load_state()
|
|
1396
|
+
try:
|
|
1397
|
+
state, login_changed = _complete_matrx_login(state)
|
|
1398
|
+
except ValueError as exc:
|
|
1399
|
+
print(str(exc), file=sys.stderr)
|
|
1400
|
+
return 1
|
|
1401
|
+
if login_changed:
|
|
1402
|
+
save_state(state)
|
|
1403
|
+
|
|
1404
|
+
if sub == "list":
|
|
1405
|
+
return cmd_list(args)
|
|
1406
|
+
if sub == "push":
|
|
1407
|
+
return cmd_push(args)
|
|
1408
|
+
if sub == "watch":
|
|
1409
|
+
return cmd_watch(args)
|
|
1410
|
+
if sub == "status":
|
|
1411
|
+
return cmd_status(args)
|
|
1412
|
+
|
|
1413
|
+
print("Usage: mtrx agents <list|push|watch|status>", file=sys.stderr)
|
|
1089
1414
|
return 1
|
|
1090
1415
|
|
|
1091
1416
|
|
package/src/matrx/cli/state.py
CHANGED
|
@@ -49,6 +49,13 @@ DEFAULT_STATE: dict = {
|
|
|
49
49
|
"gemini": None,
|
|
50
50
|
"cursor": None,
|
|
51
51
|
},
|
|
52
|
+
# True = user ran `mtrx use <tool> direct` and we should not auto-switch to matrx.
|
|
53
|
+
"explicit_direct_route": {
|
|
54
|
+
"codex": False,
|
|
55
|
+
"claude": False,
|
|
56
|
+
"gemini": False,
|
|
57
|
+
"cursor": False,
|
|
58
|
+
},
|
|
52
59
|
"workspaces": {
|
|
53
60
|
"bindings": {},
|
|
54
61
|
},
|
|
@@ -166,6 +173,10 @@ def normalize_matrx_key(value: str | None) -> str:
|
|
|
166
173
|
return cleaned
|
|
167
174
|
|
|
168
175
|
|
|
176
|
+
def _normalize_binding_value(value: str | None) -> str:
|
|
177
|
+
return (value or "").strip()
|
|
178
|
+
|
|
179
|
+
|
|
169
180
|
def ensure_v1_url(base_url: str | None) -> str:
|
|
170
181
|
cleaned = _normalize_base_url(base_url).rstrip("/")
|
|
171
182
|
if cleaned.endswith("/v1"):
|
|
@@ -292,6 +303,10 @@ def _merge_dicts(target: dict, source: dict) -> None:
|
|
|
292
303
|
|
|
293
304
|
|
|
294
305
|
def _normalize_state(state: dict) -> None:
|
|
306
|
+
explicit = state.setdefault("explicit_direct_route", {})
|
|
307
|
+
for tool in ("codex", "claude", "gemini", "cursor"):
|
|
308
|
+
explicit.setdefault(tool, False)
|
|
309
|
+
|
|
295
310
|
auth = state.setdefault("auth", {}).setdefault("matrx", {})
|
|
296
311
|
auth["key"] = normalize_matrx_key(auth.get("key")) or None
|
|
297
312
|
auth["base_url"] = _normalize_base_url(auth.get("base_url"))
|
|
@@ -309,6 +324,12 @@ def _normalize_state(state: dict) -> None:
|
|
|
309
324
|
binding["matrx_key"] = matrx_key
|
|
310
325
|
else:
|
|
311
326
|
binding.pop("matrx_key", None)
|
|
327
|
+
for field in ("project_id", "group_id"):
|
|
328
|
+
cleaned = _normalize_binding_value(binding.get(field))
|
|
329
|
+
if cleaned:
|
|
330
|
+
binding[field] = cleaned
|
|
331
|
+
else:
|
|
332
|
+
binding.pop(field, None)
|
|
312
333
|
|
|
313
334
|
|
|
314
335
|
def _normalize_base_url(base_url: str | None) -> str:
|