mtrx-cli 0.1.14 → 0.1.15
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 +2 -1
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/cursor_hooks.py +186 -0
- package/src/matrx/cli/main.py +25 -98
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtrx-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "MATRX CLI for routing Codex, Claude, and Cursor through Matrx",
|
|
5
5
|
"homepage": "https://mtrx.so",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"src/matrx/cli/__init__.py",
|
|
28
28
|
"src/matrx/cli/cursor_ca.py",
|
|
29
29
|
"src/matrx/cli/cursor_config.py",
|
|
30
|
+
"src/matrx/cli/cursor_hooks.py",
|
|
30
31
|
"src/matrx/cli/cursor_daemon.py",
|
|
31
32
|
"src/matrx/cli/cursor_launcher.py",
|
|
32
33
|
"src/matrx/cli/cursor_proxy.py",
|
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.15"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor Hooks integration for MTRX telemetry.
|
|
3
|
+
|
|
4
|
+
Uses Cursor's official Hooks API (https://cursor.com/docs/hooks) to send
|
|
5
|
+
session/agent events to MTRX. Works regardless of how Cursor was launched
|
|
6
|
+
(Dock, Spotlight, CLI) - same clean model as Claude/Codex.
|
|
7
|
+
|
|
8
|
+
Configures ~/.cursor/hooks.json and a Python script that POSTs to
|
|
9
|
+
POST /v1/telemetry/cursor/hooks.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from matrx.cli.state import config_dir
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_CURSOR_HOME = Path.home() / ".cursor"
|
|
24
|
+
_HOOKS_JSON = _CURSOR_HOME / "hooks.json"
|
|
25
|
+
_HOOKS_DIR = _CURSOR_HOME / "hooks"
|
|
26
|
+
_MTRX_HOOK_SCRIPT = _HOOKS_DIR / "mtrx-telemetry.py"
|
|
27
|
+
_CONFIG_PATH = "cursor-hooks-config.json"
|
|
28
|
+
|
|
29
|
+
_MTRX_HOOK_MARKER = "# MTRX cursor hooks (managed by mtrx cursor)"
|
|
30
|
+
_MTRX_HOOKS_KEY = "_mtrx_managed_hooks"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _hooks_config_path() -> Path:
|
|
34
|
+
return config_dir() / _CONFIG_PATH
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _read_hooks_json() -> dict:
|
|
38
|
+
if not _HOOKS_JSON.exists():
|
|
39
|
+
return {"version": 1, "hooks": {}}
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(_HOOKS_JSON.read_text(encoding="utf-8"))
|
|
42
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
43
|
+
logger.debug("cursor_hooks: could not read hooks.json: %s", exc)
|
|
44
|
+
return {"version": 1, "hooks": {}}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _write_hooks_json(data: dict) -> bool:
|
|
48
|
+
try:
|
|
49
|
+
_HOOKS_JSON.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
_HOOKS_JSON.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
51
|
+
return True
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
logger.debug("cursor_hooks: could not write hooks.json: %s", exc)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _hook_script_content() -> str:
|
|
58
|
+
return '''#!/usr/bin/env python3
|
|
59
|
+
"""MTRX Cursor hooks telemetry — forwards events to MTRX. Managed by mtrx cursor."""
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import json
|
|
63
|
+
import os
|
|
64
|
+
import sys
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
|
|
67
|
+
def _config_path() -> Path:
|
|
68
|
+
config_dir = Path(os.environ.get("MTRX_CONFIG_DIR", Path.home() / ".config" / "mtrx"))
|
|
69
|
+
return config_dir / "cursor-hooks-config.json"
|
|
70
|
+
|
|
71
|
+
def main() -> None:
|
|
72
|
+
try:
|
|
73
|
+
payload = json.load(sys.stdin)
|
|
74
|
+
except (json.JSONDecodeError, EOFError):
|
|
75
|
+
print("{}")
|
|
76
|
+
return
|
|
77
|
+
cfg_path = _config_path()
|
|
78
|
+
if not cfg_path.exists():
|
|
79
|
+
print("{}")
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
|
83
|
+
except (json.JSONDecodeError, OSError):
|
|
84
|
+
print("{}")
|
|
85
|
+
return
|
|
86
|
+
url = (cfg.get("matrx_base_url") or "").rstrip("/") + "/v1/telemetry/cursor/hooks"
|
|
87
|
+
key = (cfg.get("matrx_key") or "").strip()
|
|
88
|
+
if not url or not key or not key.startswith("mx_"):
|
|
89
|
+
print("{}")
|
|
90
|
+
return
|
|
91
|
+
try:
|
|
92
|
+
import urllib.request
|
|
93
|
+
req = urllib.request.Request(
|
|
94
|
+
url,
|
|
95
|
+
data=json.dumps(payload).encode(),
|
|
96
|
+
headers={"Content-Type": "application/json", "X-Matrx-Key": key},
|
|
97
|
+
method="POST",
|
|
98
|
+
)
|
|
99
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
100
|
+
pass
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
print("{}")
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
107
|
+
'''
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def install_mtrx_hooks(matrx_key: str, matrx_base_url: str) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Install MTRX hooks: create script, config, and merge into hooks.json.
|
|
113
|
+
|
|
114
|
+
Hooks: sessionEnd, stop — fire-and-forget telemetry to MTRX.
|
|
115
|
+
"""
|
|
116
|
+
base = (matrx_base_url or "").rstrip("/")
|
|
117
|
+
if not base or not matrx_key or not matrx_key.startswith("mx_"):
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
# Write hook script
|
|
121
|
+
_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
script_path = _HOOKS_DIR / "mtrx-telemetry.py"
|
|
123
|
+
try:
|
|
124
|
+
script_path.write_text(_hook_script_content(), encoding="utf-8")
|
|
125
|
+
script_path.chmod(0o755)
|
|
126
|
+
except OSError as exc:
|
|
127
|
+
logger.debug("cursor_hooks: could not write script: %s", exc)
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
# Write config (script reads this at runtime)
|
|
131
|
+
cfg_path = _hooks_config_path()
|
|
132
|
+
config_dir().mkdir(parents=True, exist_ok=True)
|
|
133
|
+
try:
|
|
134
|
+
cfg_path.write_text(
|
|
135
|
+
json.dumps({"matrx_key": matrx_key, "matrx_base_url": base}),
|
|
136
|
+
encoding="utf-8",
|
|
137
|
+
)
|
|
138
|
+
cfg_path.chmod(0o600)
|
|
139
|
+
except OSError as exc:
|
|
140
|
+
logger.debug("cursor_hooks: could not write config: %s", exc)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
# Merge into hooks.json — use ./hooks/mtrx-telemetry.py (relative to ~/.cursor/)
|
|
144
|
+
hooks = _read_hooks_json()
|
|
145
|
+
if "hooks" not in hooks:
|
|
146
|
+
hooks["hooks"] = {}
|
|
147
|
+
if "version" not in hooks:
|
|
148
|
+
hooks["version"] = 1
|
|
149
|
+
|
|
150
|
+
managed = {
|
|
151
|
+
"sessionEnd": [{"command": "./hooks/mtrx-telemetry.py"}],
|
|
152
|
+
"stop": [{"command": "./hooks/mtrx-telemetry.py"}],
|
|
153
|
+
}
|
|
154
|
+
for hook_name, defs in managed.items():
|
|
155
|
+
existing = hooks["hooks"].get(hook_name) or []
|
|
156
|
+
# Avoid duplicate
|
|
157
|
+
our_cmd = "./hooks/mtrx-telemetry.py"
|
|
158
|
+
if not any(d.get("command") == our_cmd for d in existing):
|
|
159
|
+
existing.extend(defs)
|
|
160
|
+
hooks["hooks"][hook_name] = existing
|
|
161
|
+
|
|
162
|
+
return _write_hooks_json(hooks)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def remove_mtrx_hooks() -> bool:
|
|
166
|
+
"""Remove MTRX hooks from hooks.json and delete config + script."""
|
|
167
|
+
removed = False
|
|
168
|
+
hooks = _read_hooks_json()
|
|
169
|
+
if "hooks" in hooks:
|
|
170
|
+
our_cmd = "./hooks/mtrx-telemetry.py"
|
|
171
|
+
for hook_name in ("sessionEnd", "stop"):
|
|
172
|
+
defs = hooks["hooks"].get(hook_name) or []
|
|
173
|
+
new_defs = [d for d in defs if d.get("command") != our_cmd]
|
|
174
|
+
if len(new_defs) != len(defs):
|
|
175
|
+
removed = True
|
|
176
|
+
hooks["hooks"][hook_name] = new_defs if new_defs else []
|
|
177
|
+
if removed:
|
|
178
|
+
_write_hooks_json(hooks)
|
|
179
|
+
_hooks_config_path().unlink(missing_ok=True)
|
|
180
|
+
_MTRX_HOOK_SCRIPT.unlink(missing_ok=True)
|
|
181
|
+
return removed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_mtrx_hooks_installed() -> bool:
|
|
185
|
+
"""Return True if MTRX hooks are configured."""
|
|
186
|
+
return _hooks_config_path().exists() and _MTRX_HOOK_SCRIPT.exists()
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -506,8 +506,13 @@ def _cmd_use(args) -> int:
|
|
|
506
506
|
def _restore_cursor_if_needed() -> None:
|
|
507
507
|
import json as _json
|
|
508
508
|
|
|
509
|
+
from matrx.cli.cursor_hooks import remove_mtrx_hooks
|
|
509
510
|
from matrx.cli.cursor_service import is_proxy_running, uninstall_service
|
|
510
511
|
|
|
512
|
+
# Remove MTRX Cursor hooks
|
|
513
|
+
if remove_mtrx_hooks():
|
|
514
|
+
print("Cursor hooks removed.")
|
|
515
|
+
|
|
511
516
|
# Stop the MITM proxy service if it's running
|
|
512
517
|
if is_proxy_running():
|
|
513
518
|
uninstall_service()
|
|
@@ -836,43 +841,23 @@ def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
|
836
841
|
|
|
837
842
|
|
|
838
843
|
def _cmd_cursor(args) -> int:
|
|
839
|
-
import
|
|
840
|
-
|
|
841
|
-
from matrx.cli.cursor_ca import (
|
|
842
|
-
ca_cert_path,
|
|
843
|
-
ca_exists,
|
|
844
|
-
generate_ca,
|
|
845
|
-
is_ca_trusted,
|
|
846
|
-
trust_ca_system,
|
|
847
|
-
)
|
|
848
|
-
from matrx.cli.cursor_proxy import DEFAULT_PORT, PROXY_HOST
|
|
849
|
-
from matrx.cli.cursor_launcher import (
|
|
850
|
-
find_cursor_executable,
|
|
851
|
-
launch_cursor_with_proxy,
|
|
852
|
-
)
|
|
853
|
-
from matrx.cli.cursor_service import (
|
|
854
|
-
get_proxy_status,
|
|
855
|
-
install_service,
|
|
856
|
-
is_proxy_running,
|
|
857
|
-
uninstall_service,
|
|
858
|
-
)
|
|
844
|
+
from matrx.cli.cursor_hooks import install_mtrx_hooks, is_mtrx_hooks_installed
|
|
859
845
|
|
|
860
846
|
route = args.route
|
|
861
847
|
|
|
862
|
-
# --status:
|
|
848
|
+
# --status: report hooks status
|
|
863
849
|
if args.status:
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
print("
|
|
867
|
-
print(f" requests processed: {status.get('requests', '?')}")
|
|
850
|
+
if is_mtrx_hooks_installed():
|
|
851
|
+
print("MTRX Cursor hooks: active")
|
|
852
|
+
print(" sessionEnd, stop → telemetry to MTRX")
|
|
868
853
|
else:
|
|
869
|
-
print("MTRX Cursor
|
|
854
|
+
print("MTRX Cursor hooks: not installed")
|
|
870
855
|
return 0
|
|
871
856
|
|
|
872
|
-
# --stop: tear down
|
|
857
|
+
# --stop: tear down
|
|
873
858
|
if args.stop:
|
|
874
859
|
_restore_cursor_if_needed()
|
|
875
|
-
print("Cursor route set to direct — MTRX
|
|
860
|
+
print("Cursor route set to direct — MTRX hooks disabled.")
|
|
876
861
|
return 0
|
|
877
862
|
|
|
878
863
|
state = load_state()
|
|
@@ -881,12 +866,12 @@ def _cmd_cursor(args) -> int:
|
|
|
881
866
|
|
|
882
867
|
if effective_route == "direct":
|
|
883
868
|
_restore_cursor_if_needed()
|
|
884
|
-
print("Cursor route set to direct — MTRX
|
|
869
|
+
print("Cursor route set to direct — MTRX hooks disabled.")
|
|
885
870
|
if cursor_is_running():
|
|
886
871
|
print(" Restart Cursor for settings to take effect.")
|
|
887
872
|
return 0
|
|
888
873
|
|
|
889
|
-
# --- matrx route:
|
|
874
|
+
# --- matrx route: install Cursor Hooks (official, works like Claude/Codex) ---
|
|
890
875
|
|
|
891
876
|
try:
|
|
892
877
|
state, login_changed = _complete_matrx_login(state)
|
|
@@ -919,75 +904,17 @@ def _cmd_cursor(args) -> int:
|
|
|
919
904
|
"Use `mtrx use cursor direct` to opt out.",
|
|
920
905
|
)
|
|
921
906
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
print("
|
|
925
|
-
|
|
926
|
-
print(
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
print("Trusting MTRX CA certificate (may require password)...")
|
|
931
|
-
if trust_ca_system():
|
|
932
|
-
print(" CA trusted in system keychain.")
|
|
933
|
-
else:
|
|
934
|
-
print(
|
|
935
|
-
f" [warn] Could not auto-trust CA. Cursor needs NODE_EXTRA_CA_CERTS={ca_cert_path()}"
|
|
936
|
-
)
|
|
937
|
-
print(
|
|
938
|
-
" You can manually trust it or set the env var before launching Cursor."
|
|
939
|
-
)
|
|
940
|
-
|
|
941
|
-
# Step 3: Install and start the proxy service
|
|
942
|
-
proxy_url = f"http://{PROXY_HOST}:{DEFAULT_PORT}"
|
|
943
|
-
if is_proxy_running():
|
|
944
|
-
print(f"MTRX proxy already running on {proxy_url}")
|
|
945
|
-
else:
|
|
946
|
-
print("Starting MTRX Cursor proxy service...")
|
|
947
|
-
if install_service(
|
|
948
|
-
matrx_key=mx_key,
|
|
949
|
-
matrx_base_url=matrx_base_url,
|
|
950
|
-
host=PROXY_HOST,
|
|
951
|
-
port=DEFAULT_PORT,
|
|
952
|
-
):
|
|
953
|
-
print(f" Proxy running on {proxy_url}")
|
|
954
|
-
else:
|
|
955
|
-
print("[warn] Proxy service may not have started. Check logs at:")
|
|
956
|
-
print(f" {config_dir() / 'logs' / 'cursor-proxy.err.log'}")
|
|
957
|
-
|
|
958
|
-
# Step 4: Configure Cursor's settings.json
|
|
959
|
-
conf_dir = config_dir()
|
|
960
|
-
conf_dir.mkdir(parents=True, exist_ok=True)
|
|
961
|
-
|
|
962
|
-
previous = configure_cursor_proxy_settings(
|
|
963
|
-
proxy_url=proxy_url,
|
|
964
|
-
ca_cert_path=str(ca_cert_path()),
|
|
965
|
-
)
|
|
966
|
-
prev_path = conf_dir / "cursor-proxy-previous-settings.json"
|
|
967
|
-
prev_path.write_text(_json.dumps(previous), encoding="utf-8")
|
|
968
|
-
|
|
969
|
-
print()
|
|
970
|
-
print("Cursor configured to route ALL traffic through MTRX.")
|
|
971
|
-
print(f" proxy: {proxy_url}")
|
|
972
|
-
print(f" ca_cert: {ca_cert_path()}")
|
|
973
|
-
print(f" telemetry: {matrx_base_url}/v1/telemetry/cursor")
|
|
974
|
-
print()
|
|
975
|
-
|
|
976
|
-
# Launch Cursor with proxy env vars (required for traffic to flow)
|
|
977
|
-
if getattr(args, "launch", False):
|
|
978
|
-
from matrx.cli.cursor_launcher import find_cursor_executable, launch_cursor_with_proxy
|
|
979
|
-
|
|
980
|
-
if launch_cursor_with_proxy(proxy_url, str(ca_cert_path())):
|
|
981
|
-
print(" Launched Cursor with proxy env vars — traffic will flow through MTRX.")
|
|
982
|
-
else:
|
|
983
|
-
print(" [warn] Could not launch Cursor. Is it installed?")
|
|
984
|
-
print(" To route traffic, launch Cursor via: mtrx cursor --launch")
|
|
907
|
+
if install_mtrx_hooks(mx_key, matrx_base_url):
|
|
908
|
+
print()
|
|
909
|
+
print("Cursor configured with MTRX hooks — Chat & Agent usage flows to MTRX.")
|
|
910
|
+
print(f" telemetry: {matrx_base_url}/v1/telemetry/cursor/hooks")
|
|
911
|
+
print()
|
|
912
|
+
print(" Works with Cursor from Dock, Spotlight, or CLI — no special launch needed.")
|
|
913
|
+
print(" Check status: mtrx cursor --status")
|
|
914
|
+
print(" To disable: mtrx use cursor direct")
|
|
985
915
|
else:
|
|
986
|
-
print("
|
|
987
|
-
|
|
988
|
-
print()
|
|
989
|
-
print(" Check status: mtrx cursor --status")
|
|
990
|
-
print(" To disable: mtrx use cursor direct")
|
|
916
|
+
print("[warn] Could not install Cursor hooks. Check ~/.cursor/ is writable.", file=sys.stderr)
|
|
917
|
+
return 1
|
|
991
918
|
return 0
|
|
992
919
|
|
|
993
920
|
|