mirrorneuron-cli 1.1.9__tar.gz → 1.2.6__tar.gz

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.
Files changed (99) hide show
  1. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/.gitignore +17 -1
  2. mirrorneuron_cli-1.2.6/.python-version +1 -0
  3. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/AGENTS.md +1 -0
  4. {mirrorneuron_cli-1.1.9/mirrorneuron_cli.egg-info → mirrorneuron_cli-1.2.6}/PKG-INFO +5 -4
  5. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/README.md +3 -3
  6. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6/mirrorneuron_cli.egg-info}/PKG-INFO +5 -4
  7. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mirrorneuron_cli.egg-info/SOURCES.txt +23 -1
  8. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mirrorneuron_cli.egg-info/requires.txt +1 -0
  9. mirrorneuron_cli-1.2.6/mn_cli/config.py +38 -0
  10. mirrorneuron_cli-1.2.6/mn_cli/libs/artifacts.py +139 -0
  11. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/backup_cmds.py +20 -8
  12. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/blueprint_cmds.py +741 -54
  13. mirrorneuron_cli-1.2.6/mn_cli/libs/blueprint_models.py +118 -0
  14. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/blueprint_repository.py +25 -16
  15. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/blueprint_resources.py +62 -55
  16. mirrorneuron_cli-1.2.6/mn_cli/libs/bundles.py +40 -0
  17. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/deployment_cmds.py +68 -29
  18. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/event_relay.py +0 -2
  19. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/job_cmds.py +185 -22
  20. mirrorneuron_cli-1.2.6/mn_cli/libs/model_cmds.py +542 -0
  21. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/resource_cmds.py +65 -9
  22. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/run_cmds.py +764 -454
  23. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/run_logs.py +5 -4
  24. mirrorneuron_cli-1.2.6/mn_cli/libs/run_manifest.py +1131 -0
  25. mirrorneuron_cli-1.2.6/mn_cli/libs/runtime_health.py +322 -0
  26. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/schedule_cmds.py +78 -12
  27. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/service_cmds.py +9 -3
  28. mirrorneuron_cli-1.2.6/mn_cli/libs/skill_runtime.py +668 -0
  29. mirrorneuron_cli-1.2.6/mn_cli/libs/sys_cmds.py +354 -0
  30. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/ui.py +89 -3
  31. mirrorneuron_cli-1.2.6/mn_cli/libs/workflow_progress.py +318 -0
  32. mirrorneuron_cli-1.2.6/mn_cli/libs/workflow_validation.py +677 -0
  33. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/main.py +76 -8
  34. mirrorneuron_cli-1.2.6/mn_cli/runtime_mode.py +66 -0
  35. mirrorneuron_cli-1.2.6/mn_cli/runtime_state.py +135 -0
  36. mirrorneuron_cli-1.2.6/mn_cli/schemas/workflow_manifest.schema.json +261 -0
  37. mirrorneuron_cli-1.2.6/mn_cli/sdk_path.py +12 -0
  38. mirrorneuron_cli-1.2.6/mn_cli/server_cmds.py +3756 -0
  39. mirrorneuron_cli-1.2.6/mn_cli/shared.py +56 -0
  40. mirrorneuron_cli-1.2.6/mn_cli/terminal.py +25 -0
  41. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/update_cmds.py +139 -73
  42. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/pyproject.toml +4 -0
  43. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_backup_cmds.py +52 -0
  44. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_blueprint_cmds.py +386 -37
  45. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_blueprint_repository.py +6 -3
  46. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_deployment_cmds.py +18 -2
  47. mirrorneuron_cli-1.2.6/tests/test_docker_network_integration.py +101 -0
  48. mirrorneuron_cli-1.2.6/tests/test_job_cmds.py +57 -0
  49. mirrorneuron_cli-1.2.6/tests/test_main.py +178 -0
  50. mirrorneuron_cli-1.2.6/tests/test_model_cmds.py +356 -0
  51. mirrorneuron_cli-1.2.6/tests/test_resource_cmds.py +52 -0
  52. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_run_cmds.py +523 -82
  53. mirrorneuron_cli-1.2.6/tests/test_run_helpers.py +1092 -0
  54. mirrorneuron_cli-1.2.6/tests/test_runtime_health.py +329 -0
  55. mirrorneuron_cli-1.2.6/tests/test_runtime_mode.py +57 -0
  56. mirrorneuron_cli-1.2.6/tests/test_runtime_state.py +84 -0
  57. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_schedule_cmds.py +22 -0
  58. mirrorneuron_cli-1.2.6/tests/test_server_cmds.py +2846 -0
  59. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_service_cmds.py +17 -1
  60. mirrorneuron_cli-1.2.6/tests/test_shared.py +242 -0
  61. mirrorneuron_cli-1.2.6/tests/test_sys_cmds.py +161 -0
  62. mirrorneuron_cli-1.2.6/tests/test_terminal.py +32 -0
  63. mirrorneuron_cli-1.2.6/tests/test_ui.py +49 -0
  64. mirrorneuron_cli-1.2.6/tests/test_update_cmds.py +275 -0
  65. mirrorneuron_cli-1.2.6/tests/test_workflow_validation.py +95 -0
  66. mirrorneuron_cli-1.2.6/uv.lock +393 -0
  67. mirrorneuron_cli-1.1.9/mn_cli/config.py +0 -106
  68. mirrorneuron_cli-1.1.9/mn_cli/libs/run_manifest.py +0 -522
  69. mirrorneuron_cli-1.1.9/mn_cli/libs/sys_cmds.py +0 -146
  70. mirrorneuron_cli-1.1.9/mn_cli/libs/workflow_progress.py +0 -131
  71. mirrorneuron_cli-1.1.9/mn_cli/server_cmds.py +0 -1929
  72. mirrorneuron_cli-1.1.9/mn_cli/shared.py +0 -45
  73. mirrorneuron_cli-1.1.9/tests/test_job_cmds.py +0 -507
  74. mirrorneuron_cli-1.1.9/tests/test_main.py +0 -58
  75. mirrorneuron_cli-1.1.9/tests/test_run_helpers.py +0 -291
  76. mirrorneuron_cli-1.1.9/tests/test_server_cmds.py +0 -1310
  77. mirrorneuron_cli-1.1.9/tests/test_shared.py +0 -121
  78. mirrorneuron_cli-1.1.9/tests/test_sys_cmds.py +0 -178
  79. mirrorneuron_cli-1.1.9/tests/test_update_cmds.py +0 -151
  80. mirrorneuron_cli-1.1.9/uv.lock +0 -204
  81. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/.github/workflows/ci.yml +0 -0
  82. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/.github/workflows/release.yml +0 -0
  83. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/LICENSE +0 -0
  84. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/RELEASE.md +0 -0
  85. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
  86. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
  87. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
  88. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/__init__.py +0 -0
  89. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/banner.py +0 -0
  90. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/error_handler.py +0 -0
  91. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/__init__.py +0 -0
  92. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/libs/blueprint_observability.py +0 -0
  93. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/mn_cli/logging_config.py +0 -0
  94. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/scripts/check-release-artifacts.sh +0 -0
  95. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/scripts/make-release-zip.sh +0 -0
  96. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/scripts/validate-version-tag.sh +0 -0
  97. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/setup.cfg +0 -0
  98. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/conftest.py +0 -0
  99. {mirrorneuron_cli-1.1.9 → mirrorneuron_cli-1.2.6}/tests/test_blueprint_resources.py +0 -0
