mtrx-cli 0.1.11 → 0.1.14
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 +7 -2
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/cursor_ca.py +275 -0
- package/src/matrx/cli/cursor_config.py +262 -74
- package/src/matrx/cli/cursor_daemon.py +64 -0
- package/src/matrx/cli/cursor_launcher.py +106 -0
- package/src/matrx/cli/cursor_proxy.py +459 -261
- package/src/matrx/cli/cursor_service.py +343 -0
- package/src/matrx/cli/launcher.py +47 -27
- package/src/matrx/cli/main.py +156 -52
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Read / write Cursor IDE settings
|
|
2
|
+
Read / write Cursor IDE settings.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Two configuration surfaces:
|
|
5
|
+
|
|
6
|
+
1. **settings.json** -- Cursor's user preferences file (VS-Code–compatible).
|
|
7
|
+
The MITM proxy approach writes ``http.proxy`` and ``http.proxyStrictSSL``
|
|
8
|
+
here so that Cursor routes *all* traffic through the local proxy.
|
|
9
|
+
|
|
10
|
+
2. **state.vscdb** -- Cursor's internal SQLite key-value store. The legacy
|
|
11
|
+
"Override OpenAI Base URL" approach writes API-key / base-URL overrides
|
|
12
|
+
here. These helpers are retained for ``mtrx use cursor direct`` restore.
|
|
7
13
|
|
|
8
14
|
This module provides helpers to:
|
|
9
|
-
- locate the database on each platform
|
|
10
|
-
-
|
|
11
|
-
-
|
|
15
|
+
- locate the database / settings file on each platform
|
|
16
|
+
- read, write, backup, and restore settings
|
|
17
|
+
- configure ``http.proxy`` for the MITM proxy
|
|
12
18
|
- print manual setup instructions as a fallback
|
|
13
19
|
"""
|
|
14
20
|
|
|
@@ -36,6 +42,14 @@ _OPENAI_BASE_URL_FIELD = "openaiBaseUrl"
|
|
|
36
42
|
_OVERRIDE_OPENAI_BASE_URL_FIELD = "enableOpenaiBaseUrlOverride"
|
|
37
43
|
_ANTHROPIC_KEY_FIELD = "anthropicApiKey"
|
|
38
44
|
|
|
45
|
+
# Cursor v2.5+ moved BYOK settings to the reactive persistent storage blob.
|
|
46
|
+
_REACTIVE_STORAGE_KEY = (
|
|
47
|
+
"src.vs.platform.reactivestorage.browser."
|
|
48
|
+
"reactiveStorageServiceImpl.persistentStorage.applicationUser"
|
|
49
|
+
)
|
|
50
|
+
_RS_USE_OPENAI_KEY = "useOpenAIKey"
|
|
51
|
+
_RS_BASE_URL = "openAIBaseUrl"
|
|
52
|
+
|
|
39
53
|
|
|
40
54
|
def cursor_state_db_path() -> Path:
|
|
41
55
|
"""Return the platform-specific path to Cursor's ``state.vscdb``."""
|
|
@@ -206,6 +220,34 @@ def write_cursor_settings(
|
|
|
206
220
|
conn.close()
|
|
207
221
|
|
|
208
222
|
|
|
223
|
+
def _read_reactive_storage(conn: sqlite3.Connection) -> dict | None:
|
|
224
|
+
cursor = conn.cursor()
|
|
225
|
+
cursor.execute(
|
|
226
|
+
"SELECT value FROM ItemTable WHERE key = ?", (_REACTIVE_STORAGE_KEY,)
|
|
227
|
+
)
|
|
228
|
+
row = cursor.fetchone()
|
|
229
|
+
if row is None:
|
|
230
|
+
return None
|
|
231
|
+
try:
|
|
232
|
+
return json.loads(row[0]) if isinstance(row[0], str) else None
|
|
233
|
+
except (json.JSONDecodeError, TypeError):
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _write_reactive_storage(conn: sqlite3.Connection, data: dict) -> bool:
|
|
238
|
+
try:
|
|
239
|
+
cursor = conn.cursor()
|
|
240
|
+
cursor.execute(
|
|
241
|
+
"INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)",
|
|
242
|
+
(_REACTIVE_STORAGE_KEY, json.dumps(data)),
|
|
243
|
+
)
|
|
244
|
+
conn.commit()
|
|
245
|
+
return True
|
|
246
|
+
except sqlite3.Error as exc:
|
|
247
|
+
logger.debug("cursor_config: reactive storage write error: %s", exc)
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
209
251
|
def configure_cursor_for_proxy(
|
|
210
252
|
proxy_url: str,
|
|
211
253
|
proxy_api_key: str = "mtrx-cursor-proxy",
|
|
@@ -213,56 +255,78 @@ def configure_cursor_for_proxy(
|
|
|
213
255
|
db_path: Path | None = None,
|
|
214
256
|
) -> dict | None:
|
|
215
257
|
"""
|
|
216
|
-
Configure Cursor to route
|
|
258
|
+
Configure Cursor to route all model traffic through MTRX.
|
|
217
259
|
|
|
218
|
-
|
|
260
|
+
Writes to both the legacy ``storage.ServiceStorage`` blob and the
|
|
261
|
+
current reactive persistent storage used by Cursor v2.5+.
|
|
219
262
|
Returns the previous settings dict (for later restoration), or None on failure.
|
|
220
263
|
"""
|
|
221
264
|
db_path = db_path or cursor_state_db_path()
|
|
222
|
-
|
|
265
|
+
conn = _open_db(db_path)
|
|
266
|
+
if conn is None:
|
|
267
|
+
return None
|
|
223
268
|
|
|
224
|
-
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if
|
|
229
|
-
|
|
230
|
-
|
|
269
|
+
try:
|
|
270
|
+
# --- Reactive storage (Cursor v2.5+) ---
|
|
271
|
+
rs_data = _read_reactive_storage(conn)
|
|
272
|
+
rs_previous = {}
|
|
273
|
+
if rs_data is not None:
|
|
274
|
+
rs_previous = {
|
|
275
|
+
_RS_USE_OPENAI_KEY: rs_data.get(_RS_USE_OPENAI_KEY),
|
|
276
|
+
_RS_BASE_URL: rs_data.get(_RS_BASE_URL),
|
|
277
|
+
}
|
|
278
|
+
rs_data[_RS_USE_OPENAI_KEY] = True
|
|
279
|
+
rs_data[_RS_BASE_URL] = proxy_url
|
|
280
|
+
_write_reactive_storage(conn, rs_data)
|
|
281
|
+
|
|
282
|
+
# --- Legacy storage (still written for older Cursor versions) ---
|
|
283
|
+
legacy_previous = {}
|
|
284
|
+
settings_key = _find_settings_key(conn) or _scan_for_settings_key(conn)
|
|
285
|
+
if settings_key:
|
|
286
|
+
cur = conn.cursor()
|
|
287
|
+
cur.execute("SELECT value FROM ItemTable WHERE key = ?", (settings_key,))
|
|
288
|
+
row = cur.fetchone()
|
|
289
|
+
if row:
|
|
290
|
+
current = json.loads(row[0]) if isinstance(row[0], str) else {}
|
|
291
|
+
legacy_previous = {
|
|
292
|
+
_OPENAI_KEY_FIELD: current.get(_OPENAI_KEY_FIELD),
|
|
293
|
+
_OPENAI_BASE_URL_FIELD: current.get(_OPENAI_BASE_URL_FIELD),
|
|
294
|
+
_OVERRIDE_OPENAI_BASE_URL_FIELD: current.get(_OVERRIDE_OPENAI_BASE_URL_FIELD),
|
|
295
|
+
_ANTHROPIC_KEY_FIELD: current.get(_ANTHROPIC_KEY_FIELD),
|
|
296
|
+
"_mtrx_settings_key": settings_key,
|
|
297
|
+
}
|
|
298
|
+
current[_OPENAI_KEY_FIELD] = proxy_api_key
|
|
299
|
+
current[_OPENAI_BASE_URL_FIELD] = proxy_url
|
|
300
|
+
current[_OVERRIDE_OPENAI_BASE_URL_FIELD] = True
|
|
301
|
+
cur.execute(
|
|
302
|
+
"INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)",
|
|
303
|
+
(settings_key, json.dumps(current)),
|
|
304
|
+
)
|
|
305
|
+
conn.commit()
|
|
306
|
+
if not legacy_previous:
|
|
231
307
|
settings_key = _CANDIDATE_SETTINGS_KEYS[0]
|
|
232
308
|
fresh: dict = {
|
|
233
309
|
_OPENAI_KEY_FIELD: proxy_api_key,
|
|
234
310
|
_OPENAI_BASE_URL_FIELD: proxy_url,
|
|
235
311
|
_OVERRIDE_OPENAI_BASE_URL_FIELD: True,
|
|
236
312
|
}
|
|
237
|
-
|
|
238
|
-
|
|
313
|
+
cur = conn.cursor()
|
|
314
|
+
cur.execute(
|
|
239
315
|
"INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)",
|
|
240
316
|
(settings_key, json.dumps(fresh)),
|
|
241
317
|
)
|
|
242
318
|
conn.commit()
|
|
243
|
-
|
|
244
|
-
except sqlite3.Error as exc:
|
|
245
|
-
logger.debug("cursor_config: fresh insert error: %s", exc)
|
|
246
|
-
return None
|
|
247
|
-
finally:
|
|
248
|
-
conn.close()
|
|
319
|
+
legacy_previous = {"_mtrx_settings_key": settings_key, "_mtrx_was_absent": True}
|
|
249
320
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
current[_OPENAI_KEY_FIELD] = proxy_api_key
|
|
260
|
-
current[_OPENAI_BASE_URL_FIELD] = proxy_url
|
|
261
|
-
current[_OVERRIDE_OPENAI_BASE_URL_FIELD] = True
|
|
262
|
-
|
|
263
|
-
if write_cursor_settings(db_path, settings_key, current):
|
|
264
|
-
return previous
|
|
265
|
-
return None
|
|
321
|
+
return {
|
|
322
|
+
**legacy_previous,
|
|
323
|
+
"_mtrx_rs_previous": rs_previous,
|
|
324
|
+
}
|
|
325
|
+
except sqlite3.Error as exc:
|
|
326
|
+
logger.debug("cursor_config: configure error: %s", exc)
|
|
327
|
+
return None
|
|
328
|
+
finally:
|
|
329
|
+
conn.close()
|
|
266
330
|
|
|
267
331
|
|
|
268
332
|
def restore_cursor_settings(
|
|
@@ -272,46 +336,56 @@ def restore_cursor_settings(
|
|
|
272
336
|
) -> bool:
|
|
273
337
|
"""Restore Cursor settings to their pre-proxy state."""
|
|
274
338
|
db_path = db_path or cursor_state_db_path()
|
|
275
|
-
|
|
339
|
+
conn = _open_db(db_path)
|
|
340
|
+
if conn is None:
|
|
341
|
+
return False
|
|
276
342
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
343
|
+
ok = True
|
|
344
|
+
try:
|
|
345
|
+
# --- Reactive storage ---
|
|
346
|
+
rs_prev = previous.get("_mtrx_rs_previous", {})
|
|
347
|
+
if rs_prev:
|
|
348
|
+
rs_data = _read_reactive_storage(conn)
|
|
349
|
+
if rs_data is not None:
|
|
350
|
+
for field in (_RS_USE_OPENAI_KEY, _RS_BASE_URL):
|
|
351
|
+
old_value = rs_prev.get(field)
|
|
352
|
+
if old_value is None:
|
|
353
|
+
rs_data.pop(field, None)
|
|
354
|
+
else:
|
|
355
|
+
rs_data[field] = old_value
|
|
356
|
+
_write_reactive_storage(conn, rs_data)
|
|
357
|
+
|
|
358
|
+
# --- Legacy storage ---
|
|
359
|
+
settings_key = previous.get("_mtrx_settings_key", "")
|
|
360
|
+
if previous.get("_mtrx_was_absent") and settings_key:
|
|
282
361
|
cursor = conn.cursor()
|
|
283
362
|
cursor.execute(
|
|
284
363
|
"DELETE FROM ItemTable WHERE key = ?", (settings_key,)
|
|
285
364
|
)
|
|
286
365
|
conn.commit()
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
old_value = previous.get(field)
|
|
309
|
-
if old_value is None:
|
|
310
|
-
current.pop(field, None)
|
|
311
|
-
else:
|
|
312
|
-
current[field] = old_value
|
|
366
|
+
elif settings_key:
|
|
367
|
+
current = read_cursor_settings(db_path)
|
|
368
|
+
if current is not None:
|
|
369
|
+
current.pop("_mtrx_settings_key", None)
|
|
370
|
+
for field in (
|
|
371
|
+
_OPENAI_KEY_FIELD,
|
|
372
|
+
_OPENAI_BASE_URL_FIELD,
|
|
373
|
+
_OVERRIDE_OPENAI_BASE_URL_FIELD,
|
|
374
|
+
_ANTHROPIC_KEY_FIELD,
|
|
375
|
+
):
|
|
376
|
+
old_value = previous.get(field)
|
|
377
|
+
if old_value is None:
|
|
378
|
+
current.pop(field, None)
|
|
379
|
+
else:
|
|
380
|
+
current[field] = old_value
|
|
381
|
+
ok = write_cursor_settings(db_path, settings_key, current)
|
|
382
|
+
except sqlite3.Error as exc:
|
|
383
|
+
logger.debug("cursor_config: restore error: %s", exc)
|
|
384
|
+
ok = False
|
|
385
|
+
finally:
|
|
386
|
+
conn.close()
|
|
313
387
|
|
|
314
|
-
return
|
|
388
|
+
return ok
|
|
315
389
|
|
|
316
390
|
|
|
317
391
|
def print_manual_setup_instructions(proxy_url: str) -> None:
|
|
@@ -329,3 +403,117 @@ def print_manual_setup_instructions(proxy_url: str) -> None:
|
|
|
329
403
|
print()
|
|
330
404
|
print(" All models (Claude, GPT, Gemini, etc.) will route through MTRX.")
|
|
331
405
|
print()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# =========================================================================
|
|
409
|
+
# settings.json helpers (MITM proxy approach)
|
|
410
|
+
# =========================================================================
|
|
411
|
+
|
|
412
|
+
def cursor_settings_json_path() -> Path:
|
|
413
|
+
"""Return the path to Cursor's ``settings.json``."""
|
|
414
|
+
system = platform.system()
|
|
415
|
+
if system == "Darwin":
|
|
416
|
+
return (
|
|
417
|
+
Path.home()
|
|
418
|
+
/ "Library"
|
|
419
|
+
/ "Application Support"
|
|
420
|
+
/ "Cursor"
|
|
421
|
+
/ "User"
|
|
422
|
+
/ "settings.json"
|
|
423
|
+
)
|
|
424
|
+
if system == "Linux":
|
|
425
|
+
return Path.home() / ".config" / "Cursor" / "User" / "settings.json"
|
|
426
|
+
# Windows
|
|
427
|
+
appdata = os.environ.get("APPDATA", "")
|
|
428
|
+
if appdata:
|
|
429
|
+
return Path(appdata) / "Cursor" / "User" / "settings.json"
|
|
430
|
+
return Path.home() / "AppData" / "Roaming" / "Cursor" / "User" / "settings.json"
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _read_settings_json(path: Path | None = None) -> dict:
|
|
434
|
+
path = path or cursor_settings_json_path()
|
|
435
|
+
if not path.exists():
|
|
436
|
+
return {}
|
|
437
|
+
try:
|
|
438
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
439
|
+
except (json.JSONDecodeError, OSError):
|
|
440
|
+
return {}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _write_settings_json(data: dict, path: Path | None = None) -> bool:
|
|
444
|
+
path = path or cursor_settings_json_path()
|
|
445
|
+
try:
|
|
446
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
447
|
+
path.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8")
|
|
448
|
+
return True
|
|
449
|
+
except OSError as exc:
|
|
450
|
+
logger.debug("cursor_config: settings.json write error: %s", exc)
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def configure_cursor_proxy_settings(
|
|
455
|
+
proxy_url: str,
|
|
456
|
+
ca_cert_path: str,
|
|
457
|
+
) -> dict:
|
|
458
|
+
"""Set ``http.proxy`` and ``NODE_EXTRA_CA_CERTS`` in Cursor's settings.
|
|
459
|
+
|
|
460
|
+
Returns the previous values of the modified keys (for restoration).
|
|
461
|
+
"""
|
|
462
|
+
settings = _read_settings_json()
|
|
463
|
+
|
|
464
|
+
previous = {
|
|
465
|
+
"http.proxy": settings.get("http.proxy"),
|
|
466
|
+
"http.proxyStrictSSL": settings.get("http.proxyStrictSSL"),
|
|
467
|
+
"terminal.integrated.env.osx": settings.get("terminal.integrated.env.osx"),
|
|
468
|
+
"terminal.integrated.env.linux": settings.get("terminal.integrated.env.linux"),
|
|
469
|
+
"terminal.integrated.env.windows": settings.get("terminal.integrated.env.windows"),
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
settings["http.proxy"] = proxy_url
|
|
473
|
+
settings["http.proxyStrictSSL"] = False
|
|
474
|
+
|
|
475
|
+
# Inject NODE_EXTRA_CA_CERTS into integrated terminal env so Cursor's
|
|
476
|
+
# Node.js runtime trusts our CA. Cursor itself reads this from the
|
|
477
|
+
# process environment, but setting it here covers more cases.
|
|
478
|
+
for env_key in (
|
|
479
|
+
"terminal.integrated.env.osx",
|
|
480
|
+
"terminal.integrated.env.linux",
|
|
481
|
+
"terminal.integrated.env.windows",
|
|
482
|
+
):
|
|
483
|
+
env_block = settings.get(env_key)
|
|
484
|
+
if not isinstance(env_block, dict):
|
|
485
|
+
env_block = {}
|
|
486
|
+
env_block["NODE_EXTRA_CA_CERTS"] = ca_cert_path
|
|
487
|
+
settings[env_key] = env_block
|
|
488
|
+
|
|
489
|
+
_write_settings_json(settings)
|
|
490
|
+
return previous
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def restore_cursor_proxy_settings(previous: dict) -> bool:
|
|
494
|
+
"""Restore Cursor's settings.json to pre-proxy values."""
|
|
495
|
+
settings = _read_settings_json()
|
|
496
|
+
|
|
497
|
+
for key in ("http.proxy", "http.proxyStrictSSL"):
|
|
498
|
+
old = previous.get(key)
|
|
499
|
+
if old is None:
|
|
500
|
+
settings.pop(key, None)
|
|
501
|
+
else:
|
|
502
|
+
settings[key] = old
|
|
503
|
+
|
|
504
|
+
for env_key in (
|
|
505
|
+
"terminal.integrated.env.osx",
|
|
506
|
+
"terminal.integrated.env.linux",
|
|
507
|
+
"terminal.integrated.env.windows",
|
|
508
|
+
):
|
|
509
|
+
old = previous.get(env_key)
|
|
510
|
+
if old is None:
|
|
511
|
+
env_block = settings.get(env_key)
|
|
512
|
+
if isinstance(env_block, dict):
|
|
513
|
+
env_block.pop("NODE_EXTRA_CA_CERTS", None)
|
|
514
|
+
if not env_block:
|
|
515
|
+
settings.pop(env_key, None)
|
|
516
|
+
else:
|
|
517
|
+
settings[env_key] = old
|
|
518
|
+
|
|
519
|
+
return _write_settings_json(settings)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daemon entry-point for the MTRX Cursor MITM proxy.
|
|
3
|
+
|
|
4
|
+
Launched by the platform service manager (Launch Agent, systemd, or
|
|
5
|
+
detached subprocess). Reads proxy configuration from a JSON file and
|
|
6
|
+
starts the MITM proxy loop.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
python -m matrx.cli.cursor_daemon --config ~/.config/mtrx/cursor-proxy.json \
|
|
11
|
+
--pid-file ~/.config/mtrx/cursor-proxy.pid
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
logging.basicConfig(
|
|
23
|
+
level=logging.INFO,
|
|
24
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> int:
|
|
29
|
+
parser = argparse.ArgumentParser(description="MTRX Cursor MITM proxy daemon")
|
|
30
|
+
parser.add_argument("--config", required=True, help="Path to proxy config JSON")
|
|
31
|
+
parser.add_argument("--pid-file", default=None, help="Path to write PID file")
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
config_path = Path(args.config)
|
|
35
|
+
if not config_path.exists():
|
|
36
|
+
print(f"Config file not found: {config_path}", file=sys.stderr)
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
40
|
+
matrx_key = config.get("matrx_key", "")
|
|
41
|
+
matrx_base_url = config.get("matrx_base_url", "")
|
|
42
|
+
host = config.get("host", "127.0.0.1")
|
|
43
|
+
port = config.get("port", 8842)
|
|
44
|
+
|
|
45
|
+
if not matrx_key or not matrx_base_url:
|
|
46
|
+
print("Invalid proxy config: matrx_key and matrx_base_url required", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
pid_file = Path(args.pid_file) if args.pid_file else None
|
|
50
|
+
|
|
51
|
+
from matrx.cli.cursor_proxy import run_proxy
|
|
52
|
+
|
|
53
|
+
run_proxy(
|
|
54
|
+
matrx_key=matrx_key,
|
|
55
|
+
matrx_base_url=matrx_base_url,
|
|
56
|
+
host=host,
|
|
57
|
+
port=port,
|
|
58
|
+
pid_file=pid_file,
|
|
59
|
+
)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform launcher for Cursor IDE with proxy environment variables.
|
|
3
|
+
|
|
4
|
+
Cursor must be started with HTTP_PROXY, HTTPS_PROXY, and NODE_EXTRA_CA_CERTS
|
|
5
|
+
for the MITM proxy to work. Launching from Dock/Spotlight does not pass
|
|
6
|
+
these env vars; this module finds the Cursor executable and launches it
|
|
7
|
+
with the correct environment on any platform.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _cursor_executable_darwin() -> str | None:
|
|
23
|
+
"""macOS: /Applications/Cursor.app/Contents/MacOS/Cursor."""
|
|
24
|
+
app = Path("/Applications/Cursor.app")
|
|
25
|
+
exe = app / "Contents" / "MacOS" / "Cursor"
|
|
26
|
+
if exe.exists():
|
|
27
|
+
return str(exe)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cursor_executable_linux() -> str | None:
|
|
32
|
+
"""Linux: cursor in PATH or common install locations."""
|
|
33
|
+
cursor = shutil.which("cursor")
|
|
34
|
+
if cursor:
|
|
35
|
+
return cursor
|
|
36
|
+
for path in (
|
|
37
|
+
Path.home() / ".local" / "share" / "cursor" / "bin" / "cursor",
|
|
38
|
+
Path.home() / ".local" / "bin" / "cursor",
|
|
39
|
+
):
|
|
40
|
+
if path.exists():
|
|
41
|
+
return str(path)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cursor_executable_windows() -> str | None:
|
|
46
|
+
"""Windows: Cursor.exe in Local AppData."""
|
|
47
|
+
local = os.environ.get("LOCALAPPDATA", "")
|
|
48
|
+
if local:
|
|
49
|
+
for parts in (("Programs", "cursor", "Cursor.exe"), ("Cursor", "Cursor.exe")):
|
|
50
|
+
exe = Path(local) / Path(*parts)
|
|
51
|
+
if exe.exists():
|
|
52
|
+
return str(exe)
|
|
53
|
+
alt = Path(local) / "cursor" / "Cursor.exe"
|
|
54
|
+
if alt.exists():
|
|
55
|
+
return str(alt)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_cursor_executable() -> str | None:
|
|
60
|
+
"""Return the path to the Cursor executable for the current platform."""
|
|
61
|
+
system = platform.system()
|
|
62
|
+
if system == "Darwin":
|
|
63
|
+
return _cursor_executable_darwin()
|
|
64
|
+
if system == "Linux":
|
|
65
|
+
return _cursor_executable_linux()
|
|
66
|
+
if system == "Windows":
|
|
67
|
+
return _cursor_executable_windows()
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def launch_cursor_with_proxy(
|
|
72
|
+
proxy_url: str,
|
|
73
|
+
ca_cert_path: str,
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Launch Cursor with HTTP_PROXY, HTTPS_PROXY, and NODE_EXTRA_CA_CERTS set.
|
|
77
|
+
|
|
78
|
+
Returns True if Cursor was launched, False if executable not found.
|
|
79
|
+
"""
|
|
80
|
+
exe = find_cursor_executable()
|
|
81
|
+
if not exe:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
env = os.environ.copy()
|
|
85
|
+
env["HTTP_PROXY"] = proxy_url
|
|
86
|
+
env["HTTPS_PROXY"] = proxy_url
|
|
87
|
+
env["NODE_EXTRA_CA_CERTS"] = str(ca_cert_path)
|
|
88
|
+
|
|
89
|
+
kwargs: dict = {}
|
|
90
|
+
if platform.system() == "Windows":
|
|
91
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
subprocess.Popen(
|
|
95
|
+
[exe],
|
|
96
|
+
env=env,
|
|
97
|
+
stdin=subprocess.DEVNULL,
|
|
98
|
+
stdout=subprocess.DEVNULL,
|
|
99
|
+
stderr=subprocess.DEVNULL,
|
|
100
|
+
start_new_session=(platform.system() != "Windows"),
|
|
101
|
+
**kwargs,
|
|
102
|
+
)
|
|
103
|
+
return True
|
|
104
|
+
except OSError as exc:
|
|
105
|
+
logger.warning("Failed to launch Cursor: %s", exc)
|
|
106
|
+
return False
|