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 +1 -1
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/launcher.py +20 -18
- package/src/matrx/cli/main.py +9 -91
package/package.json
CHANGED
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
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
|
-
|
|
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"] =
|
|
495
|
-
env
|
|
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 =
|
|
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 =
|
|
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: {
|
|
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",
|
package/src/matrx/cli/main.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
359
|
-
if not _prompt_yes_no("
|
|
360
|
-
raise ValueError("Claude
|
|
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
|
-
|
|
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
|
|