@@ -1,6 +1,9 @@
1
1
  # Environments
2
2
  .env
3
+ .env.*
4
+ !.env.example
3
5
  .venv
6
+ .venv/
4
7
  env/
5
8
  venv/
6
9
  ENV/
@@ -30,5 +33,18 @@ __pycache__/
30
33
  *.py[cod]
31
34
  *$py.class
32
35
  .pytest_cache/
36
+ .ruff_cache/
37
+ .mypy_cache/
38
+ .pyre/
39
+ .hypothesis/
40
+ .tox/
41
+ .nox/
33
42
  .coverage
34
- htmlcov/
43
+ .coverage.*
44
+ coverage.xml
45
+ htmlcov/
46
+
47
+ # Local editor and OS files
48
+ .DS_Store
49
+ .idea/
50
+ .vscode/
@@ -0,0 +1 @@
1
+ 3.11.15
@@ -7,3 +7,4 @@ Guidance for future coding agents working in this repository.
7
7
  - Unless the user explicitly asks for a temporary workaround, fix the root cause in the intended layer or contract.
8
8
  - Avoid adding fallback paths, compatibility shims, feature flags, or temp solutions that mask a broken primary path.
9
9
  - If fallback behavior is already product-specified, keep it narrow, documented, and tested; do not use it to avoid fixing the primary path.
