mtrx-cli 0.1.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrx-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "MATRX CLI for routing Codex and Claude through Matrx",
5
5
  "homepage": "https://mtrx.so",
6
6
  "repository": {
@@ -1 +1 @@
1
- __version__ = "0.1.4"
1
+ __version__ = "0.1.5"
@@ -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,22 +263,103 @@ 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
+
265
345
  def _complete_claude_login(state: dict) -> tuple[dict, bool]:
266
346
  token = (read_claude_oauth_token() or "").strip()
267
- if token:
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):
268
351
  return state, False
269
352
  if not _is_interactive_terminal():
270
- raise ValueError("Claude login required. Run: claude auth login")
353
+ raise ValueError(
354
+ "Claude provider connection required. "
355
+ "Run `mtrx login claude-code --import` or rerun interactively to connect in the browser"
356
+ )
271
357
 
272
- print("Claude login required.")
273
- if not _prompt_yes_no("Run `claude auth login` now?", default=True):
274
- raise ValueError("Claude login cancelled")
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")
275
361
 
276
- executable = find_executable("claude") or "claude"
277
- result = subprocess.run([executable, "auth", "login"], check=False)
278
- token = (read_claude_oauth_token() or "").strip()
279
- if result.returncode != 0 or not token:
280
- raise ValueError("Claude login did not complete successfully")
362
+ _run_claude_subscription_browser_login(state)
281
363
 
282
364
  return state, False
283
365
 
@@ -310,7 +392,11 @@ def _cmd_login(args) -> int:
310
392
  token = read_claude_oauth_token()
311
393
  if not token:
312
394
  print(
313
- f"No Claude OAuth token found at {claude_credentials_path()}",
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
+ ),
314
400
  file=sys.stderr,
315
401
  )
316
402
  return 1