mtrx-cli 0.1.8 → 0.1.10
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 +3 -1
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/cursor_config.py +331 -0
- package/src/matrx/cli/cursor_proxy.py +351 -0
- package/src/matrx/cli/launcher.py +160 -8
- package/src/matrx/cli/main.py +278 -12
- package/src/matrx/cli/state.py +22 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime as dt
|
|
3
4
|
import base64
|
|
4
5
|
import hashlib
|
|
5
|
-
import datetime as dt
|
|
6
|
-
import httpx
|
|
7
6
|
import json
|
|
8
7
|
import os
|
|
9
8
|
import platform
|
|
10
9
|
import re
|
|
11
10
|
import shutil
|
|
12
11
|
import subprocess
|
|
12
|
+
import threading
|
|
13
13
|
import uuid
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
14
16
|
try:
|
|
15
17
|
import tomllib
|
|
16
18
|
except ModuleNotFoundError: # pragma: no cover - Python 3.10 compatibility
|
|
@@ -23,6 +25,7 @@ from matrx.cli.state import (
|
|
|
23
25
|
ensure_root_url,
|
|
24
26
|
ensure_v1_url,
|
|
25
27
|
get_workspace_binding,
|
|
28
|
+
normalize_matrx_key,
|
|
26
29
|
set_workspace_binding,
|
|
27
30
|
)
|
|
28
31
|
|
|
@@ -74,6 +77,7 @@ class LaunchPlan:
|
|
|
74
77
|
args: list[str]
|
|
75
78
|
env: dict[str, str]
|
|
76
79
|
auth_source: str
|
|
80
|
+
orchestration: dict | None = None
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
def configured_route(state: dict, tool: str) -> str | None:
|
|
@@ -124,7 +128,8 @@ def build_launch_plan(
|
|
|
124
128
|
raise ValueError(f"{tool} executable not found in PATH")
|
|
125
129
|
|
|
126
130
|
route = resolve_route(state, tool, route_override)
|
|
127
|
-
|
|
131
|
+
source_env = dict(os.environ if base_env is None else base_env)
|
|
132
|
+
env = dict(source_env)
|
|
128
133
|
auth_source = ""
|
|
129
134
|
launch_args = list(passthrough_args or [])
|
|
130
135
|
|
|
@@ -147,6 +152,12 @@ def build_launch_plan(
|
|
|
147
152
|
args=launch_args,
|
|
148
153
|
env=env,
|
|
149
154
|
auth_source=auth_source,
|
|
155
|
+
orchestration=_build_orchestration_metadata(
|
|
156
|
+
state=state,
|
|
157
|
+
tool=tool,
|
|
158
|
+
route=route,
|
|
159
|
+
env=source_env,
|
|
160
|
+
),
|
|
150
161
|
)
|
|
151
162
|
|
|
152
163
|
|
|
@@ -180,8 +191,20 @@ def prepare_routed_setup(
|
|
|
180
191
|
|
|
181
192
|
|
|
182
193
|
def launch(plan: LaunchPlan) -> int:
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
heartbeat_stop = None
|
|
195
|
+
heartbeat_thread = None
|
|
196
|
+
if plan.orchestration:
|
|
197
|
+
_best_effort_register_cli_agent(plan.orchestration)
|
|
198
|
+
heartbeat_stop, heartbeat_thread = _start_orchestration_heartbeat(plan.orchestration)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
result = subprocess.run([plan.executable, *plan.args], env=plan.env, check=False)
|
|
202
|
+
return int(result.returncode)
|
|
203
|
+
finally:
|
|
204
|
+
if heartbeat_stop is not None:
|
|
205
|
+
heartbeat_stop.set()
|
|
206
|
+
if heartbeat_thread is not None:
|
|
207
|
+
heartbeat_thread.join(timeout=2)
|
|
185
208
|
|
|
186
209
|
|
|
187
210
|
def validate_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
@@ -360,16 +383,16 @@ def _resolve_matrx_route_key(
|
|
|
360
383
|
state: dict,
|
|
361
384
|
env: dict[str, str],
|
|
362
385
|
) -> tuple[str, str]:
|
|
363
|
-
env_key = (env.get("MTRX_KEY")
|
|
386
|
+
env_key = normalize_matrx_key(env.get("MTRX_KEY"))
|
|
364
387
|
if env_key:
|
|
365
388
|
return env_key, "env_matrx_key"
|
|
366
389
|
|
|
367
390
|
binding = get_workspace_binding(state, cwd=_workspace_cwd(env))
|
|
368
|
-
binding_key = ((binding or {}).get("matrx_key")
|
|
391
|
+
binding_key = normalize_matrx_key((binding or {}).get("matrx_key"))
|
|
369
392
|
if binding_key:
|
|
370
393
|
return binding_key, "workspace_matrx_key"
|
|
371
394
|
|
|
372
|
-
saved_key = (state.get("auth", {}).get("matrx", {}).get("key")
|
|
395
|
+
saved_key = normalize_matrx_key(state.get("auth", {}).get("matrx", {}).get("key"))
|
|
373
396
|
if saved_key:
|
|
374
397
|
return saved_key, "saved_matrx_key"
|
|
375
398
|
|
|
@@ -555,6 +578,135 @@ def _build_claude_env(
|
|
|
555
578
|
return env, "existing_claude_auth"
|
|
556
579
|
|
|
557
580
|
|
|
581
|
+
def _build_orchestration_metadata(
|
|
582
|
+
*,
|
|
583
|
+
state: dict,
|
|
584
|
+
tool: str,
|
|
585
|
+
route: str,
|
|
586
|
+
env: dict[str, str],
|
|
587
|
+
) -> dict | None:
|
|
588
|
+
if route != "matrx":
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
mx_key, _ = _resolve_matrx_route_key(state, env)
|
|
592
|
+
if not mx_key:
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
if tool == "codex":
|
|
596
|
+
agent_id = "codex-cli"
|
|
597
|
+
provider = "codex"
|
|
598
|
+
capabilities = ["cli", "codex"]
|
|
599
|
+
name = "Codex CLI"
|
|
600
|
+
elif tool == "claude":
|
|
601
|
+
agent_id = "claude-cli"
|
|
602
|
+
provider = "claude_code"
|
|
603
|
+
capabilities = ["claude", "cli"]
|
|
604
|
+
name = "Claude CLI"
|
|
605
|
+
else:
|
|
606
|
+
agent_id = f"{tool}-cli"
|
|
607
|
+
provider = tool
|
|
608
|
+
capabilities = ["cli", tool]
|
|
609
|
+
name = f"{tool.capitalize()} CLI"
|
|
610
|
+
|
|
611
|
+
_, project_id = _resolve_matrx_context_overrides(state, env)
|
|
612
|
+
return {
|
|
613
|
+
"base_url": ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url")),
|
|
614
|
+
"matrx_key": mx_key,
|
|
615
|
+
"agent_id": agent_id,
|
|
616
|
+
"provider": provider,
|
|
617
|
+
"name": name,
|
|
618
|
+
"capabilities": capabilities,
|
|
619
|
+
"project_id": project_id or None,
|
|
620
|
+
"heartbeat_interval_seconds": 60,
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _best_effort_register_cli_agent(orchestration: dict) -> None:
|
|
625
|
+
if not orchestration:
|
|
626
|
+
return
|
|
627
|
+
base_url = (orchestration.get("base_url") or "").rstrip("/")
|
|
628
|
+
matrx_key = (orchestration.get("matrx_key") or "").strip()
|
|
629
|
+
agent_id = (orchestration.get("agent_id") or "").strip()
|
|
630
|
+
if not base_url or not matrx_key or not agent_id:
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
payload = {
|
|
634
|
+
"agent_id": agent_id,
|
|
635
|
+
"name": orchestration.get("name") or agent_id,
|
|
636
|
+
"capabilities": list(orchestration.get("capabilities") or []),
|
|
637
|
+
"max_concurrent_tasks": 1,
|
|
638
|
+
}
|
|
639
|
+
headers = {
|
|
640
|
+
"X-Matrx-Key": matrx_key,
|
|
641
|
+
"X-Matrx-Agent-Id": agent_id,
|
|
642
|
+
"X-Matrx-Provider": orchestration.get("provider") or "cli",
|
|
643
|
+
"Content-Type": "application/json",
|
|
644
|
+
}
|
|
645
|
+
project_id = (orchestration.get("project_id") or "").strip()
|
|
646
|
+
if project_id:
|
|
647
|
+
headers["X-Matrx-Project-Id"] = project_id
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
with httpx.Client(timeout=5) as client:
|
|
651
|
+
response = client.post(
|
|
652
|
+
f"{base_url}/v1/orchestration/agents/register",
|
|
653
|
+
headers=headers,
|
|
654
|
+
json=payload,
|
|
655
|
+
)
|
|
656
|
+
if response.status_code in {404, 401, 403}:
|
|
657
|
+
return
|
|
658
|
+
response.raise_for_status()
|
|
659
|
+
except httpx.HTTPError:
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _heartbeat_cli_agent(orchestration: dict, stop_event) -> None:
|
|
664
|
+
base_url = (orchestration.get("base_url") or "").rstrip("/")
|
|
665
|
+
matrx_key = (orchestration.get("matrx_key") or "").strip()
|
|
666
|
+
agent_id = (orchestration.get("agent_id") or "").strip()
|
|
667
|
+
if not base_url or not matrx_key or not agent_id:
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
headers = {
|
|
671
|
+
"X-Matrx-Key": matrx_key,
|
|
672
|
+
"X-Matrx-Agent-Id": agent_id,
|
|
673
|
+
"X-Matrx-Provider": orchestration.get("provider") or "cli",
|
|
674
|
+
}
|
|
675
|
+
project_id = (orchestration.get("project_id") or "").strip()
|
|
676
|
+
if project_id:
|
|
677
|
+
headers["X-Matrx-Project-Id"] = project_id
|
|
678
|
+
|
|
679
|
+
interval = int(orchestration.get("heartbeat_interval_seconds") or 60)
|
|
680
|
+
while True:
|
|
681
|
+
try:
|
|
682
|
+
with httpx.Client(timeout=5) as client:
|
|
683
|
+
response = client.patch(
|
|
684
|
+
f"{base_url}/v1/orchestration/agents/{agent_id}/heartbeat",
|
|
685
|
+
headers=headers,
|
|
686
|
+
)
|
|
687
|
+
if response.status_code in {404, 401, 403}:
|
|
688
|
+
return
|
|
689
|
+
except httpx.HTTPError:
|
|
690
|
+
return
|
|
691
|
+
if stop_event.wait(interval):
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _start_orchestration_heartbeat(
|
|
696
|
+
orchestration: dict,
|
|
697
|
+
) -> tuple[threading.Event, threading.Thread] | tuple[None, None]:
|
|
698
|
+
if not orchestration:
|
|
699
|
+
return None, None
|
|
700
|
+
stop_event = threading.Event()
|
|
701
|
+
thread = threading.Thread(
|
|
702
|
+
target=_heartbeat_cli_agent,
|
|
703
|
+
args=(orchestration, stop_event),
|
|
704
|
+
daemon=True,
|
|
705
|
+
)
|
|
706
|
+
thread.start()
|
|
707
|
+
return stop_event, thread
|
|
708
|
+
|
|
709
|
+
|
|
558
710
|
def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
559
711
|
"""Validate Claude launch plan before spawning."""
|
|
560
712
|
if plan.route != "matrx":
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -9,6 +9,7 @@ import sys
|
|
|
9
9
|
import threading
|
|
10
10
|
import urllib.parse
|
|
11
11
|
import webbrowser
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
14
15
|
|
|
@@ -29,6 +30,15 @@ from matrx.cli.launcher import (
|
|
|
29
30
|
resolve_route,
|
|
30
31
|
validate_launch_plan,
|
|
31
32
|
)
|
|
33
|
+
from matrx.cli.cursor_config import (
|
|
34
|
+
configure_cursor_for_proxy,
|
|
35
|
+
cursor_is_running,
|
|
36
|
+
cursor_state_db_path,
|
|
37
|
+
print_manual_setup_instructions,
|
|
38
|
+
read_cursor_settings,
|
|
39
|
+
restore_cursor_settings,
|
|
40
|
+
)
|
|
41
|
+
from matrx.cli.cursor_proxy import CursorProxyServer
|
|
32
42
|
from matrx.cli.state import (
|
|
33
43
|
ensure_app_url,
|
|
34
44
|
ensure_root_url,
|
|
@@ -36,6 +46,7 @@ from matrx.cli.state import (
|
|
|
36
46
|
get_workspace_binding,
|
|
37
47
|
load_state,
|
|
38
48
|
mask_secret,
|
|
49
|
+
normalize_matrx_key,
|
|
39
50
|
save_state,
|
|
40
51
|
)
|
|
41
52
|
|
|
@@ -62,6 +73,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
62
73
|
return _cmd_personal(args)
|
|
63
74
|
if args.command in {"codex", "claude"}:
|
|
64
75
|
return _cmd_launch(args.command, args.route, remainder)
|
|
76
|
+
if args.command == "cursor":
|
|
77
|
+
return _cmd_cursor(args.route, args.port)
|
|
65
78
|
|
|
66
79
|
parser.print_help()
|
|
67
80
|
return 1
|
|
@@ -81,7 +94,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
81
94
|
login.add_argument("--import", dest="do_import", action="store_true")
|
|
82
95
|
|
|
83
96
|
use = subparsers.add_parser("use")
|
|
84
|
-
use.add_argument("tool", choices=["codex", "claude"])
|
|
97
|
+
use.add_argument("tool", choices=["codex", "claude", "cursor"])
|
|
85
98
|
use.add_argument("route", choices=["direct", "matrx"])
|
|
86
99
|
|
|
87
100
|
subparsers.add_parser("help")
|
|
@@ -100,6 +113,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
100
113
|
claude = subparsers.add_parser("claude")
|
|
101
114
|
claude.add_argument("--route", choices=["direct", "matrx"])
|
|
102
115
|
|
|
116
|
+
cursor = subparsers.add_parser("cursor")
|
|
117
|
+
cursor.add_argument("--route", choices=["direct", "matrx"])
|
|
118
|
+
cursor.add_argument("--port", type=int, default=0)
|
|
119
|
+
|
|
103
120
|
return parser
|
|
104
121
|
|
|
105
122
|
|
|
@@ -123,6 +140,73 @@ def _prompt_for_secret(prompt: str) -> str:
|
|
|
123
140
|
return input(f"{prompt}: ").strip()
|
|
124
141
|
|
|
125
142
|
|
|
143
|
+
def _legacy_shell_proxy_profiles() -> list[Path]:
|
|
144
|
+
markers = (
|
|
145
|
+
"# BEGIN MATRX PROXY (CODEX)",
|
|
146
|
+
'claude() { matrx_refresh_routing; command claude "$@"; }',
|
|
147
|
+
)
|
|
148
|
+
profiles: list[Path] = []
|
|
149
|
+
for path in (
|
|
150
|
+
Path.home() / ".zshrc",
|
|
151
|
+
Path.home() / ".zprofile",
|
|
152
|
+
Path.home() / ".bashrc",
|
|
153
|
+
Path.home() / ".bash_profile",
|
|
154
|
+
):
|
|
155
|
+
if not path.exists():
|
|
156
|
+
continue
|
|
157
|
+
try:
|
|
158
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
159
|
+
except OSError:
|
|
160
|
+
continue
|
|
161
|
+
if any(marker in text for marker in markers):
|
|
162
|
+
profiles.append(path)
|
|
163
|
+
return profiles
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _active_claude_proxy_env(env: dict[str, str] | None = None) -> dict[str, str]:
|
|
167
|
+
env = env or os.environ
|
|
168
|
+
active: dict[str, str] = {}
|
|
169
|
+
|
|
170
|
+
base_url = (env.get("ANTHROPIC_BASE_URL") or "").strip()
|
|
171
|
+
if base_url and "api.anthropic.com" not in base_url:
|
|
172
|
+
active["ANTHROPIC_BASE_URL"] = base_url
|
|
173
|
+
|
|
174
|
+
custom_headers = (env.get("ANTHROPIC_CUSTOM_HEADERS") or "").strip()
|
|
175
|
+
if "x-matrx-" in custom_headers.lower():
|
|
176
|
+
active["ANTHROPIC_CUSTOM_HEADERS"] = "x-matrx-*"
|
|
177
|
+
|
|
178
|
+
active_route = (env.get("MATRX_ACTIVE_ROUTE") or "").strip()
|
|
179
|
+
if active_route in {"proxy", "proxy_forced"}:
|
|
180
|
+
active["MATRX_ACTIVE_ROUTE"] = active_route
|
|
181
|
+
|
|
182
|
+
return active
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _print_claude_shell_proxy_hint(*, stream=None) -> None:
|
|
186
|
+
stream = stream or sys.stdout
|
|
187
|
+
profiles = _legacy_shell_proxy_profiles()
|
|
188
|
+
active_env = _active_claude_proxy_env()
|
|
189
|
+
if not profiles and not active_env:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if profiles:
|
|
193
|
+
joined = ", ".join(str(path) for path in profiles)
|
|
194
|
+
print(
|
|
195
|
+
f"[warn] Legacy shell MATRX proxy setup found in {joined}; plain `claude` will still be proxied outside `mtrx claude`.",
|
|
196
|
+
file=stream,
|
|
197
|
+
)
|
|
198
|
+
if active_env:
|
|
199
|
+
keys = ", ".join(sorted(active_env))
|
|
200
|
+
print(
|
|
201
|
+
f"[warn] Current shell still exports Claude proxy env ({keys}); plain `claude` in this shell will still be proxied.",
|
|
202
|
+
file=stream,
|
|
203
|
+
)
|
|
204
|
+
print(
|
|
205
|
+
"[warn] Cleanup: run `unfunction claude 2>/dev/null || true` and `unset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN MATRX_ACTIVE_ROUTE MATRX_BASE_URL MATRX_API_KEY MATRX_CLAUDE_MODE MATRX_FALLBACK_ENABLED MATRX_PROXY_TIMEOUT_SEC ANTHROPIC_DIRECT_BASE_URL`.",
|
|
206
|
+
file=stream,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
126
210
|
def _derive_callback_payload(params: dict[str, list[str]]) -> dict[str, str | None]:
|
|
127
211
|
def _one(name: str) -> str | None:
|
|
128
212
|
values = params.get(name) or []
|
|
@@ -140,7 +224,22 @@ def _derive_callback_payload(params: dict[str, list[str]]) -> dict[str, str | No
|
|
|
140
224
|
}
|
|
141
225
|
|
|
142
226
|
|
|
143
|
-
def
|
|
227
|
+
def _matrx_browser_redirect_url(app_url: str | None, payload: dict[str, str | None]) -> str | None:
|
|
228
|
+
base = (app_url or "").strip().rstrip("/")
|
|
229
|
+
if not base:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
project_id = (payload.get("project_id") or "").strip()
|
|
233
|
+
if project_id:
|
|
234
|
+
return f"{base}/dashboard/projects/{urllib.parse.quote(project_id, safe='')}"
|
|
235
|
+
return f"{base}/dashboard"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _matrx_browser_callback_html(
|
|
239
|
+
payload: dict[str, str | None],
|
|
240
|
+
*,
|
|
241
|
+
app_url: str | None = None,
|
|
242
|
+
) -> bytes:
|
|
144
243
|
error = (payload.get("error") or "").strip()
|
|
145
244
|
if error:
|
|
146
245
|
escaped_error = html.escape(error)
|
|
@@ -152,11 +251,23 @@ def _matrx_browser_callback_html(payload: dict[str, str | None]) -> bytes:
|
|
|
152
251
|
"</body></html>"
|
|
153
252
|
)
|
|
154
253
|
else:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
254
|
+
redirect_url = _matrx_browser_redirect_url(app_url, payload)
|
|
255
|
+
if redirect_url:
|
|
256
|
+
escaped_url = html.escape(redirect_url, quote=True)
|
|
257
|
+
body = (
|
|
258
|
+
"<html><head>"
|
|
259
|
+
f'<meta http-equiv="refresh" content="0;url={escaped_url}">'
|
|
260
|
+
"</head><body><h1>Matrx login complete.</h1>"
|
|
261
|
+
"<p>Redirecting to the Matrx dashboard.</p>"
|
|
262
|
+
f'<p>If you are not redirected automatically, <a href="{escaped_url}">open the dashboard</a>.</p>'
|
|
263
|
+
"</body></html>"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
body = (
|
|
267
|
+
"<html><body><h1>Matrx login complete.</h1>"
|
|
268
|
+
"<p>You can close this window and return to the terminal.</p>"
|
|
269
|
+
"</body></html>"
|
|
270
|
+
)
|
|
160
271
|
return body.encode("utf-8")
|
|
161
272
|
|
|
162
273
|
|
|
@@ -184,7 +295,7 @@ def _run_matrx_browser_login(
|
|
|
184
295
|
payload = _derive_callback_payload(urllib.parse.parse_qs(parsed.query))
|
|
185
296
|
result.update(payload)
|
|
186
297
|
event.set()
|
|
187
|
-
body = _matrx_browser_callback_html(payload)
|
|
298
|
+
body = _matrx_browser_callback_html(payload, app_url=app_url)
|
|
188
299
|
self.send_response(200)
|
|
189
300
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
190
301
|
self.send_header("Content-Length", str(len(body)))
|
|
@@ -376,6 +487,8 @@ def _cmd_use(args) -> int:
|
|
|
376
487
|
path = save_state(state)
|
|
377
488
|
print(f"Default route for {args.tool}: {args.route}")
|
|
378
489
|
print(f"Saved to {path}")
|
|
490
|
+
if args.tool == "claude" and args.route == "direct":
|
|
491
|
+
_print_claude_shell_proxy_hint()
|
|
379
492
|
return 0
|
|
380
493
|
|
|
381
494
|
|
|
@@ -385,6 +498,7 @@ def _cmd_status() -> int:
|
|
|
385
498
|
print("Defaults:")
|
|
386
499
|
print(f" codex: {_default_route_label(configured_route(state, 'codex'))}")
|
|
387
500
|
print(f" claude: {_default_route_label(configured_route(state, 'claude'))}")
|
|
501
|
+
print(f" cursor: {_default_route_label(configured_route(state, 'cursor'))}")
|
|
388
502
|
print("Auth:")
|
|
389
503
|
print(
|
|
390
504
|
" matrx: "
|
|
@@ -415,6 +529,16 @@ def _cmd_status() -> int:
|
|
|
415
529
|
print("Executables:")
|
|
416
530
|
print(f" codex: {find_executable('codex') or 'not found'}")
|
|
417
531
|
print(f" claude: {find_executable('claude') or 'not found'}")
|
|
532
|
+
profiles = _legacy_shell_proxy_profiles()
|
|
533
|
+
active_env = _active_claude_proxy_env()
|
|
534
|
+
if profiles or active_env:
|
|
535
|
+
print("Shell:")
|
|
536
|
+
if profiles:
|
|
537
|
+
joined = ", ".join(str(path) for path in profiles)
|
|
538
|
+
print(f" legacy claude proxy setup: present in {joined}")
|
|
539
|
+
if active_env:
|
|
540
|
+
keys = ", ".join(sorted(active_env))
|
|
541
|
+
print(f" active claude proxy env: {keys}")
|
|
418
542
|
return 0
|
|
419
543
|
|
|
420
544
|
|
|
@@ -527,6 +651,17 @@ def _cmd_doctor() -> int:
|
|
|
527
651
|
print(f"[fail] {tool} executable not found")
|
|
528
652
|
failures += 1
|
|
529
653
|
|
|
654
|
+
db_path = cursor_state_db_path()
|
|
655
|
+
if db_path.exists():
|
|
656
|
+
print(f"[ok] Cursor settings DB: {db_path}")
|
|
657
|
+
cursor_settings = read_cursor_settings(db_path)
|
|
658
|
+
if cursor_settings:
|
|
659
|
+
print("[ok] Cursor settings readable")
|
|
660
|
+
else:
|
|
661
|
+
print("[warn] Cursor settings key not found (set a custom API key in Cursor first)")
|
|
662
|
+
else:
|
|
663
|
+
print(f"[warn] Cursor settings DB not found at {db_path}")
|
|
664
|
+
|
|
530
665
|
matrx_key = (state["auth"]["matrx"].get("key") or "").strip()
|
|
531
666
|
if matrx_key:
|
|
532
667
|
print("[ok] Matrx key saved")
|
|
@@ -558,13 +693,30 @@ def _cmd_doctor() -> int:
|
|
|
558
693
|
else:
|
|
559
694
|
print(f"[warn] Local Claude OAuth not found at {claude_credentials_path()}")
|
|
560
695
|
|
|
561
|
-
|
|
696
|
+
profiles = _legacy_shell_proxy_profiles()
|
|
697
|
+
if profiles:
|
|
698
|
+
joined = ", ".join(str(path) for path in profiles)
|
|
699
|
+
print(
|
|
700
|
+
f"[warn] Legacy shell MATRX proxy setup found in {joined}; plain `claude` will still be proxied outside `mtrx claude`.",
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
active_env = _active_claude_proxy_env()
|
|
704
|
+
if active_env:
|
|
705
|
+
keys = ", ".join(sorted(active_env))
|
|
706
|
+
print(f"[warn] Current shell still exports Claude proxy env ({keys})")
|
|
707
|
+
print(
|
|
708
|
+
"[warn] Cleanup: `unfunction claude 2>/dev/null || true` and `unset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN MATRX_ACTIVE_ROUTE MATRX_BASE_URL MATRX_API_KEY MATRX_CLAUDE_MODE MATRX_FALLBACK_ENABLED MATRX_PROXY_TIMEOUT_SEC ANTHROPIC_DIRECT_BASE_URL`",
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
for tool in ("codex", "claude", "cursor"):
|
|
562
712
|
route = configured_route(state, tool)
|
|
563
713
|
if route == "matrx" and not _has_matrx_login(state, env=os.environ):
|
|
564
714
|
print(f"[fail] Default {tool} route is matrx but no Matrx key is saved")
|
|
565
715
|
failures += 1
|
|
566
716
|
elif route is None:
|
|
567
717
|
print(f"[warn] Default {tool} route not chosen yet (first `mtrx {tool}` launch will initialize it to matrx)")
|
|
718
|
+
if tool == "cursor":
|
|
719
|
+
continue
|
|
568
720
|
config_status = get_tool_config_status(state, tool)
|
|
569
721
|
if tool == "codex" and config_status["verified"] and not config_status["configured"]:
|
|
570
722
|
if route == "matrx":
|
|
@@ -591,6 +743,8 @@ def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
|
591
743
|
if effective_route == "matrx":
|
|
592
744
|
state, login_changed = _complete_matrx_login(state)
|
|
593
745
|
auth_changed = auth_changed or login_changed
|
|
746
|
+
if login_changed:
|
|
747
|
+
save_state(state)
|
|
594
748
|
if tool == "codex":
|
|
595
749
|
_complete_codex_login()
|
|
596
750
|
if tool == "claude":
|
|
@@ -626,16 +780,128 @@ def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
|
626
780
|
return launch(plan)
|
|
627
781
|
|
|
628
782
|
|
|
783
|
+
def _cmd_cursor(route: str | None, port: int) -> int:
|
|
784
|
+
import signal as _signal
|
|
785
|
+
|
|
786
|
+
state = load_state()
|
|
787
|
+
route, promoted = _maybe_promote_direct_route(state, "cursor", route)
|
|
788
|
+
effective_route = resolve_route(state, "cursor", route)
|
|
789
|
+
|
|
790
|
+
if effective_route == "direct":
|
|
791
|
+
print("Cursor route set to direct — MTRX proxy disabled.")
|
|
792
|
+
return 0
|
|
793
|
+
|
|
794
|
+
try:
|
|
795
|
+
state, login_changed = _complete_matrx_login(state)
|
|
796
|
+
except ValueError as exc:
|
|
797
|
+
print(str(exc), file=sys.stderr)
|
|
798
|
+
return 1
|
|
799
|
+
|
|
800
|
+
initialized = initialize_first_launch_route(state, "cursor", route)
|
|
801
|
+
|
|
802
|
+
mx_key = (
|
|
803
|
+
normalize_matrx_key(os.environ.get("MTRX_KEY"))
|
|
804
|
+
or normalize_matrx_key(
|
|
805
|
+
(get_workspace_binding(state, cwd=os.getcwd()) or {}).get("matrx_key")
|
|
806
|
+
)
|
|
807
|
+
or normalize_matrx_key(state.get("auth", {}).get("matrx", {}).get("key"))
|
|
808
|
+
)
|
|
809
|
+
if not mx_key:
|
|
810
|
+
print("No Matrx key available. Run: mtrx login matrx --key mx_...", file=sys.stderr)
|
|
811
|
+
return 1
|
|
812
|
+
|
|
813
|
+
matrx_base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
814
|
+
|
|
815
|
+
binding = get_workspace_binding(state, cwd=os.getcwd()) or {}
|
|
816
|
+
group_id = (os.environ.get("MTRX_GROUP_ID") or binding.get("group_id") or "").strip()
|
|
817
|
+
project_id = (os.environ.get("MTRX_PROJECT_ID") or binding.get("project_id") or "").strip()
|
|
818
|
+
|
|
819
|
+
if initialized or login_changed or promoted:
|
|
820
|
+
save_state(state)
|
|
821
|
+
if initialized:
|
|
822
|
+
print(
|
|
823
|
+
"First-time setup: default route for cursor set to matrx. "
|
|
824
|
+
"Use `mtrx use cursor direct` to opt out.",
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
proxy = CursorProxyServer(
|
|
828
|
+
matrx_key=mx_key,
|
|
829
|
+
matrx_base_url=matrx_base_url,
|
|
830
|
+
group_id=group_id,
|
|
831
|
+
project_id=project_id,
|
|
832
|
+
port=port,
|
|
833
|
+
)
|
|
834
|
+
try:
|
|
835
|
+
proxy.start_background()
|
|
836
|
+
except RuntimeError as exc:
|
|
837
|
+
print(f"Failed to start proxy: {exc}", file=sys.stderr)
|
|
838
|
+
return 1
|
|
839
|
+
|
|
840
|
+
proxy_url = proxy.url
|
|
841
|
+
print(f"MTRX Cursor proxy running at {proxy_url}")
|
|
842
|
+
print(f" session: {proxy.session_id}")
|
|
843
|
+
print(f" matrx_key: {mask_secret(mx_key)}")
|
|
844
|
+
print(f" upstream: {matrx_base_url}")
|
|
845
|
+
if group_id:
|
|
846
|
+
print(f" group: {group_id}")
|
|
847
|
+
if project_id:
|
|
848
|
+
print(f" project: {project_id}")
|
|
849
|
+
|
|
850
|
+
previous_settings = None
|
|
851
|
+
db_path = cursor_state_db_path()
|
|
852
|
+
if db_path.exists():
|
|
853
|
+
if cursor_is_running():
|
|
854
|
+
print()
|
|
855
|
+
print(" [warn] Cursor is currently running.")
|
|
856
|
+
print(" Settings changes may require restarting Cursor to take effect.")
|
|
857
|
+
previous_settings = configure_cursor_for_proxy(proxy_url, db_path=db_path)
|
|
858
|
+
if previous_settings is not None:
|
|
859
|
+
print()
|
|
860
|
+
print(" Cursor settings configured automatically.")
|
|
861
|
+
print(" Restart Cursor if it is already open.")
|
|
862
|
+
else:
|
|
863
|
+
print_manual_setup_instructions(proxy_url)
|
|
864
|
+
else:
|
|
865
|
+
print_manual_setup_instructions(proxy_url)
|
|
866
|
+
|
|
867
|
+
print()
|
|
868
|
+
print("Press Ctrl+C to stop the proxy and restore settings.")
|
|
869
|
+
|
|
870
|
+
stop_event = threading.Event()
|
|
871
|
+
|
|
872
|
+
def _on_signal(sig, frame):
|
|
873
|
+
stop_event.set()
|
|
874
|
+
|
|
875
|
+
_signal.signal(_signal.SIGINT, _on_signal)
|
|
876
|
+
_signal.signal(_signal.SIGTERM, _on_signal)
|
|
877
|
+
|
|
878
|
+
stop_event.wait()
|
|
879
|
+
|
|
880
|
+
print()
|
|
881
|
+
print("Shutting down MTRX Cursor proxy...")
|
|
882
|
+
proxy.stop()
|
|
883
|
+
|
|
884
|
+
if previous_settings is not None:
|
|
885
|
+
if restore_cursor_settings(previous_settings, db_path=db_path):
|
|
886
|
+
print("Cursor settings restored to previous values.")
|
|
887
|
+
else:
|
|
888
|
+
print("[warn] Could not restore Cursor settings automatically.")
|
|
889
|
+
print(" You may need to reset your OpenAI API Key and Base URL in Cursor Settings > Models.")
|
|
890
|
+
|
|
891
|
+
print("Done.")
|
|
892
|
+
return 0
|
|
893
|
+
|
|
894
|
+
|
|
629
895
|
def _has_matrx_login(state: dict, env: dict[str, str] | None = None) -> bool:
|
|
630
896
|
env = env or {}
|
|
631
|
-
env_key = (env.get("MTRX_KEY")
|
|
897
|
+
env_key = normalize_matrx_key(env.get("MTRX_KEY"))
|
|
632
898
|
if env_key:
|
|
633
899
|
return True
|
|
634
900
|
workspace_binding = get_workspace_binding(state, cwd=env.get("PWD") or os.getcwd()) or {}
|
|
635
|
-
workspace_key = (workspace_binding.get("matrx_key")
|
|
901
|
+
workspace_key = normalize_matrx_key(workspace_binding.get("matrx_key"))
|
|
636
902
|
if workspace_key:
|
|
637
903
|
return True
|
|
638
|
-
return bool((state.get("auth", {}).get("matrx", {}).get("key")
|
|
904
|
+
return bool(normalize_matrx_key(state.get("auth", {}).get("matrx", {}).get("key")))
|
|
639
905
|
|
|
640
906
|
|
|
641
907
|
def _default_route_label(route: str | None) -> str:
|