10
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.1.9
3
+ Version: 1.2.6
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: mirrorneuron-python-sdk
11
+ Requires-Dist: jsonschema>=4.25
11
12
  Requires-Dist: typer>=0.9.0
12
13
  Requires-Dist: rich>=13.0.0
13
14
  Dynamic: license-file
@@ -23,10 +24,10 @@ services installed by `mn-deploy`.
23
24
  Install locally and run tests:
24
25
 
25
26
  ```bash
26
- python3 -m venv .venv
27
+ python3.11 -m venv .venv
27
28
  . .venv/bin/activate
28
- python3 -m pip install -e .
29
- python3 -m pytest -q
29
+ .venv/bin/python -m pip install -e .
30
+ .venv/bin/python -m pytest -q
30
31
  ```
31
32
 
32
33
  Try the CLI:
@@ -9,10 +9,10 @@ services installed by `mn-deploy`.
9
9
  Install locally and run tests:
10
10
 
11
11
  ```bash
12
- python3 -m venv .venv
12
+ python3.11 -m venv .venv
13
13
  . .venv/bin/activate
14
- python3 -m pip install -e .
15
- python3 -m pytest -q
14
+ .venv/bin/python -m pip install -e .
15
+ .venv/bin/python -m pytest -q
16
16
  ```
17
17
 
18
18
  Try the CLI:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.1.9
3
+ Version: 1.2.6
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: mirrorneuron-python-sdk
11
+ Requires-Dist: jsonschema>=4.25
11
12
  Requires-Dist: typer>=0.9.0
12
13
  Requires-Dist: rich>=13.0.0
13
14
  Dynamic: license-file
@@ -23,10 +24,10 @@ services installed by `mn-deploy`.
23
24
  Install locally and run tests:
24
25
 
25
26
  ```bash
26
- python3 -m venv .venv
27
+ python3.11 -m venv .venv
27
28
  . .venv/bin/activate
28
- python3 -m pip install -e .
29
- python3 -m pytest -q
29
+ .venv/bin/python -m pip install -e .
30
+ .venv/bin/python -m pytest -q
30
31
  ```
31
32
 
32
33
  Try the CLI:
@@ -1,4 +1,5 @@
1
1
  .gitignore
2
+ .python-version
2
3
  AGENTS.md
3
4
  LICENSE
4
5
  README.md
@@ -19,27 +20,39 @@ mn_cli/config.py
19
20
  mn_cli/error_handler.py
20
21
  mn_cli/logging_config.py
21
22
  mn_cli/main.py
23
+ mn_cli/runtime_mode.py
24
+ mn_cli/runtime_state.py
25
+ mn_cli/sdk_path.py
22
26
  mn_cli/server_cmds.py
23
27
  mn_cli/shared.py
28
+ mn_cli/terminal.py
24
29
  mn_cli/update_cmds.py
25
30
  mn_cli/libs/__init__.py
31
+ mn_cli/libs/artifacts.py
26
32
  mn_cli/libs/backup_cmds.py
27
33
  mn_cli/libs/blueprint_cmds.py
34
+ mn_cli/libs/blueprint_models.py
28
35
  mn_cli/libs/blueprint_observability.py
29
36
  mn_cli/libs/blueprint_repository.py
30
37
  mn_cli/libs/blueprint_resources.py
