@magpiecloud/mags 1.5.0 → 1.6.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.
Files changed (42) hide show
  1. package/API.md +381 -0
  2. package/Mags-API.postman_collection.json +374 -0
  3. package/QUICKSTART.md +283 -0
  4. package/README.md +287 -79
  5. package/bin/mags.js +161 -27
  6. package/deploy-page.sh +171 -0
  7. package/index.js +1 -163
  8. package/mags +0 -0
  9. package/mags.sh +270 -0
  10. package/nodejs/README.md +191 -0
  11. package/nodejs/bin/mags.js +1146 -0
  12. package/nodejs/index.js +326 -0
  13. package/nodejs/package.json +42 -0
  14. package/package.json +4 -15
  15. package/python/INTEGRATION.md +747 -0
  16. package/python/README.md +139 -0
  17. package/python/dist/magpie_mags-1.0.0-py3-none-any.whl +0 -0
  18. package/python/dist/magpie_mags-1.0.0.tar.gz +0 -0
  19. package/python/examples/demo.py +181 -0
  20. package/python/pyproject.toml +39 -0
  21. package/python/src/magpie_mags.egg-info/PKG-INFO +164 -0
  22. package/python/src/magpie_mags.egg-info/SOURCES.txt +9 -0
  23. package/python/src/magpie_mags.egg-info/dependency_links.txt +1 -0
  24. package/python/src/magpie_mags.egg-info/requires.txt +1 -0
  25. package/python/src/magpie_mags.egg-info/top_level.txt +1 -0
  26. package/python/src/mags/__init__.py +6 -0
  27. package/python/src/mags/client.py +283 -0
  28. package/skill.md +153 -0
  29. package/website/api.html +927 -0
  30. package/website/claude-skill.html +483 -0
  31. package/website/cookbook/hn-marketing.html +410 -0
  32. package/website/cookbook/hn-marketing.sh +50 -0
  33. package/website/cookbook.html +278 -0
  34. package/website/env.js +4 -0
  35. package/website/index.html +718 -0
  36. package/website/llms.txt +242 -0
  37. package/website/login.html +88 -0
  38. package/website/mags.md +171 -0
  39. package/website/script.js +425 -0
  40. package/website/styles.css +845 -0
  41. package/website/tokens.html +171 -0
  42. package/website/usage.html +187 -0
