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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrx-cli",
3
- "version": "0.1.14",
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",
@@ -1 +1 @@
1
- __version__ = "0.1.14"
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()
@@ -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 json as _json
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: just report proxy health
848
+ # --status: report hooks status
863
849
  if args.status:
864
- status = get_proxy_status()
865
- if status:
866
- print("MTRX Cursor proxy: running")
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 proxy: not running")
854
+ print("MTRX Cursor hooks: not installed")
870
855
  return 0
871
856
 
872
- # --stop: tear down the proxy
857
+ # --stop: tear down
873
858
  if args.stop:
874
859
  _restore_cursor_if_needed()
875
- print("Cursor route set to direct — MTRX proxy disabled.")
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 proxy disabled.")
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: set up the MITM proxy ---
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
- # Step 1: Generate CA certificate if needed
923
- if not ca_exists():
924
- print("Generating MTRX CA certificate...")
925
- generate_ca()
926
- print(f" CA cert: {ca_cert_path()}")
927
-
928
- # Step 2: Trust CA (one-time)
929
- if not is_ca_trusted():
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(" To route traffic, launch Cursor via: mtrx cursor --launch")
987
- print(" (Cursor must be started with proxy env vars; Dock/Spotlight launch won't work)")
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