38
+ mn_cli/libs/bundles.py
31
39
  mn_cli/libs/deployment_cmds.py
32
40
  mn_cli/libs/event_relay.py
33
41
  mn_cli/libs/job_cmds.py
42
+ mn_cli/libs/model_cmds.py
34
43
  mn_cli/libs/resource_cmds.py
35
44
  mn_cli/libs/run_cmds.py
36
45
  mn_cli/libs/run_logs.py
37
46
  mn_cli/libs/run_manifest.py
47
+ mn_cli/libs/runtime_health.py
38
48
  mn_cli/libs/schedule_cmds.py
39
49
  mn_cli/libs/service_cmds.py
50
+ mn_cli/libs/skill_runtime.py
40
51
  mn_cli/libs/sys_cmds.py
41
52
  mn_cli/libs/ui.py
42
53
  mn_cli/libs/workflow_progress.py
54
+ mn_cli/libs/workflow_validation.py
55
+ mn_cli/schemas/workflow_manifest.schema.json
43
56
  scripts/check-release-artifacts.sh
44
57
  scripts/make-release-zip.sh
45
58
  scripts/validate-version-tag.sh
@@ -49,13 +62,22 @@ tests/test_blueprint_cmds.py
49
62
  tests/test_blueprint_repository.py
50
63
  tests/test_blueprint_resources.py
51
64
  tests/test_deployment_cmds.py
65
+ tests/test_docker_network_integration.py
52
66
  tests/test_job_cmds.py
53
67
  tests/test_main.py
68
+ tests/test_model_cmds.py
69
+ tests/test_resource_cmds.py
54
70
  tests/test_run_cmds.py
55
71
  tests/test_run_helpers.py
72
+ tests/test_runtime_health.py
73
+ tests/test_runtime_mode.py
74
+ tests/test_runtime_state.py
56
75
  tests/test_schedule_cmds.py
57
76
  tests/test_server_cmds.py
58
77
  tests/test_service_cmds.py
59
78
  tests/test_shared.py
60
79
  tests/test_sys_cmds.py
61
- tests/test_update_cmds.py
80
+ tests/test_terminal.py
81
+ tests/test_ui.py
82
+ tests/test_update_cmds.py
83
+ tests/test_workflow_validation.py
@@ -1,3 +1,4 @@
1
1
  mirrorneuron-python-sdk
2
+ jsonschema>=4.25
2
3
  typer>=0.9.0
