monarch-cloud 0.1.0__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.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: monarch-cloud
3
+ Version: 0.1.0
4
+ Summary: Public developer CLI for Monarch Cloud
5
+ Author: Africa Compute Fund
6
+ Project-URL: Homepage, https://app.africacomputefund.com
7
+ Project-URL: Documentation, https://app.africacomputefund.com/docs
8
+ Keywords: monarch,cloud,compute,developer-tools,cli
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: httpx<1,>=0.24
16
+
17
+ # Monarch Cloud CLI
18
+
19
+ A lightweight public developer client for the existing Monarch REST Platform API.
20
+
21
+ ```bash
22
+ python -m pip install monarch-cloud
23
+ monarch login
24
+ monarch whoami
25
+ monarch catalog
26
+ monarch gpus
27
+ monarch models --scope catalog
28
+ monarch jobs
29
+ monarch wallet
30
+ monarch init "My app" --blueprint backend
31
+ monarch status
32
+ monarch dev
33
+ monarch deploy hello ./hello.js
34
+ ```
35
+
36
+ **Authentication:** `monarch login` prompts for an existing Monarch REST API key
37
+ without echoing it, validates it against `GET /v1/account/identity`, then saves
38
+ it in a mode-0600 file under your user configuration directory. For ephemeral
39
+ environments set `MONARCH_API_KEY` instead; this takes precedence. This is REST
40
+ API-key login, **not** the OAuth browser flow used by the MCP connector. Key
41
+ creation and account access are provided through Monarch Cloud.
42
+
43
+ `monarch mcp` prints the public OAuth MCP endpoint for compatible AI clients;
44
+ it does not bundle an MCP server or impersonate an OAuth token.
45
+
46
+ ## Command surfaces
47
+
48
+ - Discovery: `catalog`, `gpus`, `models`, `datasets`, `jobs`, `job ID`,
49
+ `wallet`, `inference-models`, `whoami`.
50
+ - Projects: `init`, `status`, `dev`, `deploy NAME SOURCE_JS`.
51
+ - Migration: `migrate`, `migrate-storage`, `migrate-postgres`,
52
+ `migrate-gcs`, `import-documents`.
53
+ - Account: `login`, `logout`, `mcp`.
54
+
55
+ `deploy` deploys one JavaScript **Project Function**, not arbitrary whole-app
56
+ infrastructure. Some public REST APIs and the MCP's purpose-built tools are
57
+ not yet wrapped as CLI commands; Cloud, REST and MCP remain the source of truth.
58
+
59
+ Project selection is stored in `.monarch/project.json`. The one-time Project
60
+ server key is saved in `.monarch/server-key` and must never be committed.
61
+ Source migration secrets are only read from environment variables or named
62
+ credential files, not CLI arguments. The server enforces the same account,
63
+ scope, billing, quotas, and approvals as its other public clients.
64
+
65
+ ## For maintainers
66
+
67
+ The public release is built from the **canonical** implementation at
68
+ `scripts/monarch_platform.py` using
69
+ `python scripts/package_monarch_cloud.py --build`. The staging tool copies
70
+ only this public module, the package metadata, and this README into an isolated
71
+ temporary build directory. The private `./monarch` operator CLI, admin scripts,
72
+ deployment infrastructure, and internal MCP server are not packaged.
73
+
74
+ There is no CI publisher and no GitHub Actions workflow. The owner publishes
75
+ the reviewed files to PyPI explicitly after checking the build. See
76
+ `docs/operations/MONARCH_CLOUD_PYPI.md` for release instructions.
@@ -0,0 +1,60 @@
1
+ # Monarch Cloud CLI
2
+
3
+ A lightweight public developer client for the existing Monarch REST Platform API.
4
+
5
+ ```bash
6
+ python -m pip install monarch-cloud
7
+ monarch login
8
+ monarch whoami
9
+ monarch catalog
10
+ monarch gpus
11
+ monarch models --scope catalog
12
+ monarch jobs
13
+ monarch wallet
14
+ monarch init "My app" --blueprint backend
15
+ monarch status
16
+ monarch dev
17
+ monarch deploy hello ./hello.js
18
+ ```
19
+
20
+ **Authentication:** `monarch login` prompts for an existing Monarch REST API key
21
+ without echoing it, validates it against `GET /v1/account/identity`, then saves
22
+ it in a mode-0600 file under your user configuration directory. For ephemeral
23
+ environments set `MONARCH_API_KEY` instead; this takes precedence. This is REST
24
+ API-key login, **not** the OAuth browser flow used by the MCP connector. Key
25
+ creation and account access are provided through Monarch Cloud.
26
+
27
+ `monarch mcp` prints the public OAuth MCP endpoint for compatible AI clients;
28
+ it does not bundle an MCP server or impersonate an OAuth token.
29
+
30
+ ## Command surfaces
31
+
32
+ - Discovery: `catalog`, `gpus`, `models`, `datasets`, `jobs`, `job ID`,
33
+ `wallet`, `inference-models`, `whoami`.
34
+ - Projects: `init`, `status`, `dev`, `deploy NAME SOURCE_JS`.
35
+ - Migration: `migrate`, `migrate-storage`, `migrate-postgres`,
36
+ `migrate-gcs`, `import-documents`.
37
+ - Account: `login`, `logout`, `mcp`.
38
+
39
+ `deploy` deploys one JavaScript **Project Function**, not arbitrary whole-app
40
+ infrastructure. Some public REST APIs and the MCP's purpose-built tools are
41
+ not yet wrapped as CLI commands; Cloud, REST and MCP remain the source of truth.
42
+
43
+ Project selection is stored in `.monarch/project.json`. The one-time Project
44
+ server key is saved in `.monarch/server-key` and must never be committed.
45
+ Source migration secrets are only read from environment variables or named
46
+ credential files, not CLI arguments. The server enforces the same account,
47
+ scope, billing, quotas, and approvals as its other public clients.
48
+
49
+ ## For maintainers
50
+
51
+ The public release is built from the **canonical** implementation at
52
+ `scripts/monarch_platform.py` using
53
+ `python scripts/package_monarch_cloud.py --build`. The staging tool copies
54
+ only this public module, the package metadata, and this README into an isolated
55
+ temporary build directory. The private `./monarch` operator CLI, admin scripts,
56
+ deployment infrastructure, and internal MCP server are not packaged.
57
+
58
+ There is no CI publisher and no GitHub Actions workflow. The owner publishes
59
+ the reviewed files to PyPI explicitly after checking the build. See
60
+ `docs/operations/MONARCH_CLOUD_PYPI.md` for release instructions.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "monarch-cloud"
7
+ version = "0.1.0"
8
+ description = "Public developer CLI for Monarch Cloud"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["httpx>=0.24,<1"]
12
+ authors = [{name = "Africa Compute Fund"}]
13
+ keywords = ["monarch", "cloud", "compute", "developer-tools", "cli"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://app.africacomputefund.com"
23
+ Documentation = "https://app.africacomputefund.com/docs"
24
+
25
+ [project.scripts]
26
+ monarch = "monarch_cloud.cli:entrypoint"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+ include = ["monarch_cloud*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Monarch Cloud public developer client."""
@@ -0,0 +1,843 @@
1
+ #!/usr/bin/env python3
2
+ """Unified Monarch Platform CLI for developers and agents.
3
+
4
+ The CLI is intentionally a thin client over Monarch's public Platform API. It
5
+ never bypasses server-side policy, billing, readiness, or authority checks.
6
+
7
+ Secrets:
8
+ - MONARCH_API_KEY supplies the developer/account credential.
9
+ - One-time Project server keys are written to .monarch/server-key with mode 0600.
10
+ - Source migration credentials stay in environment variables, never argv.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import getpass
17
+ import json
18
+ import os
19
+ import re
20
+ import stat
21
+ import sys
22
+ import uuid
23
+ from pathlib import Path
24
+ from typing import Any
25
+ from urllib.parse import urlsplit
26
+
27
+ import httpx
28
+
29
+ DEFAULT_API_BASE = "https://app.africacomputefund.com"
30
+ DEFAULT_STATE_DIR = Path(".monarch")
31
+ PUBLIC_MCP_URL = "https://app.africacomputefund.com/mcp"
32
+ PROJECT_FILE = "project.json"
33
+ SERVER_KEY_FILE = "server-key"
34
+
35
+
36
+ class MonarchCLIError(RuntimeError):
37
+ pass
38
+
39
+
40
+ def _api_base(value: str | None) -> str:
41
+ base = str(value or os.getenv("MONARCH_API_BASE_URL") or DEFAULT_API_BASE).rstrip("/")
42
+ url = urlsplit(base)
43
+ allowed_local = url.scheme == "http" and url.hostname in {"localhost", "127.0.0.1", "::1"}
44
+ if not (url.scheme == "https" or allowed_local) or not url.hostname:
45
+ raise MonarchCLIError("Monarch API URL must use HTTPS (HTTP allowed only for localhost)")
46
+ if url.username or url.password or url.query or url.fragment:
47
+ raise MonarchCLIError("Invalid Monarch API URL: credentials, query and fragment are not accepted")
48
+ return base
49
+
50
+
51
+ def _credentials_path() -> Path:
52
+ override = os.getenv("MONARCH_CONFIG_HOME")
53
+ if override:
54
+ return Path(override).expanduser() / "credentials.json"
55
+ return Path(os.getenv("XDG_CONFIG_HOME") or Path.home() / ".config").expanduser() / "monarch" / "credentials.json"
56
+
57
+
58
+ def _token(required: bool = True) -> str:
59
+ value = str(os.getenv("MONARCH_API_KEY") or "").strip()
60
+ if not value:
61
+ path = _credentials_path()
62
+ if path.is_symlink():
63
+ raise MonarchCLIError("Refusing to read a symlinked credentials file")
64
+ if path.exists():
65
+ if os.name != "nt" and stat.S_IMODE(path.stat().st_mode) & 0o077:
66
+ raise MonarchCLIError("Credentials must have private permissions (0600)")
67
+ try:
68
+ payload = json.loads(path.read_text(encoding="utf-8"))
69
+ value = str(payload.get("api_key") or "").strip() if isinstance(payload, dict) else ""
70
+ except (ValueError, OSError) as exc:
71
+ raise MonarchCLIError("Credentials file is unreadable") from exc
72
+ if required and not value:
73
+ raise MonarchCLIError("Run 'monarch login' or set MONARCH_API_KEY")
74
+ return value
75
+
76
+
77
+ def _required_env(name: str) -> str:
78
+ value = str(os.getenv(name) or "").strip()
79
+ if not value:
80
+ raise MonarchCLIError(f"{name} is required")
81
+ return value
82
+
83
+
84
+ def _json_print(value: Any) -> None:
85
+ print(json.dumps(value, indent=2, sort_keys=True, default=str))
86
+
87
+
88
+ def _compact_print(value: Any) -> None:
89
+ print(json.dumps(value, separators=(",", ":"), default=str))
90
+
91
+
92
+ def _request(
93
+ method: str,
94
+ path: str,
95
+ *,
96
+ api_base: str,
97
+ token: str | None = None,
98
+ body: dict[str, Any] | None = None,
99
+ timeout: float = 120.0,
100
+ idempotency_key: str | None = None,
101
+ ) -> dict[str, Any]:
102
+ headers = {"Accept": "application/json", "X-Request-ID": str(uuid.uuid4())}
103
+ if token:
104
+ headers["Authorization"] = f"Bearer {token}"
105
+ if body is not None:
106
+ headers["Content-Type"] = "application/json"
107
+ if idempotency_key:
108
+ headers["Idempotency-Key"] = idempotency_key
109
+ try:
110
+ with httpx.Client(timeout=httpx.Timeout(timeout, connect=30.0), follow_redirects=False) as client:
111
+ response = client.request(method.upper(), api_base + path, headers=headers, json=body)
112
+ except httpx.HTTPError as exc:
113
+ raise MonarchCLIError("Monarch could not be reached") from exc
114
+
115
+ try:
116
+ payload: Any = response.json()
117
+ except Exception:
118
+ payload = {"message": response.text[:1000]}
119
+
120
+ if response.status_code >= 400:
121
+ message = "Monarch request failed"
122
+ code = None
123
+ if isinstance(payload, dict):
124
+ detail = payload.get("detail") or payload.get("error") or payload
125
+ if isinstance(detail, dict):
126
+ message = str(detail.get("error") or detail.get("message") or message)
127
+ code = detail.get("code")
128
+ elif isinstance(detail, str):
129
+ message = detail
130
+ suffix = f" [{code}]" if code else ""
131
+ raise MonarchCLIError(f"{message}{suffix} (HTTP {response.status_code})")
132
+ if not isinstance(payload, (dict, list)):
133
+ raise MonarchCLIError("Monarch returned an unexpected response")
134
+ return payload
135
+
136
+
137
+ def _state_dir(value: str | None) -> Path:
138
+ return Path(value or DEFAULT_STATE_DIR).expanduser()
139
+
140
+
141
+ def _project_path(state_dir: Path) -> Path:
142
+ return state_dir / PROJECT_FILE
143
+
144
+
145
+ def _server_key_path(state_dir: Path) -> Path:
146
+ return state_dir / SERVER_KEY_FILE
147
+
148
+
149
+ def _read_project(state_dir: Path) -> dict[str, Any]:
150
+ path = _project_path(state_dir)
151
+ if not path.exists():
152
+ return {}
153
+ try:
154
+ value = json.loads(path.read_text(encoding="utf-8"))
155
+ except Exception as exc:
156
+ raise MonarchCLIError(f"Project state is unreadable: {path}") from exc
157
+ if not isinstance(value, dict):
158
+ raise MonarchCLIError(f"Project state must be a JSON object: {path}")
159
+ return value
160
+
161
+
162
+ def _project_id(args: argparse.Namespace, state_dir: Path) -> str:
163
+ explicit = str(getattr(args, "project_id", "") or "").strip()
164
+ if explicit:
165
+ return explicit
166
+ stored = str(_read_project(state_dir).get("project_id") or "").strip()
167
+ if stored:
168
+ return stored
169
+ raise MonarchCLIError("No Project selected. Run 'monarch init' or pass --project-id.")
170
+
171
+
172
+ def _write_private(path: Path, value: str) -> None:
173
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
174
+ if path.is_symlink():
175
+ raise MonarchCLIError("Refusing to write a symlinked private file")
176
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
177
+ if hasattr(os, "O_NOFOLLOW"):
178
+ flags |= os.O_NOFOLLOW
179
+ fd = os.open(path, flags, 0o600)
180
+ if hasattr(os, "fchmod"):
181
+ os.fchmod(fd, 0o600)
182
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
183
+ stream.write(value.rstrip("\n") + "\n")
184
+ try:
185
+ path.chmod(stat.S_IRUSR | stat.S_IWUSR)
186
+ except OSError:
187
+ pass
188
+
189
+
190
+ def _write_project_state(state_dir: Path, payload: dict[str, Any]) -> None:
191
+ state_dir.mkdir(parents=True, exist_ok=True)
192
+ _project_path(state_dir).write_text(
193
+ json.dumps(payload, indent=2, sort_keys=True) + "\n",
194
+ encoding="utf-8",
195
+ )
196
+
197
+
198
+ def cmd_login(args: argparse.Namespace) -> int:
199
+ key = getpass.getpass("Monarch REST API key (hidden): ").strip()
200
+ if not key:
201
+ raise MonarchCLIError("No API key entered")
202
+ identity = _request("GET", "/v1/account/identity", api_base=_api_base(args.api_base_url), token=key)
203
+ _write_private(_credentials_path(), json.dumps({"api_key": key}))
204
+ _json_print({"ok": True, "identity": identity})
205
+ return 0
206
+
207
+
208
+ def cmd_logout(args: argparse.Namespace) -> int:
209
+ path = _credentials_path()
210
+ if path.is_symlink():
211
+ raise MonarchCLIError("Refusing to remove symlinked credentials")
212
+ path.unlink(missing_ok=True)
213
+ _json_print({"ok": True, "note": "MONARCH_API_KEY environment variables are unaffected"})
214
+ return 0
215
+
216
+
217
+ def cmd_whoami(args: argparse.Namespace) -> int:
218
+ _json_print(_request("GET", "/v1/account/identity", api_base=_api_base(args.api_base_url), token=_token()))
219
+ return 0
220
+
221
+
222
+ def cmd_discover(args: argparse.Namespace) -> int:
223
+ paths = {"gpus": "/v1/gpus", "models": "/v1/models", "datasets": "/v1/datasets",
224
+ "jobs": "/v1/jobs", "wallet": "/v1/account/balance",
225
+ "inference-models": "/v1/inference/catalog"}
226
+ path = paths[args.command]
227
+ if args.command == "models" and args.scope:
228
+ path += "?scope=" + args.scope
229
+ _json_print(_request("GET", path, api_base=_api_base(args.api_base_url), token=_token()))
230
+ return 0
231
+
232
+
233
+ def cmd_job(args: argparse.Namespace) -> int:
234
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", args.job_id):
235
+ raise MonarchCLIError("Invalid training job identifier")
236
+ _json_print(_request("GET", f"/v1/jobs/{args.job_id}", api_base=_api_base(args.api_base_url), token=_token()))
237
+ return 0
238
+
239
+
240
+ def cmd_mcp(args: argparse.Namespace) -> int:
241
+ _json_print({"url": PUBLIC_MCP_URL, "authentication": "OAuth",
242
+ "note": "Connect this URL in an MCP-compatible client; no operator tools are exposed."})
243
+ return 0
244
+
245
+
246
+ def cmd_init(args: argparse.Namespace) -> int:
247
+ api_base = _api_base(args.api_base_url)
248
+ token = _token()
249
+ state_dir = _state_dir(args.state_dir)
250
+ existing = _read_project(state_dir)
251
+ if existing and not args.force:
252
+ raise MonarchCLIError(
253
+ f"{_project_path(state_dir)} already selects Project {existing.get('project_id')}; use --force to replace local selection"
254
+ )
255
+ body = {
256
+ "name": args.name,
257
+ "blueprint_id": args.blueprint,
258
+ "region": args.region,
259
+ "key_name": args.key_name,
260
+ }
261
+ if args.org_id:
262
+ body["org_id"] = args.org_id
263
+ result = _request("POST", "/v1/platform/launch", api_base=api_base, token=token, body=body)
264
+ project = result.get("project") if isinstance(result.get("project"), dict) else {}
265
+ key = result.get("server_key") if isinstance(result.get("server_key"), dict) else {}
266
+ project_id = str(project.get("project_id") or "").strip()
267
+ if not project_id:
268
+ raise MonarchCLIError("Monarch launch response did not include project_id")
269
+ secret = str(key.get("secret") or "").strip()
270
+ state = {
271
+ "api_base_url": api_base,
272
+ "project_id": project_id,
273
+ "name": project.get("name"),
274
+ "blueprint_id": project.get("blueprint_id"),
275
+ "region": project.get("region"),
276
+ }
277
+ _write_project_state(state_dir, state)
278
+ if secret:
279
+ _write_private(_server_key_path(state_dir), secret)
280
+ output = {
281
+ "ok": True,
282
+ "project": state,
283
+ "server_key_file": str(_server_key_path(state_dir)) if secret else None,
284
+ "bindings": result.get("bindings") or [],
285
+ "warnings": result.get("warnings") or [],
286
+ "next": [
287
+ "monarch status",
288
+ "monarch dev",
289
+ "monarch deploy-function <name> <source.js>",
290
+ ],
291
+ }
292
+ _json_print(output)
293
+ return 0
294
+
295
+
296
+ def cmd_status(args: argparse.Namespace) -> int:
297
+ state_dir = _state_dir(args.state_dir)
298
+ stored = _read_project(state_dir)
299
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
300
+ token = _token()
301
+ project_id = _project_id(args, state_dir)
302
+ runtime = _request("GET", f"/v1/projects/{project_id}/runtime", api_base=api_base, token=token)
303
+ usage = _request("GET", f"/v1/projects/{project_id}/usage", api_base=api_base, token=token)
304
+ bindings = _request("GET", f"/v1/projects/{project_id}/bindings", api_base=api_base, token=token)
305
+ _json_print({"project_id": project_id, "runtime": runtime, "usage": usage, "bindings": bindings})
306
+ return 0
307
+
308
+
309
+ def cmd_dev(args: argparse.Namespace) -> int:
310
+ state_dir = _state_dir(args.state_dir)
311
+ stored = _read_project(state_dir)
312
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
313
+ token = _token()
314
+ project_id = _project_id(args, state_dir)
315
+ config = _request("GET", f"/v1/projects/{project_id}/client-config", api_base=api_base, token=token)
316
+ runtime = _request("GET", f"/v1/projects/{project_id}/runtime", api_base=api_base, token=token)
317
+ _json_print({
318
+ "project_id": project_id,
319
+ "client_config": config,
320
+ "runtime": runtime,
321
+ "server_key_file": str(_server_key_path(state_dir)) if _server_key_path(state_dir).exists() else None,
322
+ "note": "Use the public client config in browser/mobile code. Keep the server key on trusted backend infrastructure only.",
323
+ })
324
+ return 0
325
+
326
+
327
+ def cmd_deploy_function(args: argparse.Namespace) -> int:
328
+ state_dir = _state_dir(args.state_dir)
329
+ stored = _read_project(state_dir)
330
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
331
+ token = _token()
332
+ project_id = _project_id(args, state_dir)
333
+ source_path = Path(args.source).expanduser()
334
+ source = source_path.read_text(encoding="utf-8")
335
+ body = {
336
+ "name": args.name,
337
+ "source": source,
338
+ "scopes": args.scope or [],
339
+ }
340
+ result = _request(
341
+ "POST",
342
+ f"/v1/projects/{project_id}/functions",
343
+ api_base=api_base,
344
+ token=token,
345
+ body=body,
346
+ timeout=180,
347
+ idempotency_key=args.idempotency_key or str(uuid.uuid4()),
348
+ )
349
+ _json_print(result)
350
+ return 0
351
+
352
+
353
+ def _services(value: str) -> list[str]:
354
+ return [item.strip() for item in str(value or "").split(",") if item.strip()]
355
+
356
+
357
+ def cmd_migrate(args: argparse.Namespace) -> int:
358
+ state_dir = _state_dir(args.state_dir)
359
+ stored = _read_project(state_dir)
360
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
361
+ token = _token()
362
+ services = _services(args.services)
363
+ analyze = _request(
364
+ "POST",
365
+ "/v1/platform/migrations/analyze",
366
+ api_base=api_base,
367
+ token=token,
368
+ body={"provider": args.provider, "services": services},
369
+ )
370
+ manifest = _request(
371
+ "POST",
372
+ "/v1/platform/migrations/manifest",
373
+ api_base=api_base,
374
+ token=token,
375
+ body={"provider": args.provider, "services": services, "project_name": args.name},
376
+ )
377
+ output: dict[str, Any] = {"analysis": analyze, "manifest": manifest}
378
+ if args.prepare:
379
+ body: dict[str, Any] = {
380
+ "provider": args.provider,
381
+ "services": services,
382
+ "name": args.name,
383
+ "region": args.region,
384
+ "key_name": args.key_name,
385
+ }
386
+ if args.org_id:
387
+ body["org_id"] = args.org_id
388
+ prepared = _request(
389
+ "POST",
390
+ "/v1/platform/migrations/prepare",
391
+ api_base=api_base,
392
+ token=token,
393
+ body=body,
394
+ timeout=180,
395
+ )
396
+ output["prepared"] = prepared
397
+ project = prepared.get("project") if isinstance(prepared.get("project"), dict) else {}
398
+ key = prepared.get("server_key") if isinstance(prepared.get("server_key"), dict) else {}
399
+ project_id = str(project.get("project_id") or "").strip()
400
+ if project_id:
401
+ _write_project_state(state_dir, {
402
+ "api_base_url": api_base,
403
+ "project_id": project_id,
404
+ "name": project.get("name"),
405
+ "blueprint_id": project.get("blueprint_id"),
406
+ "region": project.get("region"),
407
+ "migration_source": args.provider,
408
+ })
409
+ secret = str(key.get("secret") or "").strip()
410
+ if secret:
411
+ _write_private(_server_key_path(state_dir), secret)
412
+ if isinstance(output.get("prepared"), dict) and secret:
413
+ output["prepared"] = dict(output["prepared"])
414
+ server_key = output["prepared"].get("server_key")
415
+ if isinstance(server_key, dict):
416
+ output["prepared"]["server_key"] = {k: v for k, v in server_key.items() if k != "secret"}
417
+ output["prepared"]["server_key_file"] = str(_server_key_path(state_dir))
418
+ _json_print(output)
419
+ return 0
420
+
421
+
422
+ def cmd_migrate_storage(args: argparse.Namespace) -> int:
423
+ state_dir = _state_dir(args.state_dir)
424
+ stored = _read_project(state_dir)
425
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
426
+ token = _token()
427
+ project_id = _project_id(args, state_dir)
428
+ source_access = _required_env("MONARCH_MIGRATION_SOURCE_ACCESS_KEY_ID")
429
+ source_secret = _required_env("MONARCH_MIGRATION_SOURCE_SECRET_ACCESS_KEY")
430
+ source_session = str(os.getenv("MONARCH_MIGRATION_SOURCE_SESSION_TOKEN") or "").strip() or None
431
+
432
+ cursor = str(args.start_after or "")
433
+ totals = {"copied": 0, "skipped": 0, "bytes_copied": 0}
434
+ batch_number = 0
435
+ while True:
436
+ batch_number += 1
437
+ body: dict[str, Any] = {
438
+ "source_bucket": args.bucket,
439
+ "source_access_key_id": source_access,
440
+ "source_secret_access_key": source_secret,
441
+ "source_prefix": args.prefix,
442
+ "start_after": cursor,
443
+ "max_objects": args.max_objects,
444
+ "max_batch_bytes": args.max_batch_bytes,
445
+ "dry_run": bool(args.dry_run),
446
+ }
447
+ if source_session:
448
+ body["source_session_token"] = source_session
449
+ if args.endpoint:
450
+ body["source_endpoint"] = args.endpoint
451
+ if args.source_region:
452
+ body["source_region"] = args.source_region
453
+ if args.target_region:
454
+ body["target_region"] = args.target_region
455
+
456
+ result = _request(
457
+ "POST",
458
+ f"/v1/projects/{project_id}/migrations/object-storage/s3",
459
+ api_base=api_base,
460
+ token=token,
461
+ body=body,
462
+ timeout=300,
463
+ idempotency_key=str(uuid.uuid4()),
464
+ )
465
+ totals["copied"] += int(result.get("copied") or 0)
466
+ totals["skipped"] += int(result.get("skipped") or 0)
467
+ totals["bytes_copied"] += int(result.get("bytes_copied") or 0)
468
+ _compact_print({
469
+ "batch": batch_number,
470
+ "copied": result.get("copied"),
471
+ "skipped": result.get("skipped"),
472
+ "bytes_copied": result.get("bytes_copied"),
473
+ "complete": result.get("complete"),
474
+ "next_start_after": result.get("next_start_after"),
475
+ })
476
+ if bool(result.get("complete")) or args.one_batch:
477
+ _json_print({
478
+ "ok": bool(result.get("complete")) or bool(args.one_batch),
479
+ "dry_run": bool(args.dry_run),
480
+ "batches": batch_number,
481
+ **totals,
482
+ "complete": bool(result.get("complete")),
483
+ "resume_after": result.get("next_start_after"),
484
+ })
485
+ return 0
486
+ next_cursor = str(result.get("next_start_after") or "")
487
+ if not next_cursor or next_cursor == cursor:
488
+ raise MonarchCLIError("Migration cursor did not advance; refusing an infinite retry loop")
489
+ cursor = next_cursor
490
+
491
+
492
+ def cmd_migrate_postgres(args: argparse.Namespace) -> int:
493
+ state_dir = _state_dir(args.state_dir)
494
+ stored = _read_project(state_dir)
495
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
496
+ token = _token()
497
+ project_id = _project_id(args, state_dir)
498
+ dsn = _required_env("MONARCH_MIGRATION_POSTGRES_DSN")
499
+ cursor = str(args.start_after or "")
500
+ totals = {"imported": 0, "skipped": 0, "rows_considered": 0}
501
+ batches = 0
502
+ while True:
503
+ batches += 1
504
+ body = {
505
+ "source_dsn": dsn,
506
+ "table": args.table,
507
+ "schema_name": args.schema,
508
+ "primary_key": args.primary_key,
509
+ "collection": args.collection,
510
+ "start_after": cursor or None,
511
+ "max_rows": args.max_rows,
512
+ "mode": args.mode,
513
+ "dry_run": bool(args.dry_run),
514
+ }
515
+ result = _request(
516
+ "POST",
517
+ f"/v1/projects/{project_id}/migrations/database/postgres",
518
+ api_base=api_base,
519
+ token=token,
520
+ body=body,
521
+ timeout=300,
522
+ idempotency_key=str(uuid.uuid4()),
523
+ )
524
+ totals["imported"] += int(result.get("imported") or 0)
525
+ totals["skipped"] += int(result.get("skipped") or 0)
526
+ totals["rows_considered"] += int(result.get("rows_considered") or 0)
527
+ _compact_print({
528
+ "batch": batches,
529
+ "imported": result.get("imported"),
530
+ "skipped": result.get("skipped"),
531
+ "rows_considered": result.get("rows_considered"),
532
+ "complete": result.get("complete"),
533
+ "next_start_after": result.get("next_start_after"),
534
+ })
535
+ if bool(result.get("complete")) or args.one_batch:
536
+ _json_print({
537
+ "ok": bool(result.get("complete")) or bool(args.one_batch),
538
+ "dry_run": bool(args.dry_run),
539
+ "batches": batches,
540
+ **totals,
541
+ "complete": bool(result.get("complete")),
542
+ "resume_after": result.get("next_start_after"),
543
+ "semantic_target": "project_documents",
544
+ "relational_semantics_preserved": False,
545
+ })
546
+ return 0
547
+ next_cursor = str(result.get("next_start_after") or "")
548
+ if not next_cursor or next_cursor == cursor:
549
+ raise MonarchCLIError("PostgreSQL migration cursor did not advance")
550
+ cursor = next_cursor
551
+
552
+
553
+ def _gcs_credential(args: argparse.Namespace) -> str:
554
+ if args.credentials_file:
555
+ return Path(args.credentials_file).expanduser().read_text(encoding="utf-8")
556
+ return _required_env("MONARCH_MIGRATION_GCS_SERVICE_ACCOUNT_JSON")
557
+
558
+
559
+ def cmd_migrate_gcs(args: argparse.Namespace) -> int:
560
+ state_dir = _state_dir(args.state_dir)
561
+ stored = _read_project(state_dir)
562
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
563
+ token = _token()
564
+ project_id = _project_id(args, state_dir)
565
+ credential = _gcs_credential(args)
566
+ cursor = str(args.start_after or "")
567
+ totals = {"copied": 0, "skipped": 0, "bytes_copied": 0}
568
+ batches = 0
569
+ while True:
570
+ batches += 1
571
+ body = {
572
+ "source_bucket": args.bucket,
573
+ "source_service_account_json": credential,
574
+ "source_prefix": args.prefix,
575
+ "start_after": cursor,
576
+ "max_objects": args.max_objects,
577
+ "max_batch_bytes": args.max_batch_bytes,
578
+ "target_region": args.target_region,
579
+ "dry_run": bool(args.dry_run),
580
+ }
581
+ result = _request(
582
+ "POST",
583
+ f"/v1/projects/{project_id}/migrations/object-storage/gcs",
584
+ api_base=api_base,
585
+ token=token,
586
+ body=body,
587
+ timeout=300,
588
+ idempotency_key=str(uuid.uuid4()),
589
+ )
590
+ totals["copied"] += int(result.get("copied") or 0)
591
+ totals["skipped"] += int(result.get("skipped") or 0)
592
+ totals["bytes_copied"] += int(result.get("bytes_copied") or 0)
593
+ _compact_print({
594
+ "batch": batches,
595
+ "copied": result.get("copied"),
596
+ "skipped": result.get("skipped"),
597
+ "bytes_copied": result.get("bytes_copied"),
598
+ "complete": result.get("complete"),
599
+ "next_start_after": result.get("next_start_after"),
600
+ })
601
+ if bool(result.get("complete")) or args.one_batch:
602
+ _json_print({
603
+ "ok": bool(result.get("complete")) or bool(args.one_batch),
604
+ "dry_run": bool(args.dry_run),
605
+ "batches": batches,
606
+ **totals,
607
+ "complete": bool(result.get("complete")),
608
+ "resume_after": result.get("next_start_after"),
609
+ })
610
+ return 0
611
+ next_cursor = str(result.get("next_start_after") or "")
612
+ if not next_cursor or next_cursor == cursor:
613
+ raise MonarchCLIError("GCS migration cursor did not advance")
614
+ cursor = next_cursor
615
+
616
+
617
+ def _load_document_items(path: Path, collection_override: str | None) -> list[dict[str, Any]]:
618
+ raw = path.read_text(encoding="utf-8")
619
+ if path.suffix.lower() in {".jsonl", ".ndjson"}:
620
+ values = [json.loads(line) for line in raw.splitlines() if line.strip()]
621
+ else:
622
+ parsed = json.loads(raw)
623
+ if isinstance(parsed, dict) and isinstance(parsed.get("items"), list):
624
+ values = parsed["items"]
625
+ elif isinstance(parsed, list):
626
+ values = parsed
627
+ else:
628
+ raise MonarchCLIError("Document import JSON must be an array or an object with an items array")
629
+ items: list[dict[str, Any]] = []
630
+ for index, value in enumerate(values):
631
+ if not isinstance(value, dict):
632
+ raise MonarchCLIError(f"Document item {index} must be an object")
633
+ collection = str(collection_override or value.get("collection") or "").strip()
634
+ document_id = str(value.get("document_id") or value.get("id") or "").strip()
635
+ if not collection or not document_id:
636
+ raise MonarchCLIError(f"Document item {index} needs collection and document_id/id")
637
+ if isinstance(value.get("data"), dict):
638
+ data = dict(value["data"])
639
+ elif collection_override:
640
+ data = {
641
+ str(key): item
642
+ for key, item in value.items()
643
+ if key not in {"collection", "document_id", "id"}
644
+ }
645
+ else:
646
+ raise MonarchCLIError(f"Document item {index} needs a data object")
647
+ items.append({"collection": collection, "document_id": document_id, "data": data})
648
+ return items
649
+
650
+
651
+ def cmd_import_documents(args: argparse.Namespace) -> int:
652
+ state_dir = _state_dir(args.state_dir)
653
+ stored = _read_project(state_dir)
654
+ api_base = _api_base(args.api_base_url or stored.get("api_base_url"))
655
+ token = _token()
656
+ project_id = _project_id(args, state_dir)
657
+ source_path = Path(args.source).expanduser()
658
+ items = _load_document_items(source_path, args.collection)
659
+ imported = skipped = 0
660
+ batches = 0
661
+ for offset in range(0, len(items), args.batch_size):
662
+ batches += 1
663
+ batch = items[offset : offset + args.batch_size]
664
+ result = _request(
665
+ "POST",
666
+ f"/v1/projects/{project_id}/migrations/documents",
667
+ api_base=api_base,
668
+ token=token,
669
+ body={
670
+ "items": batch,
671
+ "mode": args.mode,
672
+ "dry_run": bool(args.dry_run),
673
+ "source": args.source_label,
674
+ },
675
+ timeout=180,
676
+ idempotency_key=str(uuid.uuid4()),
677
+ )
678
+ imported += int(result.get("imported") or 0)
679
+ skipped += int(result.get("skipped") or 0)
680
+ _compact_print({
681
+ "batch": batches,
682
+ "items": len(batch),
683
+ "imported": result.get("imported"),
684
+ "skipped": result.get("skipped"),
685
+ "dry_run": result.get("dry_run"),
686
+ })
687
+ _json_print({
688
+ "ok": True,
689
+ "source_file": str(source_path),
690
+ "dry_run": bool(args.dry_run),
691
+ "batches": batches,
692
+ "items": len(items),
693
+ "imported": imported,
694
+ "skipped": skipped,
695
+ })
696
+ return 0
697
+
698
+
699
+ def cmd_catalog(args: argparse.Namespace) -> int:
700
+ api_base = _api_base(args.api_base_url)
701
+ payload = _request("GET", "/v1/platform/catalog", api_base=api_base, token=None)
702
+ _json_print(payload)
703
+ return 0
704
+
705
+
706
+ def build_parser() -> argparse.ArgumentParser:
707
+ parser = argparse.ArgumentParser(prog="monarch", description="Monarch Platform developer and agent CLI")
708
+ parser.add_argument("--api-base-url", default=None)
709
+ parser.add_argument("--state-dir", default=str(DEFAULT_STATE_DIR))
710
+ sub = parser.add_subparsers(dest="command", required=True)
711
+
712
+ login = sub.add_parser("login", help="Validate and securely save your Monarch REST API key")
713
+ login.set_defaults(func=cmd_login)
714
+ logout = sub.add_parser("logout", help="Delete the local developer API key")
715
+ logout.set_defaults(func=cmd_logout)
716
+ whoami = sub.add_parser("whoami", help="Show your authenticated Monarch identity")
717
+ whoami.set_defaults(func=cmd_whoami)
718
+ for name, help_text in (
719
+ ("gpus", "List customer-visible GPU offerings"),
720
+ ("datasets", "List accessible datasets"),
721
+ ("jobs", "List your training jobs"),
722
+ ("wallet", "Read account wallet balance"),
723
+ ("inference-models", "List hosted inference models"),
724
+ ):
725
+ public = sub.add_parser(name, help=help_text)
726
+ public.set_defaults(func=cmd_discover)
727
+ models = sub.add_parser("models", help="List available or owned models")
728
+ models.add_argument("--scope", choices=["catalog", "mine"], default=None)
729
+ models.set_defaults(func=cmd_discover)
730
+ job = sub.add_parser("job", help="Inspect one training job")
731
+ job.add_argument("job_id")
732
+ job.set_defaults(func=cmd_job)
733
+ mcp = sub.add_parser("mcp", help="Show the public OAuth MCP endpoint")
734
+ mcp.set_defaults(func=cmd_mcp)
735
+
736
+ init = sub.add_parser("init", help="Launch a Project from a Blueprint")
737
+ init.add_argument("name")
738
+ init.add_argument("--blueprint", default="backend")
739
+ init.add_argument("--region", default="auto")
740
+ init.add_argument("--org-id", default=None)
741
+ init.add_argument("--key-name", default="local backend")
742
+ init.add_argument("--force", action="store_true")
743
+ init.set_defaults(func=cmd_init)
744
+
745
+ status = sub.add_parser("status", help="Inspect Project runtime, usage and bindings")
746
+ status.add_argument("--project-id", default=None)
747
+ status.set_defaults(func=cmd_status)
748
+
749
+ dev = sub.add_parser("dev", help="Show public client configuration and runtime readiness")
750
+ dev.add_argument("--project-id", default=None)
751
+ dev.set_defaults(func=cmd_dev)
752
+
753
+ deploy = sub.add_parser("deploy", aliases=["deploy-function"], help="Deploy or replace one Project Function")
754
+ deploy.add_argument("name")
755
+ deploy.add_argument("source")
756
+ deploy.add_argument("--project-id", default=None)
757
+ deploy.add_argument("--scope", action="append", default=[])
758
+ deploy.add_argument("--idempotency-key", default=None)
759
+ deploy.set_defaults(func=cmd_deploy_function)
760
+
761
+ migrate = sub.add_parser("migrate", help="Analyze and optionally prepare a migration target")
762
+ migrate.add_argument("provider")
763
+ migrate.add_argument("--services", default="")
764
+ migrate.add_argument("--name", default="Migrated app")
765
+ migrate.add_argument("--region", default="auto")
766
+ migrate.add_argument("--org-id", default=None)
767
+ migrate.add_argument("--key-name", default="migration backend")
768
+ migrate.add_argument("--prepare", action="store_true")
769
+ migrate.set_defaults(func=cmd_migrate)
770
+
771
+ migrate_storage = sub.add_parser("migrate-storage", help="Copy S3-compatible objects into the selected Project")
772
+ migrate_storage.add_argument("bucket")
773
+ migrate_storage.add_argument("--project-id", default=None)
774
+ migrate_storage.add_argument("--endpoint", default=None)
775
+ migrate_storage.add_argument("--source-region", default=None)
776
+ migrate_storage.add_argument("--target-region", default=None)
777
+ migrate_storage.add_argument("--prefix", default="")
778
+ migrate_storage.add_argument("--start-after", default="")
779
+ migrate_storage.add_argument("--max-objects", type=int, default=25)
780
+ migrate_storage.add_argument("--max-batch-bytes", type=int, default=128 * 1024 * 1024)
781
+ migrate_storage.add_argument("--dry-run", action="store_true")
782
+ migrate_storage.add_argument("--one-batch", action="store_true")
783
+ migrate_storage.set_defaults(func=cmd_migrate_storage)
784
+
785
+ migrate_postgres = sub.add_parser("migrate-postgres", help="Copy PostgreSQL table rows into Project documents")
786
+ migrate_postgres.add_argument("table")
787
+ migrate_postgres.add_argument("--project-id", default=None)
788
+ migrate_postgres.add_argument("--schema", default="public")
789
+ migrate_postgres.add_argument("--primary-key", default=None)
790
+ migrate_postgres.add_argument("--collection", default=None)
791
+ migrate_postgres.add_argument("--start-after", default="")
792
+ migrate_postgres.add_argument("--max-rows", type=int, default=100)
793
+ migrate_postgres.add_argument("--mode", choices=["upsert", "skip_existing"], default="upsert")
794
+ migrate_postgres.add_argument("--dry-run", action="store_true")
795
+ migrate_postgres.add_argument("--one-batch", action="store_true")
796
+ migrate_postgres.set_defaults(func=cmd_migrate_postgres)
797
+
798
+ migrate_gcs = sub.add_parser("migrate-gcs", help="Copy GCS/Firebase Storage objects into the selected Project")
799
+ migrate_gcs.add_argument("bucket")
800
+ migrate_gcs.add_argument("--project-id", default=None)
801
+ migrate_gcs.add_argument("--credentials-file", default=None)
802
+ migrate_gcs.add_argument("--target-region", default=None)
803
+ migrate_gcs.add_argument("--prefix", default="")
804
+ migrate_gcs.add_argument("--start-after", default="")
805
+ migrate_gcs.add_argument("--max-objects", type=int, default=25)
806
+ migrate_gcs.add_argument("--max-batch-bytes", type=int, default=128 * 1024 * 1024)
807
+ migrate_gcs.add_argument("--dry-run", action="store_true")
808
+ migrate_gcs.add_argument("--one-batch", action="store_true")
809
+ migrate_gcs.set_defaults(func=cmd_migrate_gcs)
810
+
811
+ import_documents = sub.add_parser("import-documents", help="Import exported JSON/JSONL documents into the selected Project")
812
+ import_documents.add_argument("source")
813
+ import_documents.add_argument("--project-id", default=None)
814
+ import_documents.add_argument("--collection", default=None)
815
+ import_documents.add_argument("--source-label", default="local_export")
816
+ import_documents.add_argument("--batch-size", type=int, default=100, choices=range(1, 501), metavar="1..500")
817
+ import_documents.add_argument("--mode", choices=["upsert", "skip_existing"], default="upsert")
818
+ import_documents.add_argument("--dry-run", action="store_true")
819
+ import_documents.set_defaults(func=cmd_import_documents)
820
+
821
+ catalog = sub.add_parser("catalog", help="Read public Platform services and pricing")
822
+ catalog.set_defaults(func=cmd_catalog)
823
+ return parser
824
+
825
+
826
+ def main(argv: list[str] | None = None) -> int:
827
+ parser = build_parser()
828
+ args = parser.parse_args(argv)
829
+ return int(args.func(args))
830
+
831
+
832
+ def entrypoint() -> int:
833
+ try:
834
+ return main()
835
+ except KeyboardInterrupt:
836
+ return 130
837
+ except (MonarchCLIError, OSError, ValueError) as exc:
838
+ print(str(exc), file=sys.stderr)
839
+ return 1
840
+
841
+
842
+ if __name__ == "__main__":
843
+ raise SystemExit(entrypoint())
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: monarch-cloud
3
+ Version: 0.1.0
4
+ Summary: Public developer CLI for Monarch Cloud
5
+ Author: Africa Compute Fund
6
+ Project-URL: Homepage, https://app.africacomputefund.com
7
+ Project-URL: Documentation, https://app.africacomputefund.com/docs
8
+ Keywords: monarch,cloud,compute,developer-tools,cli
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: httpx<1,>=0.24
16
+
17
+ # Monarch Cloud CLI
18
+
19
+ A lightweight public developer client for the existing Monarch REST Platform API.
20
+
21
+ ```bash
22
+ python -m pip install monarch-cloud
23
+ monarch login
24
+ monarch whoami
25
+ monarch catalog
26
+ monarch gpus
27
+ monarch models --scope catalog
28
+ monarch jobs
29
+ monarch wallet
30
+ monarch init "My app" --blueprint backend
31
+ monarch status
32
+ monarch dev
33
+ monarch deploy hello ./hello.js
34
+ ```
35
+
36
+ **Authentication:** `monarch login` prompts for an existing Monarch REST API key
37
+ without echoing it, validates it against `GET /v1/account/identity`, then saves
38
+ it in a mode-0600 file under your user configuration directory. For ephemeral
39
+ environments set `MONARCH_API_KEY` instead; this takes precedence. This is REST
40
+ API-key login, **not** the OAuth browser flow used by the MCP connector. Key
41
+ creation and account access are provided through Monarch Cloud.
42
+
43
+ `monarch mcp` prints the public OAuth MCP endpoint for compatible AI clients;
44
+ it does not bundle an MCP server or impersonate an OAuth token.
45
+
46
+ ## Command surfaces
47
+
48
+ - Discovery: `catalog`, `gpus`, `models`, `datasets`, `jobs`, `job ID`,
49
+ `wallet`, `inference-models`, `whoami`.
50
+ - Projects: `init`, `status`, `dev`, `deploy NAME SOURCE_JS`.
51
+ - Migration: `migrate`, `migrate-storage`, `migrate-postgres`,
52
+ `migrate-gcs`, `import-documents`.
53
+ - Account: `login`, `logout`, `mcp`.
54
+
55
+ `deploy` deploys one JavaScript **Project Function**, not arbitrary whole-app
56
+ infrastructure. Some public REST APIs and the MCP's purpose-built tools are
57
+ not yet wrapped as CLI commands; Cloud, REST and MCP remain the source of truth.
58
+
59
+ Project selection is stored in `.monarch/project.json`. The one-time Project
60
+ server key is saved in `.monarch/server-key` and must never be committed.
61
+ Source migration secrets are only read from environment variables or named
62
+ credential files, not CLI arguments. The server enforces the same account,
63
+ scope, billing, quotas, and approvals as its other public clients.
64
+
65
+ ## For maintainers
66
+
67
+ The public release is built from the **canonical** implementation at
68
+ `scripts/monarch_platform.py` using
69
+ `python scripts/package_monarch_cloud.py --build`. The staging tool copies
70
+ only this public module, the package metadata, and this README into an isolated
71
+ temporary build directory. The private `./monarch` operator CLI, admin scripts,
72
+ deployment infrastructure, and internal MCP server are not packaged.
73
+
74
+ There is no CI publisher and no GitHub Actions workflow. The owner publishes
75
+ the reviewed files to PyPI explicitly after checking the build. See
76
+ `docs/operations/MONARCH_CLOUD_PYPI.md` for release instructions.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/monarch_cloud/__init__.py
4
+ src/monarch_cloud/cli.py
5
+ src/monarch_cloud.egg-info/PKG-INFO
6
+ src/monarch_cloud.egg-info/SOURCES.txt
7
+ src/monarch_cloud.egg-info/dependency_links.txt
8
+ src/monarch_cloud.egg-info/entry_points.txt
9
+ src/monarch_cloud.egg-info/requires.txt
10
+ src/monarch_cloud.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ monarch = monarch_cloud.cli:entrypoint
@@ -0,0 +1 @@
1
+ httpx<1,>=0.24
@@ -0,0 +1 @@
1
+ monarch_cloud