@@ -0,0 +1,283 @@
1
+ """Mags client for interacting with the Magpie VM infrastructure API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import requests
11
+
12
+
13
+ class MagsError(Exception):
14
+ """Raised when the Mags API returns an error."""
15
+
16
+ def __init__(self, message: str, status_code: int | None = None):
17
+ super().__init__(message)
18
+ self.status_code = status_code
19
+
20
+
21
+ class Mags:
22
+ """Client for the Mags API.
23
+
24
+ Args:
25
+ api_token: API token. Falls back to ``MAGS_API_TOKEN`` or ``MAGS_TOKEN`` env vars.
26
+ api_url: API base URL. Falls back to ``MAGS_API_URL`` env var or
27
+ ``https://api.magpiecloud.com``.
28
+ timeout: Default request timeout in seconds.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ api_token: str | None = None,
34
+ api_url: str | None = None,
35
+ timeout: int = 30,
36
+ ):
37
+ self.api_url = (
38
+ api_url
39
+ or os.environ.get("MAGS_API_URL")
40
+ or "https://api.magpiecloud.com"
41
+ ).rstrip("/")
42
+
43
+ self.api_token = (
44
+ api_token
45
+ or os.environ.get("MAGS_API_TOKEN")
46
+ or os.environ.get("MAGS_TOKEN")
47
+ )
48
+ if not self.api_token:
49
+ raise MagsError(
50
+ "API token required. Set MAGS_API_TOKEN env var or pass api_token."
51
+ )
52
+
53
+ self.timeout = timeout
54
+ self._session = requests.Session()
55
+ self._session.headers.update(
56
+ {
57
+ "Authorization": f"Bearer {self.api_token}",
58
+ "Content-Type": "application/json",
59
+ }
60
+ )
61
+
62
+ # ── helpers ──────────────────────────────────────────────────────
63
+
64
+ def _request(
65
+ self,
66
+ method: str,
67
+ path: str,
68
+ json: Any = None,
69
+ params: dict | None = None,
70
+ timeout: int | None = None,
71
+ ) -> Any:
72
+ url = f"{self.api_url}/api/v1{path}"
73
+ resp = self._session.request(
74
+ method,
75
+ url,
76
+ json=json,
77
+ params=params,
78
+ timeout=timeout or self.timeout,
79
+ )
80
+ if resp.status_code >= 400:
81
+ try:
82
+ body = resp.json()
83
+ msg = body.get("error") or body.get("message") or resp.text
84
+ except Exception:
85
+ msg = resp.text
86
+ raise MagsError(msg, status_code=resp.status_code)
87
+ if not resp.content:
88
+ return {}
89
+ return resp.json()
90
+
91
+ # ── jobs ─────────────────────────────────────────────────────────
92
+
93
+ def run(
94
+ self,
95
+ script: str,
96
+ *,
97
+ name: str | None = None,
98
+ workspace_id: str | None = None,
99
+ base_workspace_id: str | None = None,
100
+ persistent: bool = False,
101
+ ephemeral: bool = False,
102
+ startup_command: str | None = None,
103
+ environment: Dict[str, str] | None = None,
104
+ file_ids: List[str] | None = None,
105
+ ) -> dict:
106
+ """Submit a job for execution.
107
+
108
+ Returns ``{"request_id": ..., "status": "accepted"}``.
109
+ """
110
+ if ephemeral and workspace_id:
111
+ raise MagsError("Cannot use ephemeral with workspace_id")
112
+ if ephemeral and persistent:
113
+ raise MagsError("Cannot use ephemeral with persistent")
114
+
115
+ payload = {
116
+ "script": script,
117
+ "type": "inline",
118
+ "persistent": persistent,
119
+ }
120
+ if name:
121
+ payload["name"] = name
122
+ if not ephemeral and workspace_id:
123
+ payload["workspace_id"] = workspace_id
124
+ if base_workspace_id:
125
+ payload["base_workspace_id"] = base_workspace_id
126
+ if startup_command:
127
+ payload["startup_command"] = startup_command
128
+ if environment:
129
+ payload["environment"] = environment
130
+ if file_ids:
131
+ payload["file_ids"] = file_ids
132
+
133
+ return self._request("POST", "/mags-jobs", json=payload)
134
+
135
+ def run_and_wait(
136
+ self,
137
+ script: str,
138
+ *,
139
+ timeout: float = 60.0,
140
+ poll_interval: float = 1.0,
141
+ **run_kwargs: Any,
142
+ ) -> dict:
143
+ """Submit a job and block until it completes or times out.
144
+
145
+ Returns a dict with ``request_id``, ``status``, ``exit_code``,
146
+ ``duration_ms``, and ``logs``.
147
+ """
148
+ result = self.run(script, **run_kwargs)
149
+ request_id = result["request_id"]
150
+
151
+ deadline = time.monotonic() + timeout
152
+ while time.monotonic() < deadline:
153
+ status = self.status(request_id)
154
+ if status["status"] in ("completed", "error"):
155
+ logs_resp = self.logs(request_id)
156
+ return {
157
+ "request_id": request_id,
158
+ "status": status["status"],
159
+ "exit_code": status.get("exit_code", 0),
160
+ "duration_ms": status.get("script_duration_ms", 0),
161
+ "logs": logs_resp.get("logs", []),
162
+ }
163
+ time.sleep(poll_interval)
164
+
165
+ raise MagsError(f"Job {request_id} timed out after {timeout}s")
166
+
167
+ def status(self, request_id: str) -> dict:
168
+ """Get the status of a job."""
169
+ return self._request("GET", f"/mags-jobs/{request_id}/status")
170
+
171
+ def logs(self, request_id: str) -> dict:
172
+ """Get logs for a job. Returns ``{"logs": [...]}`."""
173
+ return self._request("GET", f"/mags-jobs/{request_id}/logs")
174
+
175
+ def list_jobs(self, *, page: int = 1, page_size: int = 20) -> dict:
176
+ """List recent jobs. Returns ``{"jobs": [...], "total": N, ...}``."""
177
+ return self._request(
178
+ "GET", "/mags-jobs", params={"page": page, "page_size": page_size}
179
+ )
180
+
181
+ def update_job(self, request_id: str, *, startup_command: str) -> dict:
182
+ """Update a job (e.g. set startup command for wake-from-sleep)."""
183
+ return self._request(
184
+ "PATCH", f"/mags-jobs/{request_id}", json={"startup_command": startup_command}
185
+ )
186
+
187
+ def enable_access(self, request_id: str, *, port: int = 8080) -> dict:
188
+ """Enable external access (URL or SSH) for a persistent job's VM.
189
+
190
+ Use ``port=22`` for SSH access, or ``port=8080`` (default) for HTTP/URL access.
191
+ """
192
+ return self._request(
193
+ "POST", f"/mags-jobs/{request_id}/access", json={"port": port}
194
+ )
195
+
196
+ def usage(self, *, window_days: int = 30) -> dict:
197
+ """Get aggregated usage summary."""
198
+ return self._request(
199
+ "GET", "/mags-jobs/usage", params={"window_days": window_days}
200
+ )
201
+
202
+ # ── file uploads ─────────────────────────────────────────────────
203
+
204
+ def upload_file(self, file_path: str) -> str:
205
+ """Upload a single file. Returns the file ID."""
206
+ p = Path(file_path)
207
+ if not p.exists():
208
+ raise MagsError(f"File not found: {file_path}")
209
+
210
+ url = f"{self.api_url}/api/v1/mags-files"
211
+ # Use a fresh request without the JSON content-type
212
+ resp = requests.post(
213
+ url,
214
+ files={"file": (p.name, p.read_bytes(), "application/octet-stream")},
215
+ headers={"Authorization": f"Bearer {self.api_token}"},
216
+ timeout=self.timeout,
217
+ )
218
+ if resp.status_code >= 400:
219
+ raise MagsError(resp.text, status_code=resp.status_code)
220
+ data = resp.json()
221
+ file_id = data.get("file_id")
222
+ if not file_id:
223
+ raise MagsError(f"Upload failed for {p.name}: {data}")
224
+ return file_id
225
+
226
+ def upload_files(self, file_paths: List[str]) -> List[str]:
227
+ """Upload multiple files. Returns a list of file IDs."""
228
+ return [self.upload_file(fp) for fp in file_paths]
229
+
230
+ # ── workspaces ────────────────────────────────────────────────────
231
+
232
+ def list_workspaces(self) -> dict:
233
+ """List all workspaces. Returns ``{"workspaces": [...], "total": N}``."""
234
+ return self._request("GET", "/mags-workspaces")
235
+
236
+ def delete_workspace(self, workspace_id: str) -> dict:
237
+ """Delete a workspace and all its stored data.
238
+
239
+ This permanently removes the workspace filesystem from S3.
240
+ Active jobs using the workspace must be stopped first.
241
+ """
242
+ return self._request("DELETE", f"/mags-workspaces/{workspace_id}")
243
+
244
+ # ── cron jobs ────────────────────────────────────────────────────
245
+
246
+ def cron_create(
247
+ self,
248
+ *,
249
+ name: str,
250
+ cron_expression: str,
251
+ script: str,
252
+ workspace_id: str | None = None,
253
+ environment: Dict[str, str] | None = None,
254
+ persistent: bool = False,
255
+ ) -> dict:
256
+ """Create a scheduled cron job."""
257
+ payload = {
258
+ "name": name,
259
+ "cron_expression": cron_expression,
260
+ "script": script,
261
+ "persistent": persistent,
262
+ }
263
+ if workspace_id:
264
+ payload["workspace_id"] = workspace_id
265
+ if environment:
266
+ payload["environment"] = environment
267
+ return self._request("POST", "/mags-cron", json=payload)
268
+
269
+ def cron_list(self) -> dict:
270
+ """List all cron jobs."""
271
+ return self._request("GET", "/mags-cron")
272
+
273
+ def cron_get(self, cron_id: str) -> dict:
274
+ """Get a cron job by ID."""
275
+ return self._request("GET", f"/mags-cron/{cron_id}")
276
+
277
+ def cron_update(self, cron_id: str, **updates: Any) -> dict:
278
+ """Update a cron job. Pass fields as keyword arguments."""
279
+ return self._request("PATCH", f"/mags-cron/{cron_id}", json=updates)
280
+
281
+ def cron_delete(self, cron_id: str) -> dict:
282
+ """Delete a cron job."""
283
+ return self._request("DELETE", f"/mags-cron/{cron_id}")
package/skill.md ADDED
@@ -0,0 +1,153 @@
1
+ # Mags - Instant VM Execution
2
+
3
+ Execute scripts instantly on Magpie's microVM infrastructure. VMs boot in <100ms from a warm pool.
4
+
5
+ ## Installation
6
+
7
+ Add this skill to your Claude Code project:
8
+
9
+ ```bash
10
+ # Copy to your project's .claude/commands folder
11
+ cp -r mags ~/.claude/commands/
12
+ ```
13
+
14
+ Or add to a specific project:
15
+ ```bash
16
+ cp -r mags /path/to/project/.claude/commands/
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Set your API token:
22
+ ```bash
23
+ export MAGS_API_TOKEN="your-token-here"
24
+ ```
25
+
26
+ Or create a `.env` file:
27
+ ```
28
+ MAGS_API_TOKEN=your-token-here
29
+ MAGS_API_URL=https://api.magpiecloud.com
30
+ ```
31
+
32
+ ## CLI Commands
33
+
34
+ ### Basic Execution
35
+ ```bash
36
+ # Run a simple command
37
+ mags run "echo Hello World"
38
+
39
+ # Run with persistent workspace (changes persist to S3)
40
+ mags run -w my-project "apk add nodejs npm && npm init -y"
41
+
42
+ # Run again - nodejs is already installed!
43
+ mags run -w my-project "node --version"
44
+ ```
45
+
46
+ ### Persistent VMs with URL Access
47
+ ```bash
48
+ # Deploy a web server with public URL
49
+ mags run -w webapp -p --url "python3 -m http.server 8080"
50
+ # Returns: https://abc123.apps.magpiecloud.com
51
+
52
+ # With startup command (runs when VM wakes from sleep)
53
+ mags run -w webapp -p --url --startup-command "npm start" "npm install && npm start"
54
+
55
+ # Custom port
56
+ mags run -w webapp -p --url --port 3000 "npm start"
57
+ ```
58
+
59
+ ### Other Commands
60
+ ```bash
61
+ # Check job status
62
+ mags status <request_id>
63
+
64
+ # View job logs
65
+ mags logs <request_id>
66
+
67
+ # List recent jobs
68
+ mags list
69
+
70
+ # Enable URL access for existing persistent job
71
+ mags url <request_id>
72
+ mags url <request_id> --port 3000
73
+ mags url <request_id> --startup "npm start"
74
+
75
+ # SSH into a workspace
76
+ mags ssh my-project
77
+ ```
78
+
79
+ ## CLI Flags
80
+
81
+ | Flag | Description |
82
+ |------|-------------|
83
+ | `-w, --workspace` | Workspace ID for persistent storage |
84
+ | `-p, --persistent` | Keep VM alive for URL/SSH access |
85
+ | `--url` | Enable public URL access (requires -p) |
86
+ | `--port` | Port to expose (default: 8080) |
87
+ | `--startup-command` | Command to run when VM wakes from sleep |
88
+ | `-e, --ephemeral` | Truly ephemeral (no S3 sync, faster) |
89
+ | `-t, --timeout` | Timeout in seconds (default: 300) |
90
+
91
+ ## API Reference
92
+
93
+ Base URL: `https://api.magpiecloud.com/api/v1`
94
+
95
+ | Endpoint | Method | Description |
96
+ |----------|--------|-------------|
97
+ | `/mags-jobs` | POST | Submit a new job |
98
+ | `/mags-jobs` | GET | List jobs |
99
+ | `/mags-jobs/{id}/status` | GET | Get job status |
100
+ | `/mags-jobs/{id}/logs` | GET | Get job logs |
101
+ | `/mags-jobs/{id}/access` | POST | Enable SSH/URL access |
102
+ | `/mags-jobs/{id}` | PATCH | Update job (startup command) |
103
+
104
+ ### Submit Job
105
+ ```bash
106
+ curl -X POST https://api.magpiecloud.com/api/v1/mags-jobs \
107
+ -H "Authorization: Bearer $MAGS_API_TOKEN" \
108
+ -H "Content-Type: application/json" \
109
+ -d '{
110
+ "script": "echo hello",
111
+ "type": "inline",
112
+ "workspace_id": "my-workspace",
113
+ "persistent": true,
114
+ "startup_command": "python3 -m http.server 8080"
115
+ }'
116
+ ```
117
+
118
+ ### Enable URL Access
119
+ ```bash
120
+ curl -X POST https://api.magpiecloud.com/api/v1/mags-jobs/{id}/access \
121
+ -H "Authorization: Bearer $MAGS_API_TOKEN" \
122
+ -H "Content-Type: application/json" \
123
+ -d '{"port": 8080}'
124
+ ```
125
+
126
+ ## Response Times
127
+
128
+ - **Warm start**: <100ms (VM from pool)
129
+ - **Cold start**: ~4 seconds (VM boot from scratch)
130
+ - **Script overhead**: ~50ms
131
+
132
+ ## Examples
133
+
134
+ ### Deploy a Python Web Server
135
+ ```bash
136
+ mags run -w webserver -p --url \
137
+ "echo '<h1>Hello from Mags!</h1>' > ~/index.html && python3 -m http.server 8080"
138
+ ```
139
+
140
+ ### Deploy Node.js App
141
+ ```bash
142
+ mags run -w nodeapp -p --url --port 3000 --startup-command "npm start" \
143
+ "apk add nodejs npm && npm install && npm start"
144
+ ```
145
+
146
+ ### Run Tests with Dependencies
147
+ ```bash
148
+ # First run installs dependencies
149
+ mags run -w myproject "apk add python3 py3-pip && pip install pytest requests"
150
+
151
+ # Subsequent runs are instant
152
+ mags run -w myproject "pytest tests/"
153
+ ```