3
4
  rich>=13.0.0
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from mn_cli.sdk_path import add_local_sdk_path
8
+
9
+ add_local_sdk_path("runtime_config.py")
10
+
11
+ from mn_sdk.runtime_config import RuntimeConfig, default_logs_root
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CliConfig:
16
+ grpc_target: str = "localhost:55051"
17
+ grpc_timeout_seconds: float | None = 10.0
18
+ grpc_auth_token: str = ""
19
+ grpc_admin_token: str = ""
20
+ log_path: Path = default_logs_root() / "cli.log"
21
+ output_mode: str = "rich"
22
+
23
+ @classmethod
24
+ def from_env(cls) -> "CliConfig":
25
+ runtime_config = RuntimeConfig.from_env()
26
+ return cls(
27
+ grpc_target=runtime_config.grpc_target,
28
+ grpc_timeout_seconds=runtime_config.grpc_timeout_seconds,
29
+ grpc_auth_token=runtime_config.grpc_auth_token,
30
+ grpc_admin_token=runtime_config.grpc_admin_token,
31
+ log_path=Path(
32
+ os.getenv(
33
+ "MN_CLI_LOG_PATH",
34
+ str(default_logs_root() / "cli.log"),
35
+ )
36
+ ).expanduser(),
37
+ output_mode=os.getenv("MN_CLI_OUTPUT", "rich"),
38
+ )
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import mimetypes
5
+ import os
6
+ import socket
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from mn_sdk.runtime_config import resolve_mn_home
11
+
12
+ from mn_cli.runtime_state import read_env_file
13
+
14
+ DEFAULT_INLINE_PAYLOAD_MAX_BYTES = 1_048_576
15
+ DEFAULT_ARTIFACT_PORT = "55660"
16
+
17
+
18
+ def promote_large_payloads_to_blob_refs(
19
+ manifest: dict[str, Any],
20
+ payloads: dict[str, bytes],
21
+ *,
22
+ runtime_env: dict[str, str] | None = None,
23
+ ) -> list[dict[str, Any]]:
24
+ threshold = _inline_payload_max_bytes()
25
+ if threshold < 0:
26
+ return []
27
+
28
+ env = _runtime_env_file_values()
29
+ env.update(os.environ)
30
+ env.update(runtime_env or {})
31
+ root = _host_blob_store_root(env)
32
+ promoted: list[dict[str, Any]] = []
33
+
34
+ for rel_path, contents in list(payloads.items()):
35
+ if len(contents) <= threshold:
36
+ continue
37
+
38
+ blob_ref = _store_payload_blob(root, rel_path, contents, env)
39
+ promoted.append(blob_ref)
40
+ del payloads[rel_path]
41
+
42
+ if promoted:
43
+ metadata = manifest.setdefault("metadata", {})
44
+ artifacts = metadata.setdefault("mn_artifacts", {})
45
+ artifacts.setdefault("blob_refs", []).extend(promoted)
46
+
47
+ return promoted
48
+
49
+
50
+ def _store_payload_blob(
51
+ root: Path,
52
+ rel_path: str,
53
+ contents: bytes,
54
+ env: dict[str, str],
55
+ ) -> dict[str, Any]:
56
+ sha256 = hashlib.sha256(contents).hexdigest()
57
+ target = root / sha256[:2] / sha256
58
+ target.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ if not target.exists():
61
+ tmp = target.with_name(f"{target.name}.tmp-{os.getpid()}")
62
+ tmp.write_bytes(contents)
63
+ os.replace(tmp, target)
64
+
65
+ media_type, _encoding = mimetypes.guess_type(rel_path)
66
+ location = _blob_location(sha256, env)
67
+
68
+ blob_ref: dict[str, Any] = {
69
+ "type": "blob_ref",
70
+ "sha256": sha256,
71
+ "size_bytes": len(contents),
72
+ "media_type": media_type or "application/octet-stream",
73
+ "logical_name": Path(rel_path).name,
74
+ "scope": "job",
75
+ "payload_path": rel_path.replace("\\", "/"),
76
+ }
77
+
78
+ if location:
79
+ blob_ref["locations"] = [location]
80
+
81
+ return blob_ref
82
+
83
+
84
+ def _blob_location(sha256: str, env: dict[str, str]) -> dict[str, str] | None:
85
+ base_url = str(env.get("MN_ARTIFACT_ADVERTISE_URL") or os.getenv("MN_ARTIFACT_ADVERTISE_URL") or "").strip()
86
+ if not base_url:
87
+ host = (
88
+ str(env.get("MN_NETWORK_ADVERTISE_HOST") or os.getenv("MN_NETWORK_ADVERTISE_HOST") or "").strip()
89
+ or _detect_lan_ip()
90
+ )
91
+ port = str(env.get("MN_ARTIFACT_PORT") or os.getenv("MN_ARTIFACT_PORT") or DEFAULT_ARTIFACT_PORT).strip()
92
+ if not host or not port:
93
+ return None
94
+ base_url = f"http://{host}:{port}"
95
+
96
+ location = {
97
+ "url": f"{base_url.rstrip('/')}/blobs/{sha256}",
98
+ "status": "available",
99
+ }
100
+ node = str(env.get("MN_NODE_NAME") or os.getenv("MN_NODE_NAME") or "").strip()
101
+ if node:
102
+ location["node"] = node
103
+ return location
104
+
105
+
106
+ def _host_blob_store_root(env: dict[str, str]) -> Path:
107
+ configured = (
108
+ env.get("MN_HOST_BLOB_STORE_DIR")
109
+ or os.getenv("MN_HOST_BLOB_STORE_DIR")
110
+ or env.get("MN_BLOB_STORE_ROOT")
111
+ or os.getenv("MN_BLOB_STORE_ROOT")
112
+ )
113
+ if configured:
114
+ return Path(configured).expanduser()
115
+ return resolve_mn_home() / "blobs"
116
+
117
+
118
+ def _runtime_env_file_values() -> dict[str, str]:
119
+ env_file = resolve_mn_home() / "docker-compose.env"
120
+ return {key.strip(): value.strip() for key, value in read_env_file(env_file).items()}
121
+
122
+
123
+ def _inline_payload_max_bytes() -> int:
124
+ value = os.getenv("MN_INLINE_PAYLOAD_MAX_BYTES", str(DEFAULT_INLINE_PAYLOAD_MAX_BYTES))
125
+ try:
126
+ return int(value)
127
+ except (TypeError, ValueError):
128
+ return DEFAULT_INLINE_PAYLOAD_MAX_BYTES
129
+
130
+
131
+ def _detect_lan_ip() -> str:
132
+ probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
133
+ try:
134
+ probe.connect(("8.8.8.8", 80))
135
+ return probe.getsockname()[0]
136
+ except OSError:
137
+ return "127.0.0.1"
138
+ finally:
139
+ probe.close()
@@ -12,8 +12,11 @@ from typing import Any
12
12
 
