mtrx-cli 0.1.5 → 0.1.7

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.5",
3
+ "version": "0.1.7",
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.5"
1
+ __version__ = "0.1.7"
@@ -170,13 +170,6 @@ def prepare_routed_setup(
170
170
  changed = True
171
171
  if route == "matrx" and _persist_workspace_binding_from_env(state, env):
172
172
  changed = True
173
- if route == "matrx" and tool == "claude":
174
- mx_key, _ = _resolve_matrx_route_key(state, env)
175
- if _approve_claude_custom_api_key(mx_key):
176
- changed = True
177
- if _sync_claude_subscription_to_matrx(state, env):
178
- changed = True
179
-
180
173
  if _sync_tool_route_config(state, tool=tool, route=route):
181
174
  changed = True
182
175
 
@@ -221,7 +214,16 @@ def codex_auth_path() -> Path:
221
214
 
222
215
  def claude_oauth_available() -> bool:
223
216
  token = read_claude_oauth_token()
224
- return bool(token)
217
+ if token:
218
+ return True
219
+ state = _read_claude_app_state()
220
+ oauth_account = state.get("oauthAccount")
221
+ if not isinstance(oauth_account, dict):
222
+ return False
223
+ return bool(
224
+ (oauth_account.get("accountUuid") or "").strip()
225
+ or (oauth_account.get("emailAddress") or "").strip()
226
+ )
225
227
 
226
228
 
227
229
  def read_claude_oauth_token() -> str | None:
@@ -491,8 +493,8 @@ def _build_claude_env(
491
493
  env.pop("MATRX_CLAUDE_MODE", None)
492
494
  env["MATRX_BASE_URL"] = proxy_root
493
495
  env["MATRX_API_KEY"] = mx_key
494
- env["ANTHROPIC_BASE_URL"] = proxy_base
495
- env["ANTHROPIC_API_KEY"] = mx_key
496
+ env["ANTHROPIC_BASE_URL"] = proxy_root
497
+ env.pop("ANTHROPIC_API_KEY", None)
496
498
  group_id, project_id = _resolve_matrx_context_overrides(state, env)
497
499
  session_id = str(uuid.uuid4())
498
500
  # Evolutionary scaffolding: env snapshot for AI context injection
@@ -500,6 +502,7 @@ def _build_claude_env(
500
502
  env_b64 = base64.b64encode(json.dumps(env_snap).encode()).decode() if env_snap else ""
501
503
  custom_headers = "\n".join(
502
504
  [
505
+ f"x-matrx-key: {mx_key}",
503
506
  "x-matrx-agent-id: claude-cli",
504
507
  "x-matrx-provider: claude_code",
505
508
  f"x-matrx-session-id: {session_id}",
@@ -558,7 +561,7 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
558
561
  return
559
562
 
560
563
  base_url = (plan.env.get("ANTHROPIC_BASE_URL") or "").strip()
561
- expected_base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
564
+ expected_base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
562
565
  if not base_url:
563
566
  raise ValueError("Claude Matrx route is missing ANTHROPIC_BASE_URL")
564
567
  if base_url != expected_base_url:
@@ -571,14 +574,12 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
571
574
  if not mx_key.startswith("mx_"):
572
575
  raise ValueError("Claude Matrx route is missing a valid MATRX_API_KEY")
573
576
 
574
- anthropic_key = (plan.env.get("ANTHROPIC_API_KEY") or "").strip()
575
- if anthropic_key != mx_key:
576
- raise ValueError("Claude Matrx route must set ANTHROPIC_API_KEY to the same mx_ key as MATRX_API_KEY")
577
-
578
577
  custom_headers = (plan.env.get("ANTHROPIC_CUSTOM_HEADERS") or "").strip()
579
578
  if not custom_headers:
580
579
  raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS")
581
580
  lowered_headers = custom_headers.lower()
581
+ if "x-matrx-key:" not in lowered_headers:
582
+ raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Key")
582
583
  if "x-matrx-session-id:" not in lowered_headers:
583
584
  raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Session-Id")
584
585
  if "x-matrx-provider: claude_code" not in lowered_headers:
@@ -588,6 +589,8 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
588
589
 
589
590
  if plan.env.get("ANTHROPIC_AUTH_TOKEN"):
590
591
  raise ValueError("Claude Matrx route should not set ANTHROPIC_AUTH_TOKEN")
592
+ if plan.env.get("ANTHROPIC_API_KEY"):
593
+ raise ValueError("Claude Matrx route should not set ANTHROPIC_API_KEY")
591
594
 
592
595
 
593
596
  def _validate_codex_launch_plan(plan: LaunchPlan, state: dict) -> None:
@@ -637,15 +640,14 @@ def describe_launch_plan(plan: LaunchPlan, state: dict) -> list[str]:
637
640
  ]
638
641
 
639
642
  if plan.tool == "claude":
640
- base_url = ensure_v1_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
643
+ base_url = ensure_root_url(state.get("auth", {}).get("matrx", {}).get("base_url"))
641
644
  custom_headers = (plan.env.get("ANTHROPIC_CUSTOM_HEADERS") or "").strip()
642
- anthropic_key = (plan.env.get("ANTHROPIC_API_KEY") or "").strip()
643
645
  return [
644
646
  "Launching claude via Matrx",
645
647
  f" base_url: {base_url or DEFAULT_MATRX_BASE_URL}",
646
648
  f" auth_source: {plan.auth_source}",
647
649
  f" custom_headers_present: {bool(custom_headers)}",
648
- f" proxy_key_present: {anthropic_key.startswith('mx_')}",
650
+ f" proxy_key_present: {'x-matrx-key:' in custom_headers.lower()}",
649
651
  f" subscription_token_available: {bool(_resolve_claude_subscription_token(state))}",
650
652
  " runtime_route: env injection",
651
653
  " persistent_route: disabled",
@@ -7,7 +7,6 @@ import os
7
7
  import subprocess
8
8
  import sys
9
9
  import threading
10
- import time
11
10
  import urllib.parse
12
11
  import webbrowser
13
12
 
@@ -263,103 +262,22 @@ def _complete_codex_login() -> None:
263
262
  raise ValueError("Codex login did not complete successfully")
264
263
 
265
264
 
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
265
  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):
266
+ if claude_oauth_available():
351
267
  return state, False
352
268
  if not _is_interactive_terminal():
353
269
  raise ValueError(
354
- "Claude provider connection required. "
355
- "Run `mtrx login claude-code --import` or rerun interactively to connect in the browser"
270
+ "Claude login required. Run: claude auth login"
356
271
  )
357
272
 
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")
273
+ print("Claude login required.")
274
+ if not _prompt_yes_no("Run `claude auth login` now?", default=True):
275
+ raise ValueError("Claude login cancelled")
361
276
 
362
- _run_claude_subscription_browser_login(state)
277
+ executable = find_executable("claude") or "claude"
278
+ result = subprocess.run([executable, "auth", "login"], check=False)
279
+ if result.returncode != 0 or not claude_oauth_available():
280
+ raise ValueError("Claude login did not complete successfully")
363
281
 
364
282
  return state, False
365
283