mtrx-cli 0.1.3 → 0.1.5
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 +1 -1
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/main.py +110 -1
package/package.json
CHANGED
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.5"
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
9
|
import threading
|
|
10
|
+
import time
|
|
10
11
|
import urllib.parse
|
|
11
12
|
import webbrowser
|
|
12
13
|
|
|
@@ -262,6 +263,107 @@ def _complete_codex_login() -> None:
|
|
|
262
263
|
raise ValueError("Codex login did not complete successfully")
|
|
263
264
|
|
|
264
265
|
|
|
266
|
+
def _resolve_matrx_launch_key(state: dict, env: dict[str, str] | None = None) -> str:
|
|
267
|
+
env = env or os.environ
|
|
268
|
+
env_key = (env.get("MTRX_KEY") or "").strip()
|
|
269
|
+
if env_key:
|
|
270
|
+
return env_key
|
|
271
|
+
workspace_binding = get_workspace_binding(state, cwd=env.get("PWD") or os.getcwd()) or {}
|
|
272
|
+
workspace_key = (workspace_binding.get("matrx_key") or "").strip()
|
|
273
|
+
if workspace_key:
|
|
274
|
+
return workspace_key
|
|
275
|
+
return (state.get("auth", {}).get("matrx", {}).get("key") or "").strip()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _matrx_request_context(state: dict) -> tuple[str, dict[str, str]]:
|
|
279
|
+
mx_key = _resolve_matrx_launch_key(state)
|
|
280
|
+
if not mx_key.startswith("mx_"):
|
|
281
|
+
raise ValueError("Matrx login required before connecting Claude Code")
|
|
282
|
+
base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
|
|
283
|
+
return base_url, {"X-Matrx-Key": mx_key}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _list_matrx_subscriptions(state: dict) -> list[dict]:
|
|
287
|
+
base_url, headers = _matrx_request_context(state)
|
|
288
|
+
try:
|
|
289
|
+
with httpx.Client(timeout=15) as client:
|
|
290
|
+
response = client.get(f"{base_url.rstrip('/')}/v1/subscriptions", headers=headers)
|
|
291
|
+
response.raise_for_status()
|
|
292
|
+
except httpx.HTTPStatusError as exc:
|
|
293
|
+
detail = exc.response.text.strip() or f"HTTP {exc.response.status_code}"
|
|
294
|
+
raise ValueError(f"Could not query Matrx subscriptions: {detail}") from exc
|
|
295
|
+
except httpx.HTTPError as exc:
|
|
296
|
+
raise ValueError(f"Could not query Matrx subscriptions: {exc}") from exc
|
|
297
|
+
|
|
298
|
+
payload = response.json()
|
|
299
|
+
subscriptions = payload.get("subscriptions")
|
|
300
|
+
if not isinstance(subscriptions, list):
|
|
301
|
+
return []
|
|
302
|
+
return [entry for entry in subscriptions if isinstance(entry, dict)]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _has_server_claude_subscription(state: dict) -> bool:
|
|
306
|
+
try:
|
|
307
|
+
subscriptions = _list_matrx_subscriptions(state)
|
|
308
|
+
except ValueError:
|
|
309
|
+
return False
|
|
310
|
+
return any((entry.get("provider") or "").strip() == "claude_code" for entry in subscriptions)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _run_claude_subscription_browser_login(state: dict) -> None:
|
|
314
|
+
base_url, headers = _matrx_request_context(state)
|
|
315
|
+
try:
|
|
316
|
+
with httpx.Client(timeout=15) as client:
|
|
317
|
+
response = client.get(
|
|
318
|
+
f"{base_url.rstrip('/')}/v1/subscriptions/claude-code/authorize",
|
|
319
|
+
headers=headers,
|
|
320
|
+
)
|
|
321
|
+
response.raise_for_status()
|
|
322
|
+
except httpx.HTTPStatusError as exc:
|
|
323
|
+
detail = exc.response.text.strip() or f"HTTP {exc.response.status_code}"
|
|
324
|
+
raise ValueError(f"Could not start Claude Code connection: {detail}") from exc
|
|
325
|
+
except httpx.HTTPError as exc:
|
|
326
|
+
raise ValueError(f"Could not start Claude Code connection: {exc}") from exc
|
|
327
|
+
|
|
328
|
+
payload = response.json()
|
|
329
|
+
auth_url = (payload.get("auth_url") or "").strip()
|
|
330
|
+
if not auth_url:
|
|
331
|
+
raise ValueError("Matrx did not return a Claude Code authorization URL")
|
|
332
|
+
|
|
333
|
+
print(f"Open this URL to connect Claude Code to Matrx:\n {auth_url}")
|
|
334
|
+
webbrowser.open(auth_url)
|
|
335
|
+
|
|
336
|
+
deadline = time.monotonic() + 300
|
|
337
|
+
while time.monotonic() < deadline:
|
|
338
|
+
if _has_server_claude_subscription(state):
|
|
339
|
+
return
|
|
340
|
+
time.sleep(2)
|
|
341
|
+
|
|
342
|
+
raise ValueError("Claude Code connection timed out")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _complete_claude_login(state: dict) -> tuple[dict, bool]:
|
|
346
|
+
token = (read_claude_oauth_token() or "").strip()
|
|
347
|
+
imported = (state.get("auth", {}).get("claude_code", {}).get("oauth_token") or "").strip()
|
|
348
|
+
if token or imported:
|
|
349
|
+
return state, False
|
|
350
|
+
if _has_server_claude_subscription(state):
|
|
351
|
+
return state, False
|
|
352
|
+
if not _is_interactive_terminal():
|
|
353
|
+
raise ValueError(
|
|
354
|
+
"Claude provider connection required. "
|
|
355
|
+
"Run `mtrx login claude-code --import` or rerun interactively to connect in the browser"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
print("Claude provider connection required.")
|
|
359
|
+
if not _prompt_yes_no("Open the browser-based Claude Code connection flow now?", default=True):
|
|
360
|
+
raise ValueError("Claude connection cancelled")
|
|
361
|
+
|
|
362
|
+
_run_claude_subscription_browser_login(state)
|
|
363
|
+
|
|
364
|
+
return state, False
|
|
365
|
+
|
|
366
|
+
|
|
265
367
|
def _maybe_promote_direct_route(state: dict, tool: str, route: str | None) -> tuple[str | None, bool]:
|
|
266
368
|
if route is not None:
|
|
267
369
|
return route, False
|
|
@@ -290,7 +392,11 @@ def _cmd_login(args) -> int:
|
|
|
290
392
|
token = read_claude_oauth_token()
|
|
291
393
|
if not token:
|
|
292
394
|
print(
|
|
293
|
-
|
|
395
|
+
(
|
|
396
|
+
f"No Claude OAuth token found at {claude_credentials_path()}. "
|
|
397
|
+
"If your Claude install no longer writes that file, use the browser-based "
|
|
398
|
+
"`mtrx claude` flow instead, or run `claude setup-token` and import that token."
|
|
399
|
+
),
|
|
294
400
|
file=sys.stderr,
|
|
295
401
|
)
|
|
296
402
|
return 1
|
|
@@ -569,6 +675,9 @@ def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
|
569
675
|
auth_changed = auth_changed or login_changed
|
|
570
676
|
if tool == "codex":
|
|
571
677
|
_complete_codex_login()
|
|
678
|
+
if tool == "claude":
|
|
679
|
+
state, login_changed = _complete_claude_login(state)
|
|
680
|
+
auth_changed = auth_changed or login_changed
|
|
572
681
|
initialized = initialize_first_launch_route(state, tool, route)
|
|
573
682
|
state, changed = prepare_routed_setup(
|
|
574
683
|
state,
|