13
13
  import typer
14
14
 
15
+ from mn_sdk.runtime_config import default_runs_root
16
+
15
17
  from mn_cli.error_handler import handle_cli_error
16
18
  from mn_cli.libs.blueprint_observability import make_blueprint_run_id
19
+ from mn_cli.libs.ui import print_success_confirmation
17
20
  from mn_cli.shared import client, console, logger
18
21
 
19
22
  SCHEMA_VERSION = "mn.backup.v1"
@@ -69,8 +72,12 @@ def backup(
69
72
  _merge_cli_source_metadata(backup_payload, target)
70
73
 
71
74
  archive_path = _write_backup_archive(backup_payload, bundle_files, output, target)
72
- console.print(f"[green]Backup written:[/green] {archive_path}")
73
- console.print(f"Source job: [bold]{job_id}[/bold]")
75
+ print_success_confirmation(
76
+ console,
77
+ "Job backup",
78
+ details=[("Backup", archive_path), ("Source job", job_id)],
79
+ next_steps=f"mn job restore <blueprint-id> --input {archive_path}",
80
+ )
74
81
  except BackupRestoreError as exc:
75
82
  console.print(f"[red]{exc}[/red]")
76
83
  raise typer.Exit(1)
@@ -119,12 +126,17 @@ def restore(
119
126
  knowledge_files,
120
127
  )
121
128
 
122
- console.print(f"[green]Restored backup as paused job:[/green] {new_job_id}")
123
- console.print(f"New run: [bold]{new_run_id}[/bold]")
124
- console.print(
125
- f"Original job: [bold]{result.get('source_job_id', 'unknown')}[/bold]"
129
+ print_success_confirmation(
130
+ console,
131
+ "Job restore",
132
+ status="paused",
133
+ details=[
134
+ ("Job ID", new_job_id),
135
+ ("Run ID", new_run_id),
136
+ ("Original job", result.get("source_job_id", "unknown")),
137
+ ],
138
+ next_steps=f"mn job resume {new_job_id}",
126
139
  )
127
- console.print(f"Resume when ready: [bold]mn job resume {new_job_id}[/bold]")
128
140
  except BackupRestoreError as exc:
129
141
  console.print(f"[red]{exc}[/red]")
130
142
  raise typer.Exit(1)
@@ -228,7 +240,7 @@ def _job_from_get_job_payload(payload: Any) -> dict[str, Any]:
228
240
 
229
241
 
230
242
  def _runs_root() -> Path:
231
- return Path(os.getenv("MN_RUNS_ROOT") or "~/.mn/runs").expanduser()
243
+ return default_runs_root()
232
244
 
233
245
 
234
246
  def _load_run_record(run_id: str) -> dict[str, Any] | None: