mtrx-cli 0.1.0

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.
@@ -0,0 +1,291 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ from copy import deepcopy
7
+ from pathlib import Path
8
+ from urllib.parse import urlparse, urlunparse
9
+
10
+ DEFAULT_MATRX_BASE_URL = "https://api.mtrx.so"
11
+ DEFAULT_MATRX_APP_URL = "https://mtrx.so"
12
+ LEGACY_MATRX_BASE_URLS = {
13
+ "https://api.matrx.dev": DEFAULT_MATRX_BASE_URL,
14
+ }
15
+ LEGACY_MATRX_APP_URLS = {
16
+ "https://app.matrx.dev",
17
+ }
18
+ PUBLIC_MATRX_APP_HOSTS_BY_API_HOST = {
19
+ "api.matrx.dev": "mtrx.so",
20
+ "api.mtrx.so": "mtrx.so",
21
+ }
22
+ CONFIG_ENV_VAR = "MTRX_CONFIG_DIR"
23
+ CONFIG_FILENAME = "config.json"
24
+
25
+ DEFAULT_STATE: dict = {
26
+ "version": 1,
27
+ "auth": {
28
+ "matrx": {
29
+ "key": None,
30
+ "base_url": DEFAULT_MATRX_BASE_URL,
31
+ "app_url": DEFAULT_MATRX_APP_URL,
32
+ },
33
+ "openai": {
34
+ "key": None,
35
+ },
36
+ "anthropic": {
37
+ "key": None,
38
+ },
39
+ "claude_code": {
40
+ "oauth_token": None,
41
+ },
42
+ },
43
+ "defaults": {
44
+ "codex": None,
45
+ "claude": None,
46
+ },
47
+ "workspaces": {
48
+ "bindings": {},
49
+ },
50
+ "setup": {
51
+ "tool_sync": {
52
+ "codex": {
53
+ "provider": None,
54
+ "fingerprint": None,
55
+ "consent_granted": False,
56
+ "scope": None,
57
+ },
58
+ "claude": {
59
+ "provider": None,
60
+ "fingerprint": None,
61
+ "consent_granted": False,
62
+ "scope": None,
63
+ },
64
+ },
65
+ "tool_config": {
66
+ "codex": {
67
+ "configured": False,
68
+ "verified": False,
69
+ "config_path": None,
70
+ "backup_path": None,
71
+ "original_backup_path": None,
72
+ "config_fingerprint": None,
73
+ "matrx_key_fingerprint": None,
74
+ "last_verified_at": None,
75
+ "previous_model_provider": None,
76
+ "previous_matrx_block": None,
77
+ "previous_values": {},
78
+ },
79
+ "claude": {
80
+ "configured": False,
81
+ "verified": False,
82
+ "config_path": None,
83
+ "backup_path": None,
84
+ "original_backup_path": None,
85
+ "config_fingerprint": None,
86
+ "matrx_key_fingerprint": None,
87
+ "last_verified_at": None,
88
+ "previous_model_provider": None,
89
+ "previous_matrx_block": None,
90
+ "previous_values": {},
91
+ },
92
+ },
93
+ },
94
+ }
95
+
96
+
97
+ def config_dir() -> Path:
98
+ override = os.environ.get(CONFIG_ENV_VAR)
99
+ if override:
100
+ return Path(override).expanduser()
101
+ appdata = os.environ.get("APPDATA")
102
+ if appdata:
103
+ return Path(appdata) / "mtrx"
104
+ return Path.home() / ".config" / "mtrx"
105
+
106
+
107
+ def config_path() -> Path:
108
+ return config_dir() / CONFIG_FILENAME
109
+
110
+
111
+ def load_state() -> dict:
112
+ path = config_path()
113
+ state = deepcopy(DEFAULT_STATE)
114
+ if not path.exists():
115
+ return state
116
+ try:
117
+ raw = json.loads(path.read_text(encoding="utf-8"))
118
+ except (json.JSONDecodeError, OSError):
119
+ return state
120
+ _merge_dicts(state, raw if isinstance(raw, dict) else {})
121
+ _normalize_state(state)
122
+ return state
123
+
124
+
125
+ def save_state(state: dict) -> Path:
126
+ path = config_path()
127
+ path.parent.mkdir(parents=True, exist_ok=True)
128
+ path.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
129
+ try:
130
+ os.chmod(path, 0o600)
131
+ except OSError:
132
+ pass
133
+ return path
134
+
135
+
136
+ def mask_secret(value: str | None) -> str:
137
+ if not value:
138
+ return "not set"
139
+ if len(value) <= 8:
140
+ return "*" * len(value)
141
+ return f"{value[:4]}...{value[-4:]}"
142
+
143
+
144
+ def ensure_v1_url(base_url: str | None) -> str:
145
+ cleaned = _normalize_base_url(base_url).rstrip("/")
146
+ if cleaned.endswith("/v1"):
147
+ return cleaned
148
+ return f"{cleaned}/v1"
149
+
150
+
151
+ def ensure_root_url(base_url: str | None) -> str:
152
+ cleaned = _normalize_base_url(base_url).rstrip("/")
153
+ if cleaned.endswith("/v1"):
154
+ return cleaned[:-3]
155
+ return cleaned
156
+
157
+
158
+ def ensure_app_url(app_url: str | None, *, base_url: str | None = None) -> str:
159
+ explicit = (app_url or "").strip().rstrip("/")
160
+ if explicit:
161
+ normalized = _normalize_known_app_url(explicit, base_url=base_url)
162
+ if normalized:
163
+ return normalized
164
+ return explicit
165
+
166
+ parsed = urlparse(ensure_root_url(base_url))
167
+ scheme = parsed.scheme or "https"
168
+ host = parsed.hostname or ""
169
+
170
+ if host in {"127.0.0.1", "localhost"}:
171
+ return urlunparse((scheme, f"{host}:5173", "", "", "", ""))
172
+
173
+ if host in PUBLIC_MATRX_APP_HOSTS_BY_API_HOST:
174
+ host = PUBLIC_MATRX_APP_HOSTS_BY_API_HOST[host]
175
+ if host.startswith("api."):
176
+ host = host[4:]
177
+
178
+ if not host:
179
+ return DEFAULT_MATRX_APP_URL
180
+
181
+ if parsed.port and parsed.port not in {80, 443}:
182
+ netloc = f"{host}:{parsed.port}"
183
+ else:
184
+ netloc = host
185
+ return urlunparse((scheme, netloc, "", "", "", ""))
186
+
187
+
188
+ def resolve_workspace_root(cwd: str | os.PathLike[str] | None = None) -> str:
189
+ root = Path(cwd or os.environ.get("PWD") or os.getcwd()).expanduser().resolve()
190
+ try:
191
+ result = subprocess.run(
192
+ ["git", "-C", str(root), "rev-parse", "--show-toplevel"],
193
+ capture_output=True,
194
+ text=True,
195
+ timeout=2,
196
+ check=False,
197
+ )
198
+ if result.returncode == 0:
199
+ git_root = result.stdout.strip()
200
+ if git_root:
201
+ return str(Path(git_root).expanduser().resolve())
202
+ except (OSError, ValueError, subprocess.SubprocessError):
203
+ pass
204
+ return str(root)
205
+
206
+
207
+ def get_workspace_binding(
208
+ state: dict,
209
+ *,
210
+ cwd: str | os.PathLike[str] | None = None,
211
+ ) -> dict | None:
212
+ root = resolve_workspace_root(cwd)
213
+ bindings = state.setdefault("workspaces", {}).setdefault("bindings", {})
214
+ binding = bindings.get(root)
215
+ if not isinstance(binding, dict):
216
+ return None
217
+ if not any(binding.get(field) for field in ("matrx_key", "project_id", "group_id")):
218
+ return None
219
+ return deepcopy(binding)
220
+
221
+
222
+ def set_workspace_binding(
223
+ state: dict,
224
+ *,
225
+ cwd: str | os.PathLike[str] | None = None,
226
+ matrx_key: str | None = None,
227
+ project_id: str | None = None,
228
+ group_id: str | None = None,
229
+ ) -> bool:
230
+ root = resolve_workspace_root(cwd)
231
+ bindings = state.setdefault("workspaces", {}).setdefault("bindings", {})
232
+ existing = deepcopy(bindings.get(root) if isinstance(bindings.get(root), dict) else {})
233
+ updated = deepcopy(existing)
234
+
235
+ for field, raw_value in (
236
+ ("matrx_key", matrx_key),
237
+ ("project_id", project_id),
238
+ ("group_id", group_id),
239
+ ):
240
+ if raw_value is None:
241
+ continue
242
+ cleaned = raw_value.strip()
243
+ if cleaned:
244
+ updated[field] = cleaned
245
+ else:
246
+ updated.pop(field, None)
247
+
248
+ if not any(updated.get(field) for field in ("matrx_key", "project_id", "group_id")):
249
+ if root in bindings:
250
+ bindings.pop(root, None)
251
+ return True
252
+ return False
253
+
254
+ if updated == existing:
255
+ return False
256
+
257
+ bindings[root] = updated
258
+ return True
259
+
260
+
261
+ def _merge_dicts(target: dict, source: dict) -> None:
262
+ for key, value in source.items():
263
+ if key in target and isinstance(target[key], dict) and isinstance(value, dict):
264
+ _merge_dicts(target[key], value)
265
+ else:
266
+ target[key] = value
267
+
268
+
269
+ def _normalize_state(state: dict) -> None:
270
+ auth = state.setdefault("auth", {}).setdefault("matrx", {})
271
+ auth["base_url"] = _normalize_base_url(auth.get("base_url"))
272
+ auth["app_url"] = ensure_app_url(
273
+ auth.get("app_url"),
274
+ base_url=auth.get("base_url"),
275
+ )
276
+
277
+
278
+ def _normalize_base_url(base_url: str | None) -> str:
279
+ cleaned = (base_url or DEFAULT_MATRX_BASE_URL).strip().rstrip("/")
280
+ return LEGACY_MATRX_BASE_URLS.get(cleaned, cleaned)
281
+
282
+
283
+ def _normalize_known_app_url(app_url: str, *, base_url: str | None = None) -> str | None:
284
+ if app_url not in LEGACY_MATRX_APP_URLS:
285
+ return None
286
+
287
+ parsed = urlparse(ensure_root_url(base_url))
288
+ host = parsed.hostname or ""
289
+ if not host or host in PUBLIC_MATRX_APP_HOSTS_BY_API_HOST:
290
+ return DEFAULT_MATRX_APP_URL
291
+ return None