mirrorneuron-cli 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: mirrorneuron-cli
3
+ Version: 1.0.0
4
+ Summary: MirrorNeuron CLI
5
+ License-Expression: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: mirrorneuron-python-sdk
11
+ Requires-Dist: typer>=0.9.0
12
+ Requires-Dist: rich>=13.0.0
13
+ Dynamic: license-file
14
+
15
+ # MirrorNeuron CLI
16
+
17
+ The official Command Line Interface for managing the MirrorNeuron distributed runtime system.
18
+
19
+ ## Installation
20
+ *Note: This tool is installed automatically and symlinked globally as `mn` by the MirrorNeuron `install.sh` script.*
21
+
22
+ ```bash
23
+ pip install mirrorneuron-cli
24
+ ```
25
+
26
+ ## Commands
27
+ Powered by Typer and Rich, this CLI provides elegant formatting for job lifecycle management:
28
+
29
+ ```bash
30
+ mn nodes # View system summary and available executor pools
31
+ mn submit ./flow.json # Submit a new workflow manifest
32
+ mn list # View all active and completed jobs
33
+ mn status <job_id> # Detailed execution state of a specific job
34
+ mn monitor <job_id> # Stream live execution events
35
+ mn cancel <job_id> # Gracefully terminate a workflow
36
+ mn metrics # Inspect runtime metrics summary
37
+ mn dead-letters <job_id> # Inspect dead-letter events
38
+ ```
39
+
40
+ ## Blueprint Commands
41
+
42
+ Blueprint execution and observability are grouped under `mn blueprint` so users have one CLI entry point:
43
+
44
+ ```bash
45
+ mn blueprint list
46
+ mn blueprint install
47
+ mn blueprint update
48
+ mn blueprint run <blueprint_id>
49
+ mn blueprint run ./path/to/bundle_or_source_blueprint
50
+ mn blueprint run <blueprint_id> --offline
51
+ mn blueprint run <blueprint_id> --revision <git_sha_or_tag>
52
+ mn blueprint monitor --follow
53
+ mn blueprint tail <run_id>
54
+ mn blueprint compare <run_a> <run_b>
55
+ mn blueprint export <run_id> --format markdown
56
+ mn blueprint export <run_id> --format html
57
+ ```
58
+
59
+ `mn blueprint run` accepts either an installed blueprint ID or a local folder. If the local folder is already a bundle, the CLI submits it directly; if it is a Python source blueprint, the CLI generates a bundle under `~/.mn/generated_blueprint_bundles/<run_id>/` first. Catalog runs use the cached blueprint library by default so runs are not silently changed by a network pull. Use `mn blueprint update` or `mn blueprint run <id> --update` when you want to refresh the cache. Each blueprint submission pre-generates a shared `MN_RUN_ID`, injects `MN_BLUEPRINT_CONFIG_JSON` into runtime workers, and prints both the blueprint run ID and runtime job ID.
60
+
61
+ Blueprint run artifacts are read from the shared run store at `~/.mn/runs/<run_id>/`.
62
+ Use `--runs-root <path>` with `monitor`, `tail`, `compare`, or `export` when inspecting a custom run directory.
63
+ When a blueprint registers a shared or custom web UI, `monitor` shows the local URL and `export --format html` creates a static report page.
64
+
65
+ ## Configuration
66
+
67
+ All overrides use `MN_` env vars:
68
+
69
+ - `MN_GRPC_TARGET`: core gRPC target.
70
+ - `MN_GRPC_TIMEOUT_SECONDS`: RPC timeout; `0` or `none` disables it.
71
+ - `MN_GRPC_AUTH_TOKEN`: optional bearer metadata for protected core gateways.
72
+ - `MN_CLI_LOG_PATH`: error log path.
73
+ - `MN_CLI_OUTPUT=plain`: disable Rich color output.
@@ -0,0 +1,19 @@
1
+ mirrorneuron_cli-1.0.0.dist-info/licenses/LICENSE,sha256=wdEmA-XJ6EaXHsjFyuRKhT-owNT5-ucLf6jbtOz0yOM,1072
2
+ mn_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ mn_cli/config.py,sha256=fJfjiM__uHGStgz7jsuSN52HK3LOv76omhwWmR74hEk,1343
4
+ mn_cli/error_handler.py,sha256=2k1PLN777U8EY90BpM3N7XtO3osPbzPzmraclWYYgYI,2276
5
+ mn_cli/logging_config.py,sha256=AXHLgpcJHPEGd-axGrtTt_bslRQwr6eQ8Dxvmp0jK_I,982
6
+ mn_cli/main.py,sha256=jShrxF1wszLKhKND4y6O5y3iQ007CO05pzwYZGBxpTc,1100
7
+ mn_cli/server_cmds.py,sha256=n4rctjJbx9ZvCNMpIHniXvsO2QhSRhZ_HZvM7phPDJE,12162
8
+ mn_cli/shared.py,sha256=aK2_mDXezAZsCiyRskPxdXmxUcjubKB7yGUGNzu7ylo,420
9
+ mn_cli/libs/__init__.py,sha256=1IXSLNPLt2HhYADcj4mt_jQBK0nd1tCwkIfIwB6ycYo,26
10
+ mn_cli/libs/blueprint_cmds.py,sha256=l7OBr4rS6c5febGLbUscoqP9xyDTqvrEPI8kD3l2fSc,24481
11
+ mn_cli/libs/job_cmds.py,sha256=cO4AoqTVRQNLzQGRCGZUBnUa_3ATT6g_8Lp9BKb-9qc,5447
12
+ mn_cli/libs/run_cmds.py,sha256=AYaH7J1RYhDAEVDDld2LLg2OLjRjng7OtWyMSkd7hO0,30046
13
+ mn_cli/libs/sys_cmds.py,sha256=LIeWoxUwZADMZfTnkrz1L5MIP3wugE_yiKjm9LRvKBU,1881
14
+ mn_cli/libs/ui.py,sha256=3qyEx2d0JFidcvP1IRq246D8l5rBaXqrlkOFx9NHHh0,5824
15
+ mirrorneuron_cli-1.0.0.dist-info/METADATA,sha256=njQm3tMNA6dtz5eN95G4_WZm7xgwq6zh3sfPLq4bv-Q,3162
16
+ mirrorneuron_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ mirrorneuron_cli-1.0.0.dist-info/entry_points.txt,sha256=e7vGuYEi_HV-mIYTbRbhfTGQMn04y-ItDDAdCTMYHdQ,39
18
+ mirrorneuron_cli-1.0.0.dist-info/top_level.txt,sha256=yE-PY4RVgDSFkh0okxYE9UHWVBU5xEsyL8G2ZA_xEAE,7
19
+ mirrorneuron_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mn = mn_cli.main:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MirrorNeuronLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mn_cli
mn_cli/__init__.py ADDED
File without changes
mn_cli/config.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CliConfig:
10
+ grpc_target: str = "localhost:50051"
11
+ grpc_timeout_seconds: float | None = 10.0
12
+ grpc_auth_token: str = ""
13
+ log_path: Path = Path.home() / ".mn" / "logs" / "cli.log"
14
+ output_mode: str = "rich"
15
+
16
+ @classmethod
17
+ def from_env(cls) -> "CliConfig":
18
+ core_host = os.getenv("MN_CORE_HOST", "localhost")
19
+ return cls(
20
+ grpc_target=os.getenv(
21
+ "MN_GRPC_TARGET",
22
+ os.getenv("MN_CORE_GRPC_TARGET", f"{core_host}:50051"),
23
+ ),
24
+ grpc_timeout_seconds=_timeout(),
25
+ grpc_auth_token=os.getenv("MN_GRPC_AUTH_TOKEN", ""),
26
+ log_path=Path(
27
+ os.getenv(
28
+ "MN_CLI_LOG_PATH",
29
+ str(Path.home() / ".mn" / "logs" / "cli.log"),
30
+ )
31
+ ).expanduser(),
32
+ output_mode=os.getenv("MN_CLI_OUTPUT", "rich"),
33
+ )
34
+
35
+
36
+ def _timeout() -> float | None:
37
+ value = os.getenv("MN_GRPC_TIMEOUT_SECONDS", "10")
38
+ if value.lower() in {"", "none", "0"}:
39
+ return None
40
+ try:
41
+ return float(value)
42
+ except ValueError as exc:
43
+ raise ValueError("MN_GRPC_TIMEOUT_SECONDS must be a number, 0, or none") from exc
@@ -0,0 +1,51 @@
1
+ import sys
2
+ import grpc
3
+ from rich.console import Console
4
+ from mn_cli.config import CliConfig
5
+ from mn_cli.logging_config import configure_logging
6
+
7
+ log_file = CliConfig.from_env().log_path
8
+ logger = configure_logging("mn-cli", log_file)
9
+
10
+ CONTEXT_MESSAGES = {
11
+ "submit": "Error submitting job",
12
+ "status": "Error fetching job status",
13
+ "list_jobs": "Error listing jobs",
14
+ "clear": "Error clearing jobs",
15
+ "cancel": "Error cancelling job",
16
+ "pause": "Error pausing job",
17
+ "resume": "Error resuming job",
18
+ "nodes": "Error fetching nodes",
19
+ "metrics": "Error fetching metrics",
20
+ "dead_letters": "Error listing dead letters",
21
+ "run bundle": "Error running bundle",
22
+ "monitor stream": "Error fetching job",
23
+ "fetch results": "Error fetching results",
24
+ "validate": "Validation failed",
25
+ "leave": "Error removing node",
26
+ }
27
+
28
+ def handle_cli_error(e: Exception, console: Console, context: str = ""):
29
+ """Handle exceptions gracefully, log the full trace, and print a friendly message."""
30
+ logger.exception(f"Error during {context}")
31
+
32
+ if isinstance(e, grpc.RpcError):
33
+ code = e.code()
34
+ details = e.details()
35
+
36
+ if code == grpc.StatusCode.NOT_FOUND:
37
+ console.print(f"[red]Error: Cannot find the job by ID. ({details})[/red]")
38
+ elif code == grpc.StatusCode.INTERNAL and "not found" in str(details).lower():
39
+ console.print(f"[red]Error: Cannot find the job by ID. ({details})[/red]")
40
+ elif code == grpc.StatusCode.INTERNAL and "terminal state" in str(details).lower():
41
+ console.print(f"[red]Error: Job is already in a terminal state and cannot be modified.[/red]")
42
+ elif code == grpc.StatusCode.RESOURCE_EXHAUSTED:
43
+ console.print("[yellow]Runtime is under CPU/GPU/memory pressure and is not accepting new jobs.[/yellow]")
44
+ console.print(f"[dim]{details}[/dim]")
45
+ else:
46
+ console.print(f"[red]Communication Error: {details} (Code: {code.name})[/red]")
47
+ console.print(f"[dim]See {log_file} for full details.[/dim]")
48
+ else:
49
+ prefix = CONTEXT_MESSAGES.get(context, "Error")
50
+ console.print(f"[red]{prefix}: {str(e)}[/red]")
51
+ console.print(f"[dim]See {log_file} for full details.[/dim]")
@@ -0,0 +1 @@
1
+ # Initialization for libs