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,510 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import html
|
|
5
|
+
import http.server
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import webbrowser
|
|
12
|
+
|
|
13
|
+
from matrx.cli.launcher import (
|
|
14
|
+
prepare_routed_setup,
|
|
15
|
+
build_launch_plan,
|
|
16
|
+
claude_credentials_path,
|
|
17
|
+
claude_oauth_available,
|
|
18
|
+
configured_route,
|
|
19
|
+
describe_launch_plan,
|
|
20
|
+
find_executable,
|
|
21
|
+
get_tool_config_status,
|
|
22
|
+
initialize_first_launch_route,
|
|
23
|
+
launch,
|
|
24
|
+
read_claude_oauth_token,
|
|
25
|
+
read_codex_access_token,
|
|
26
|
+
resolve_route,
|
|
27
|
+
validate_launch_plan,
|
|
28
|
+
)
|
|
29
|
+
from matrx.cli.state import (
|
|
30
|
+
ensure_app_url,
|
|
31
|
+
ensure_root_url,
|
|
32
|
+
ensure_v1_url,
|
|
33
|
+
get_workspace_binding,
|
|
34
|
+
load_state,
|
|
35
|
+
mask_secret,
|
|
36
|
+
save_state,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv: list[str] | None = None) -> int:
|
|
41
|
+
parser = _build_parser()
|
|
42
|
+
args, remainder = parser.parse_known_args(argv)
|
|
43
|
+
|
|
44
|
+
if args.command == "help":
|
|
45
|
+
parser.print_help()
|
|
46
|
+
return 0
|
|
47
|
+
if args.command == "login":
|
|
48
|
+
return _cmd_login(args)
|
|
49
|
+
if args.command == "use":
|
|
50
|
+
return _cmd_use(args)
|
|
51
|
+
if args.command == "status":
|
|
52
|
+
return _cmd_status()
|
|
53
|
+
if args.command == "doctor":
|
|
54
|
+
return _cmd_doctor()
|
|
55
|
+
if args.command in {"codex", "claude"}:
|
|
56
|
+
return _cmd_launch(args.command, args.route, remainder)
|
|
57
|
+
|
|
58
|
+
parser.print_help()
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
63
|
+
parser = argparse.ArgumentParser(prog="mtrx")
|
|
64
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
65
|
+
|
|
66
|
+
login = subparsers.add_parser("login")
|
|
67
|
+
login.add_argument("provider", choices=["matrx", "openai", "anthropic", "claude-code"])
|
|
68
|
+
login.add_argument("--key")
|
|
69
|
+
login.add_argument("--base-url")
|
|
70
|
+
login.add_argument("--app-url")
|
|
71
|
+
login.add_argument("--web", action="store_true")
|
|
72
|
+
login.add_argument("--import", dest="do_import", action="store_true")
|
|
73
|
+
|
|
74
|
+
use = subparsers.add_parser("use")
|
|
75
|
+
use.add_argument("tool", choices=["codex", "claude"])
|
|
76
|
+
use.add_argument("route", choices=["direct", "matrx"])
|
|
77
|
+
|
|
78
|
+
subparsers.add_parser("help")
|
|
79
|
+
subparsers.add_parser("status")
|
|
80
|
+
subparsers.add_parser("doctor")
|
|
81
|
+
|
|
82
|
+
codex = subparsers.add_parser("codex")
|
|
83
|
+
codex.add_argument("--route", choices=["direct", "matrx"])
|
|
84
|
+
|
|
85
|
+
claude = subparsers.add_parser("claude")
|
|
86
|
+
claude.add_argument("--route", choices=["direct", "matrx"])
|
|
87
|
+
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_interactive_terminal() -> bool:
|
|
92
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _prompt_yes_no(prompt: str, *, default: bool = True) -> bool:
|
|
96
|
+
if not _is_interactive_terminal():
|
|
97
|
+
return default
|
|
98
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
99
|
+
answer = input(f"{prompt} {suffix} ").strip().lower()
|
|
100
|
+
if not answer:
|
|
101
|
+
return default
|
|
102
|
+
return answer in {"y", "yes"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _prompt_for_secret(prompt: str) -> str:
|
|
106
|
+
if not _is_interactive_terminal():
|
|
107
|
+
return ""
|
|
108
|
+
return input(f"{prompt}: ").strip()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _derive_callback_payload(params: dict[str, list[str]]) -> dict[str, str | None]:
|
|
112
|
+
def _one(name: str) -> str | None:
|
|
113
|
+
values = params.get(name) or []
|
|
114
|
+
if not values:
|
|
115
|
+
return None
|
|
116
|
+
value = values[0].strip()
|
|
117
|
+
return value or None
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"state": _one("state"),
|
|
121
|
+
"matrx_key": _one("matrx_key"),
|
|
122
|
+
"org_id": _one("org_id"),
|
|
123
|
+
"project_id": _one("project_id"),
|
|
124
|
+
"error": _one("error"),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _matrx_browser_callback_html(payload: dict[str, str | None]) -> bytes:
|
|
129
|
+
error = (payload.get("error") or "").strip()
|
|
130
|
+
if error:
|
|
131
|
+
escaped_error = html.escape(error)
|
|
132
|
+
body = (
|
|
133
|
+
"<html><body><h1>Matrx login failed.</h1>"
|
|
134
|
+
"<p>The browser-based handoff returned an error.</p>"
|
|
135
|
+
f"<pre>{escaped_error}</pre>"
|
|
136
|
+
"<p>You can close this window and return to the terminal.</p>"
|
|
137
|
+
"</body></html>"
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
body = (
|
|
141
|
+
"<html><body><h1>Matrx login complete.</h1>"
|
|
142
|
+
"<p>You can close this window and return to the terminal.</p>"
|
|
143
|
+
"</body></html>"
|
|
144
|
+
)
|
|
145
|
+
return body.encode("utf-8")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _run_matrx_browser_login(
|
|
149
|
+
state: dict,
|
|
150
|
+
*,
|
|
151
|
+
requested_org_id: str | None = None,
|
|
152
|
+
requested_project_id: str | None = None,
|
|
153
|
+
) -> dict[str, str | None]:
|
|
154
|
+
auth_cfg = state.setdefault("auth", {}).setdefault("matrx", {})
|
|
155
|
+
api_root = ensure_root_url(auth_cfg.get("base_url"))
|
|
156
|
+
app_url = ensure_app_url(auth_cfg.get("app_url"), base_url=auth_cfg.get("base_url"))
|
|
157
|
+
nonce = os.urandom(12).hex()
|
|
158
|
+
result: dict[str, str | None] = {}
|
|
159
|
+
event = threading.Event()
|
|
160
|
+
|
|
161
|
+
class _CallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
162
|
+
def do_GET(self): # noqa: N802
|
|
163
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
164
|
+
if parsed.path != "/callback":
|
|
165
|
+
self.send_response(404)
|
|
166
|
+
self.end_headers()
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
payload = _derive_callback_payload(urllib.parse.parse_qs(parsed.query))
|
|
170
|
+
result.update(payload)
|
|
171
|
+
event.set()
|
|
172
|
+
body = _matrx_browser_callback_html(payload)
|
|
173
|
+
self.send_response(200)
|
|
174
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
175
|
+
self.send_header("Content-Length", str(len(body)))
|
|
176
|
+
self.end_headers()
|
|
177
|
+
self.wfile.write(body)
|
|
178
|
+
|
|
179
|
+
def log_message(self, format, *args): # noqa: A003
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _CallbackHandler)
|
|
183
|
+
try:
|
|
184
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
185
|
+
thread.start()
|
|
186
|
+
callback_url = f"http://127.0.0.1:{server.server_port}/callback"
|
|
187
|
+
params = {
|
|
188
|
+
"callback": callback_url,
|
|
189
|
+
"state": nonce,
|
|
190
|
+
"api_base": api_root,
|
|
191
|
+
}
|
|
192
|
+
if requested_org_id:
|
|
193
|
+
params["org_id"] = requested_org_id
|
|
194
|
+
if requested_project_id:
|
|
195
|
+
params["project_id"] = requested_project_id
|
|
196
|
+
login_url = f"{app_url}/cli-auth?{urllib.parse.urlencode(params)}"
|
|
197
|
+
print(f"Open this URL to complete Matrx login:\n {login_url}")
|
|
198
|
+
webbrowser.open(login_url)
|
|
199
|
+
if not event.wait(timeout=300):
|
|
200
|
+
raise ValueError("Matrx web login timed out")
|
|
201
|
+
finally:
|
|
202
|
+
server.shutdown()
|
|
203
|
+
server.server_close()
|
|
204
|
+
|
|
205
|
+
if result.get("state") != nonce:
|
|
206
|
+
raise ValueError("Matrx web login returned an invalid state token")
|
|
207
|
+
if result.get("error"):
|
|
208
|
+
raise ValueError(f"Matrx web login failed: {result['error']}")
|
|
209
|
+
if not (result.get("matrx_key") or "").startswith("mx_"):
|
|
210
|
+
raise ValueError("Matrx web login did not return a valid Matrx key")
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _complete_matrx_login(state: dict) -> tuple[dict, bool]:
|
|
215
|
+
auth_cfg = state.setdefault("auth", {}).setdefault("matrx", {})
|
|
216
|
+
if _has_matrx_login(state, env=os.environ):
|
|
217
|
+
return state, False
|
|
218
|
+
if not _is_interactive_terminal():
|
|
219
|
+
raise ValueError("Login required. Run: mtrx login matrx --key mx_... or set MTRX_KEY")
|
|
220
|
+
|
|
221
|
+
print("Matrx login required.")
|
|
222
|
+
if _prompt_yes_no("Open the browser-based Matrx sign-in flow now?", default=True):
|
|
223
|
+
session = _run_matrx_browser_login(state)
|
|
224
|
+
auth_cfg["key"] = session["matrx_key"]
|
|
225
|
+
return state, True
|
|
226
|
+
|
|
227
|
+
manual_key = _prompt_for_secret("Paste your Matrx key")
|
|
228
|
+
if not manual_key:
|
|
229
|
+
raise ValueError("Matrx login cancelled")
|
|
230
|
+
if not manual_key.startswith("mx_"):
|
|
231
|
+
raise ValueError("Matrx key must start with mx_")
|
|
232
|
+
auth_cfg["key"] = manual_key
|
|
233
|
+
return state, True
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _complete_codex_login() -> None:
|
|
237
|
+
if read_codex_access_token():
|
|
238
|
+
return
|
|
239
|
+
if not _is_interactive_terminal():
|
|
240
|
+
raise ValueError("Codex login required. Run: codex login")
|
|
241
|
+
print("Codex login required.")
|
|
242
|
+
if not _prompt_yes_no("Run `codex login` now?", default=True):
|
|
243
|
+
raise ValueError("Codex login cancelled")
|
|
244
|
+
executable = find_executable("codex") or "codex"
|
|
245
|
+
result = subprocess.run([executable, "login"], check=False)
|
|
246
|
+
if result.returncode != 0 or not read_codex_access_token():
|
|
247
|
+
raise ValueError("Codex login did not complete successfully")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _maybe_promote_direct_route(state: dict, tool: str, route: str | None) -> tuple[str | None, bool]:
|
|
251
|
+
if route is not None:
|
|
252
|
+
return route, False
|
|
253
|
+
if configured_route(state, tool) != "direct":
|
|
254
|
+
return route, False
|
|
255
|
+
if _has_matrx_login(state, env=os.environ):
|
|
256
|
+
return route, False
|
|
257
|
+
if not _is_interactive_terminal():
|
|
258
|
+
return route, False
|
|
259
|
+
|
|
260
|
+
print(f"`mtrx {tool}` is currently configured to use the direct route.")
|
|
261
|
+
if _prompt_yes_no(f"Switch {tool} to the Matrx route and continue with Matrx sign-in?", default=True):
|
|
262
|
+
state.setdefault("defaults", {})[tool] = "matrx"
|
|
263
|
+
return "matrx", True
|
|
264
|
+
return route, False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _cmd_login(args) -> int:
|
|
268
|
+
state = load_state()
|
|
269
|
+
provider = args.provider.replace("-", "_")
|
|
270
|
+
|
|
271
|
+
if provider == "claude_code":
|
|
272
|
+
if not args.do_import:
|
|
273
|
+
print("Use: mtrx login claude-code --import", file=sys.stderr)
|
|
274
|
+
return 1
|
|
275
|
+
token = read_claude_oauth_token()
|
|
276
|
+
if not token:
|
|
277
|
+
print(
|
|
278
|
+
f"No Claude OAuth token found at {claude_credentials_path()}",
|
|
279
|
+
file=sys.stderr,
|
|
280
|
+
)
|
|
281
|
+
return 1
|
|
282
|
+
state["auth"]["claude_code"]["oauth_token"] = token
|
|
283
|
+
path = save_state(state)
|
|
284
|
+
print(f"Saved Claude Code OAuth token metadata to {path}")
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
key = (args.key or "").strip()
|
|
288
|
+
if provider == "matrx":
|
|
289
|
+
if args.base_url:
|
|
290
|
+
state["auth"]["matrx"]["base_url"] = args.base_url.strip()
|
|
291
|
+
if args.app_url:
|
|
292
|
+
state["auth"]["matrx"]["app_url"] = args.app_url.strip()
|
|
293
|
+
elif not state["auth"]["matrx"].get("app_url"):
|
|
294
|
+
state["auth"]["matrx"]["app_url"] = ensure_app_url(
|
|
295
|
+
None,
|
|
296
|
+
base_url=state["auth"]["matrx"].get("base_url"),
|
|
297
|
+
)
|
|
298
|
+
if not key:
|
|
299
|
+
if not args.web and not _is_interactive_terminal():
|
|
300
|
+
print("--key is required for non-interactive Matrx login", file=sys.stderr)
|
|
301
|
+
return 1
|
|
302
|
+
try:
|
|
303
|
+
state, changed = _complete_matrx_login(state)
|
|
304
|
+
except ValueError as exc:
|
|
305
|
+
print(str(exc), file=sys.stderr)
|
|
306
|
+
return 1
|
|
307
|
+
if changed:
|
|
308
|
+
path = save_state(state)
|
|
309
|
+
print(f"Saved {args.provider} credentials to {path}")
|
|
310
|
+
print(f"Matrx base URL: {ensure_v1_url(state['auth']['matrx']['base_url'])}")
|
|
311
|
+
return 0
|
|
312
|
+
print("Matrx login did not change the current state", file=sys.stderr)
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
if not key:
|
|
316
|
+
print("--key is required for this login command", file=sys.stderr)
|
|
317
|
+
return 1
|
|
318
|
+
|
|
319
|
+
if provider == "matrx" and not key.startswith("mx_"):
|
|
320
|
+
print("Matrx key must start with mx_", file=sys.stderr)
|
|
321
|
+
return 1
|
|
322
|
+
|
|
323
|
+
state["auth"][provider]["key"] = key
|
|
324
|
+
path = save_state(state)
|
|
325
|
+
print(f"Saved {args.provider} credentials to {path}")
|
|
326
|
+
if provider == "matrx":
|
|
327
|
+
print(f"Matrx base URL: {ensure_v1_url(state['auth']['matrx']['base_url'])}")
|
|
328
|
+
return 0
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _cmd_use(args) -> int:
|
|
332
|
+
state = load_state()
|
|
333
|
+
if not _has_matrx_login(state, env=os.environ):
|
|
334
|
+
print("Login required. Run: mtrx login matrx --key mx_...", file=sys.stderr)
|
|
335
|
+
return 1
|
|
336
|
+
state["defaults"][args.tool] = args.route
|
|
337
|
+
path = save_state(state)
|
|
338
|
+
print(f"Default route for {args.tool}: {args.route}")
|
|
339
|
+
print(f"Saved to {path}")
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _cmd_status() -> int:
|
|
344
|
+
state = load_state()
|
|
345
|
+
auth = state["auth"]
|
|
346
|
+
print("Defaults:")
|
|
347
|
+
print(f" codex: {_default_route_label(configured_route(state, 'codex'))}")
|
|
348
|
+
print(f" claude: {_default_route_label(configured_route(state, 'claude'))}")
|
|
349
|
+
print("Auth:")
|
|
350
|
+
print(
|
|
351
|
+
" matrx: "
|
|
352
|
+
f"{mask_secret(auth['matrx'].get('key'))} "
|
|
353
|
+
f"({ensure_v1_url(auth['matrx'].get('base_url'))})"
|
|
354
|
+
)
|
|
355
|
+
print(f" matrx app: {ensure_app_url(auth['matrx'].get('app_url'), base_url=auth['matrx'].get('base_url'))}")
|
|
356
|
+
print(f" openai: {mask_secret(auth['openai'].get('key'))}")
|
|
357
|
+
print(f" anthropic: {mask_secret(auth['anthropic'].get('key'))}")
|
|
358
|
+
imported = "imported" if auth["claude_code"].get("oauth_token") else "not imported"
|
|
359
|
+
local = "present" if claude_oauth_available() else "missing"
|
|
360
|
+
print(f" claude-code oauth: {imported}, local credentials: {local}")
|
|
361
|
+
print("Tool config:")
|
|
362
|
+
for tool in ("codex", "claude"):
|
|
363
|
+
config_status = get_tool_config_status(state, tool)
|
|
364
|
+
route = configured_route(state, tool)
|
|
365
|
+
if tool == "codex" and config_status["verified"] and not config_status["configured"]:
|
|
366
|
+
status_label = "runtime-only" if route == "matrx" else "clean"
|
|
367
|
+
elif config_status["configured"] and config_status["verified"]:
|
|
368
|
+
status_label = "configured+verified"
|
|
369
|
+
elif config_status["configured"]:
|
|
370
|
+
status_label = "configured"
|
|
371
|
+
else:
|
|
372
|
+
status_label = "not configured"
|
|
373
|
+
location = config_status["config_path"] or "unknown"
|
|
374
|
+
verified_at = config_status["last_verified_at"] or "-"
|
|
375
|
+
print(f" {tool}: {status_label} ({location}, verified={verified_at})")
|
|
376
|
+
print("Executables:")
|
|
377
|
+
print(f" codex: {find_executable('codex') or 'not found'}")
|
|
378
|
+
print(f" claude: {find_executable('claude') or 'not found'}")
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _cmd_doctor() -> int:
|
|
383
|
+
state = load_state()
|
|
384
|
+
failures = 0
|
|
385
|
+
env_matrx_key = (os.environ.get("MTRX_KEY") or "").strip()
|
|
386
|
+
workspace_binding = get_workspace_binding(state, cwd=os.environ.get("PWD") or os.getcwd()) or {}
|
|
387
|
+
workspace_matrx_key = (workspace_binding.get("matrx_key") or "").strip()
|
|
388
|
+
|
|
389
|
+
for tool in ("codex", "claude"):
|
|
390
|
+
found = find_executable(tool)
|
|
391
|
+
if found:
|
|
392
|
+
print(f"[ok] {tool} executable: {found}")
|
|
393
|
+
else:
|
|
394
|
+
print(f"[fail] {tool} executable not found")
|
|
395
|
+
failures += 1
|
|
396
|
+
|
|
397
|
+
matrx_key = (state["auth"]["matrx"].get("key") or "").strip()
|
|
398
|
+
if matrx_key:
|
|
399
|
+
print("[ok] Matrx key saved")
|
|
400
|
+
elif workspace_matrx_key:
|
|
401
|
+
print("[warn] No Matrx key saved (current workspace has a stored MATRX binding)")
|
|
402
|
+
elif env_matrx_key:
|
|
403
|
+
print("[warn] No Matrx key saved (current shell has MTRX_KEY override)")
|
|
404
|
+
else:
|
|
405
|
+
print("[warn] No Matrx key saved")
|
|
406
|
+
if env_matrx_key:
|
|
407
|
+
print("[ok] MTRX_KEY override present in current shell")
|
|
408
|
+
if workspace_matrx_key:
|
|
409
|
+
print("[ok] Workspace MATRX binding present in current repo")
|
|
410
|
+
|
|
411
|
+
openai_key = (state["auth"]["openai"].get("key") or "").strip()
|
|
412
|
+
if openai_key:
|
|
413
|
+
print("[ok] OpenAI fallback key saved")
|
|
414
|
+
else:
|
|
415
|
+
print("[warn] No OpenAI fallback key saved")
|
|
416
|
+
|
|
417
|
+
anthropic_key = (state["auth"]["anthropic"].get("key") or "").strip()
|
|
418
|
+
if anthropic_key:
|
|
419
|
+
print("[ok] Anthropic fallback key saved")
|
|
420
|
+
else:
|
|
421
|
+
print("[warn] No Anthropic fallback key saved")
|
|
422
|
+
|
|
423
|
+
if claude_oauth_available():
|
|
424
|
+
print(f"[ok] Local Claude OAuth found at {claude_credentials_path()}")
|
|
425
|
+
else:
|
|
426
|
+
print(f"[warn] Local Claude OAuth not found at {claude_credentials_path()}")
|
|
427
|
+
|
|
428
|
+
for tool in ("codex", "claude"):
|
|
429
|
+
route = configured_route(state, tool)
|
|
430
|
+
if route == "matrx" and not _has_matrx_login(state, env=os.environ):
|
|
431
|
+
print(f"[fail] Default {tool} route is matrx but no Matrx key is saved")
|
|
432
|
+
failures += 1
|
|
433
|
+
elif route is None:
|
|
434
|
+
print(f"[warn] Default {tool} route not chosen yet (first `mtrx {tool}` launch will initialize it to matrx)")
|
|
435
|
+
config_status = get_tool_config_status(state, tool)
|
|
436
|
+
if tool == "codex" and config_status["verified"] and not config_status["configured"]:
|
|
437
|
+
if route == "matrx":
|
|
438
|
+
print(f"[ok] {tool} uses runtime launch overrides; native config is clean at {config_status['config_path']}")
|
|
439
|
+
else:
|
|
440
|
+
print(f"[ok] {tool} native config is clean at {config_status['config_path']}")
|
|
441
|
+
elif config_status["configured"] and config_status["verified"]:
|
|
442
|
+
print(f"[ok] {tool} native config verified at {config_status['config_path']}")
|
|
443
|
+
elif config_status["configured"]:
|
|
444
|
+
print(f"[warn] {tool} native config written but not verified")
|
|
445
|
+
else:
|
|
446
|
+
print(f"[warn] {tool} native config not configured")
|
|
447
|
+
|
|
448
|
+
return 1 if failures else 0
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _cmd_launch(tool: str, route: str | None, remainder: list[str]) -> int:
|
|
452
|
+
state = load_state()
|
|
453
|
+
route, promoted = _maybe_promote_direct_route(state, tool, route)
|
|
454
|
+
first_launch = route is None and configured_route(state, tool) is None
|
|
455
|
+
effective_route = resolve_route(state, tool, route)
|
|
456
|
+
try:
|
|
457
|
+
auth_changed = False
|
|
458
|
+
if effective_route == "matrx":
|
|
459
|
+
state, login_changed = _complete_matrx_login(state)
|
|
460
|
+
auth_changed = auth_changed or login_changed
|
|
461
|
+
if tool == "codex":
|
|
462
|
+
_complete_codex_login()
|
|
463
|
+
initialized = initialize_first_launch_route(state, tool, route)
|
|
464
|
+
state, changed = prepare_routed_setup(
|
|
465
|
+
state,
|
|
466
|
+
tool=tool,
|
|
467
|
+
route_override=route,
|
|
468
|
+
base_env=os.environ,
|
|
469
|
+
)
|
|
470
|
+
if initialized or changed or auth_changed or promoted:
|
|
471
|
+
save_state(state)
|
|
472
|
+
if initialized:
|
|
473
|
+
print(
|
|
474
|
+
f"First-time setup: default route for {tool} set to matrx. "
|
|
475
|
+
f"Use `mtrx use {tool} direct` to opt out.",
|
|
476
|
+
)
|
|
477
|
+
plan = build_launch_plan(
|
|
478
|
+
state,
|
|
479
|
+
tool=tool,
|
|
480
|
+
route_override=route,
|
|
481
|
+
passthrough_args=remainder,
|
|
482
|
+
base_env=os.environ,
|
|
483
|
+
)
|
|
484
|
+
validate_launch_plan(plan, state)
|
|
485
|
+
for line in describe_launch_plan(plan, state):
|
|
486
|
+
print(line)
|
|
487
|
+
except ValueError as exc:
|
|
488
|
+
print(str(exc), file=sys.stderr)
|
|
489
|
+
return 1
|
|
490
|
+
return launch(plan)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _has_matrx_login(state: dict, env: dict[str, str] | None = None) -> bool:
|
|
494
|
+
env = env or {}
|
|
495
|
+
env_key = (env.get("MTRX_KEY") or "").strip()
|
|
496
|
+
if env_key:
|
|
497
|
+
return True
|
|
498
|
+
workspace_binding = get_workspace_binding(state, cwd=env.get("PWD") or os.getcwd()) or {}
|
|
499
|
+
workspace_key = (workspace_binding.get("matrx_key") or "").strip()
|
|
500
|
+
if workspace_key:
|
|
501
|
+
return True
|
|
502
|
+
return bool((state.get("auth", {}).get("matrx", {}).get("key") or "").strip())
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _default_route_label(route: str | None) -> str:
|
|
506
|
+
return route or "unset"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
if __name__ == "__main__":
|
|
510
|
+
raise SystemExit(main())
|