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.
- package/README.md +32 -0
- package/bin/mtrx.js +111 -0
- package/package.json +34 -0
- package/src/matrx/__init__.py +1 -0
- package/src/matrx/cli/__init__.py +2 -0
- package/src/matrx/cli/launcher.py +796 -0
- package/src/matrx/cli/main.py +510 -0
- package/src/matrx/cli/state.py +291 -0
|
@@ -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
|