@magpiecloud/mags 1.8.5 → 1.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md CHANGED
@@ -212,12 +212,17 @@ HTTP services are also available via subdomain: `https://<subdomain>.apps.magpie
212
212
  PATCH /api/v1/mags-jobs/:id
213
213
  ```
214
214
 
215
- Update a job's startup command (used when waking from sleep).
215
+ Update a job's settings. All fields are optional only include the ones you want to change.
216
216
 
217
217
  **Request:**
218
218
 
219
+ | Field | Type | Description |
220
+ |-------|------|-------------|
221
+ | `startup_command` | string | Command to run when VM wakes from sleep |
222
+ | `no_sleep` | bool | If `true`, VM never auto-sleeps. If `false`, re-enables auto-sleep. Requires persistent VM. |
223
+
219
224
  ```json
220
- { "startup_command": "python3 -m http.server 8080" }
225
+ { "no_sleep": true }
221
226
  ```
222
227
 
223
228
  **Response (200):**
package/QUICKSTART.md CHANGED
@@ -139,6 +139,7 @@ mags run -w webapp-prod "..."
139
139
  | `mags list` | List jobs | `mags list -n 20` |
140
140
  | `mags logs ID` | View job logs | `mags logs abc123` |
141
141
  | `mags status ID` | Job status | `mags status abc123` |
142
+ | `mags set ID` | Update VM settings | `mags set myvm --no-sleep` |
142
143
 
143
144
  ## CLI Flags
144
145
 
package/README.md CHANGED
@@ -107,6 +107,7 @@ mags run -p --url 'python3 -m http.server 8080'
107
107
  | `mags list` | List recent jobs |
108
108
  | `mags url <job-id> [port]` | Enable URL access for a job |
109
109
  | `mags stop <job-id>` | Stop a running job |
110
+ | `mags set <name\|id> [options]` | Update VM settings (`--no-sleep`, `--sleep`) |
110
111
 
111
112
  ### Run Options
112
113
 
package/bin/mags.js CHANGED
@@ -211,6 +211,7 @@ ${colors.bold}Commands:${colors.reset}
211
211
  url alias <sub> <workspace> Create a stable URL alias for a workspace
212
212
  url alias list List your URL aliases
213
213
  url alias remove <subdomain> Delete a URL alias
214
+ set <name|id> [options] Update VM settings
214
215
  stop <name|id> Stop a running job
215
216
  resize <workspace> --disk <GB> Resize a workspace's disk (restarts VM)
216
217
  sync <workspace|id> Sync workspace to S3 (without stopping)
@@ -595,8 +596,8 @@ async function runJob(args) {
595
596
  if (baseWorkspace) log('blue', `Base workspace: ${baseWorkspace} (read-only)`);
596
597
  if (persistent) log('yellow', 'Persistent: VM will stay alive');
597
598
 
598
- // Poll for completion
599
- const maxAttempts = 120;
599
+ // Poll for completion (200ms intervals, 600 attempts = 2 min timeout)
600
+ const maxAttempts = 600;
600
601
  let attempt = 0;
601
602
 
602
603
  while (attempt < maxAttempts) {
@@ -609,7 +610,7 @@ async function runJob(args) {
609
610
  // If --url requested, wait until VM is actually assigned (vm_id populated)
610
611
  if (enableUrl && !status.vm_id) {
611
612
  process.stdout.write('.');
612
- await sleep(1000);
613
+ await sleep(200);
613
614
  attempt++;
614
615
  continue;
615
616
  }
@@ -633,7 +634,7 @@ async function runJob(args) {
633
634
  }
634
635
 
635
636
  process.stdout.write('.');
636
- await sleep(1000);
637
+ await sleep(200);
637
638
  attempt++;
638
639
  }
639
640
 
@@ -829,6 +830,48 @@ async function stopJob(nameOrId) {
829
830
  }
830
831
  }
831
832
 
833
+ async function setJobSettings(args) {
834
+ let nameOrId = null;
835
+ let noSleep = null;
836
+
837
+ for (let i = 0; i < args.length; i++) {
838
+ if (args[i] === '--no-sleep') {
839
+ noSleep = true;
840
+ } else if (args[i] === '--sleep') {
841
+ noSleep = false;
842
+ } else if (!nameOrId) {
843
+ nameOrId = args[i];
844
+ }
845
+ }
846
+
847
+ if (!nameOrId) {
848
+ log('red', 'Error: Job name or ID required');
849
+ console.log(`\nUsage: mags set <name|id> [options]\n`);
850
+ console.log('Options:');
851
+ console.log(' --no-sleep Never auto-sleep this VM');
852
+ console.log(' --sleep Re-enable auto-sleep');
853
+ process.exit(1);
854
+ }
855
+
856
+ const payload = {};
857
+ if (noSleep !== null) payload.no_sleep = noSleep;
858
+
859
+ if (Object.keys(payload).length === 0) {
860
+ log('red', 'Error: No settings to update. Use --no-sleep or --sleep');
861
+ process.exit(1);
862
+ }
863
+
864
+ const requestId = await resolveJobId(nameOrId);
865
+ log('blue', `Updating settings for ${requestId}...`);
866
+ const resp = await request('PATCH', `/api/v1/mags-jobs/${requestId}`, payload);
867
+ if (resp.success) {
868
+ if (noSleep === true) log('green', 'VM set to never auto-sleep');
869
+ if (noSleep === false) log('green', 'VM set to auto-sleep when idle');
870
+ } else {
871
+ log('red', `Failed: ${resp.error || 'unknown error'}`);
872
+ }
873
+ }
874
+
832
875
  async function resizeVM(args) {
833
876
  let name = null;
834
877
  let diskGB = 0;
@@ -1452,7 +1495,7 @@ async function sshToJob(nameOrId) {
1452
1495
  process.exit(1);
1453
1496
  }
1454
1497
  process.stdout.write('.');
1455
- await sleep(1000);
1498
+ await sleep(300);
1456
1499
  }
1457
1500
  console.log('');
1458
1501
  }
@@ -1628,7 +1671,7 @@ async function main() {
1628
1671
  break;
1629
1672
  case '--version':
1630
1673
  case '-v':
1631
- console.log('mags v1.8.5');
1674
+ console.log('mags v1.8.7');
1632
1675
  process.exit(0);
1633
1676
  break;
1634
1677
  case 'new':
@@ -1671,6 +1714,10 @@ async function main() {
1671
1714
  await requireAuth();
1672
1715
  await listJobs();
1673
1716
  break;
1717
+ case 'set':
1718
+ await requireAuth();
1719
+ await setJobSettings(args.slice(1));
1720
+ break;
1674
1721
  case 'stop':
1675
1722
  await requireAuth();
1676
1723
  await stopJob(args[1]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.8.5",
3
+ "version": "1.8.7",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "magpie-mags"
7
- version = "1.3.3"
7
+ version = "1.3.4"
8
8
  description = "Mags SDK - Execute scripts on Magpie's instant VM infrastructure"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magpie-mags
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: Mags SDK - Execute scripts on Magpie's instant VM infrastructure
5
5
  Author: Magpie Cloud
6
6
  License: MIT
@@ -91,6 +91,19 @@ m.run_and_wait(
91
91
  )
92
92
  ```
93
93
 
94
+ ### Always-On VMs
95
+
96
+ ```python
97
+ # VM that never auto-sleeps — stays running 24/7
98
+ job = m.run(
99
+ "python3 server.py",
100
+ workspace_id="my-api",
101
+ persistent=True,
102
+ no_sleep=True,
103
+ )
104
+ # Auto-recovers if the host goes down
105
+ ```
106
+
94
107
  ### Enable URL / SSH Access
95
108
 
96
109
  ```python
@@ -141,8 +154,13 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
141
154
 
142
155
  | Method | Description |
143
156
  |--------|-------------|
144
- | `run(script, **opts)` | Submit a job (returns immediately) |
157
+ | `run(script, **opts)` | Submit a job (`persistent`, `no_sleep`, `workspace_id`, ...) |
145
158
  | `run_and_wait(script, **opts)` | Submit and block until done |
159
+ | `new(name, **opts)` | Create a persistent VM workspace |
160
+ | `find_job(name_or_id)` | Find a running/sleeping job by name or workspace |
161
+ | `exec(name_or_id, command)` | Run a command on an existing VM via SSH |
162
+ | `stop(name_or_id)` | Stop a running job |
163
+ | `resize(workspace, disk_gb)` | Resize a workspace's disk |
146
164
  | `status(request_id)` | Get job status |
147
165
  | `logs(request_id)` | Get job logs |
148
166
  | `list_jobs(page, page_size)` | List recent jobs |
@@ -202,11 +202,26 @@ class Mags:
202
202
  "GET", "/mags-jobs", params={"page": page, "page_size": page_size}
203
203
  )
204
204
 
205
- def update_job(self, request_id: str, *, startup_command: str) -> dict:
206
- """Update a job (e.g. set startup command for wake-from-sleep)."""
207
- return self._request(
208
- "PATCH", f"/mags-jobs/{request_id}", json={"startup_command": startup_command}
209
- )
205
+ def update_job(
206
+ self,
207
+ request_id: str,
208
+ *,
209
+ startup_command: str | None = None,
210
+ no_sleep: bool | None = None,
211
+ ) -> dict:
212
+ """Update a job's settings.
213
+
214
+ Args:
215
+ request_id: The job/workspace ID to update.
216
+ startup_command: Command to run when VM wakes from sleep.
217
+ no_sleep: If True, VM never auto-sleeps. If False, re-enables auto-sleep.
218
+ """
219
+ payload: dict = {}
220
+ if startup_command is not None:
221
+ payload["startup_command"] = startup_command
222
+ if no_sleep is not None:
223
+ payload["no_sleep"] = no_sleep
224
+ return self._request("PATCH", f"/mags-jobs/{request_id}", json=payload)
210
225
 
211
226
  def enable_access(self, request_id: str, *, port: int = 8080) -> dict:
212
227
  """Enable external access (URL or SSH) for a persistent job's VM.
package/website/api.html CHANGED
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <meta name="api-base" content="https://api.magpiecloud.com" />
12
12
  <meta name="auth-base" content="https://api.magpiecloud.com" />
13
- <link rel="stylesheet" href="styles.css?v=2" />
13
+ <link rel="stylesheet" href="styles.css?v=6" />
14
14
  <script src="env.js"></script>
15
15
  <style>
16
16
  .endpoint { margin-bottom: 2.5rem; }
@@ -109,11 +109,11 @@
109
109
  </div>
110
110
  <nav class="nav-links">
111
111
  <a href="index.html">Home</a>
112
- <a href="index.html#quickstart">Quickstart</a>
113
- <a href="index.html#sdk">SDKs</a>
112
+ <a href="index.html#quickstart">Docs</a>
114
113
  <a href="cookbook.html">Cookbook</a>
114
+ <a href="claude-skill.html">Claude Skill</a>
115
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
115
116
  <a href="login.html">Login</a>
116
- <a href="tokens.html">Tokens</a>
117
117
  </nav>
118
118
  <div class="nav-cta">
119
119
  <a class="button ghost" href="tokens.html">Get API token</a>
@@ -617,29 +617,57 @@ const access = await mags.enableAccess(requestId, { port: 8080 });</code></pre>
617
617
  <span class="method patch">PATCH</span>
618
618
  <span class="url-path">/api/v1/mags-jobs/:id</span>
619
619
  </div>
620
- <p>Update a job's startup command (used when waking from sleep).</p>
620
+ <p>Update a job's settings. All fields are optional &mdash; only include the ones you want to change.</p>
621
621
 
622
622
  <p class="response-label">Request body</p>
623
- <pre><code>{ "startup_command": "python3 -m http.server 8080" }</code></pre>
623
+ <div class="ref-table-wrap">
624
+ <table class="ref-table">
625
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
626
+ <tbody>
627
+ <tr><td><code>startup_command</code></td><td>string</td><td>Command to run when VM wakes from sleep</td></tr>
628
+ <tr><td><code>no_sleep</code></td><td>bool</td><td>If <code>true</code>, VM never auto-sleeps. If <code>false</code>, re-enables auto-sleep. Requires persistent VM.</td></tr>
629
+ </tbody>
630
+ </table>
631
+ </div>
632
+ <pre><code>{ "no_sleep": true }</code></pre>
624
633
 
625
634
  <div class="code-tabs tab-group">
626
635
  <p class="response-label">SDK examples</p>
627
636
  <div class="tab-bar">
628
637
  <button class="tab active" data-tab="uj-curl">curl</button>
638
+ <button class="tab" data-tab="uj-cli">CLI</button>
629
639
  <button class="tab" data-tab="uj-py">Python</button>
630
640
  <button class="tab" data-tab="uj-js">Node.js</button>
631
641
  </div>
632
642
  <div class="tab-content active" data-tab="uj-curl">
633
- <pre><code>curl -X PATCH https://api.magpiecloud.com/api/v1/mags-jobs/$ID \
643
+ <pre><code># Enable no-sleep
644
+ curl -X PATCH https://api.magpiecloud.com/api/v1/mags-jobs/$ID \
645
+ -H "Authorization: Bearer $TOKEN" \
646
+ -H "Content-Type: application/json" \
647
+ -d '{ "no_sleep": true }'
648
+
649
+ # Set startup command
650
+ curl -X PATCH https://api.magpiecloud.com/api/v1/mags-jobs/$ID \
634
651
  -H "Authorization: Bearer $TOKEN" \
635
652
  -H "Content-Type: application/json" \
636
653
  -d '{ "startup_command": "python3 -m http.server 8080" }'</code></pre>
654
+ </div>
655
+ <div class="tab-content" data-tab="uj-cli">
656
+ <pre><code># Enable no-sleep on an existing VM
657
+ mags set myvm --no-sleep
658
+
659
+ # Re-enable auto-sleep
660
+ mags set myvm --sleep</code></pre>
637
661
  </div>
638
662
  <div class="tab-content" data-tab="uj-py">
639
- <pre><code>m.update_job(request_id, startup_command="python3 -m http.server 8080")</code></pre>
663
+ <pre><code># Enable no-sleep
664
+ m.update_job(request_id, no_sleep=True)
665
+
666
+ # Set startup command
667
+ m.update_job(request_id, startup_command="python3 -m http.server 8080")</code></pre>
640
668
  </div>
641
669
  <div class="tab-content" data-tab="uj-js">
642
- <pre><code>await mags.updateJob(requestId, { startupCommand: 'python3 -m http.server 8080' });</code></pre>
670
+ <pre><code>await mags.updateJob(requestId, { noSleep: true });</code></pre>
643
671
  </div>
644
672
  </div>
645
673
  </div>
@@ -924,16 +952,15 @@ mags cron remove &lt;id&gt;</code></pre>
924
952
  </div>
925
953
  <div class="footer-links">
926
954
  <a href="index.html">Home</a>
955
+ <a href="cookbook.html">Cookbook</a>
927
956
  <a href="https://pypi.org/project/magpie-mags/" rel="noreferrer">Python SDK</a>
928
957
  <a href="https://www.npmjs.com/package/@magpiecloud/mags" rel="noreferrer">Node.js SDK</a>
929
- <a href="cookbook.html">Cookbook</a>
930
- <a href="tokens.html">Tokens</a>
931
- <a href="usage.html">Usage</a>
958
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
932
959
  <a href="login.html">Login</a>
933
960
  </div>
934
961
  </div>
935
962
  </footer>
936
963
 
937
- <script src="script.js?v=2"></script>
964
+ <script src="script.js?v=7"></script>
938
965
  </body>
939
966
  </html>
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <meta name="api-base" content="https://api.magpiecloud.com" />
12
12
  <meta name="auth-base" content="https://api.magpiecloud.com" />
13
- <link rel="stylesheet" href="styles.css?v=2" />
13
+ <link rel="stylesheet" href="styles.css?v=6" />
14
14
  <script src="env.js"></script>
15
15
  </head>
16
16
  <body>
@@ -24,9 +24,10 @@
24
24
  </div>
25
25
  <nav class="nav-links">
26
26
  <a href="index.html">Home</a>
27
- <a href="index.html#sdk">SDKs</a>
28
- <a href="api.html">API Docs</a>
27
+ <a href="index.html#quickstart">Docs</a>
28
+ <a href="api.html">API</a>
29
29
  <a href="cookbook.html">Cookbook</a>
30
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
30
31
  <a href="login.html">Login</a>
31
32
  </nav>
32
33
  <div class="nav-cta">
@@ -467,17 +468,14 @@ Use workspace "monitors" so the log persists</code></pre>
467
468
  </div>
468
469
  <div class="footer-links">
469
470
  <a href="index.html">Home</a>
470
- <a href="login.html">Login</a>
471
- <a href="usage.html">Usage</a>
472
- <a href="tokens.html">Tokens</a>
471
+ <a href="api.html">API</a>
473
472
  <a href="cookbook.html">Cookbook</a>
474
- <a href="https://magpiecloud.com/docs/mags" rel="noreferrer">Documentation</a>
475
- <a href="https://magpiecloud.com/dashboard" rel="noreferrer">Dashboard</a>
476
- <a href="https://github.com/magpiecloud/mags" rel="noreferrer">GitHub</a>
473
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
474
+ <a href="login.html">Login</a>
477
475
  </div>
478
476
  </div>
479
477
  </footer>
480
478
 
481
- <script src="script.js"></script>
479
+ <script src="script.js?v=7"></script>
482
480
  </body>
483
481
  </html>
@@ -1,50 +1,42 @@
1
1
  #!/bin/sh
2
- # hn-marketing.sh — Fetch top HN stories and filter for marketing-related content
2
+ # hn-marketing.sh — Fetch top 10 HN stories and save to CSV
3
3
  # Dependencies: curl, jq (install with: apk add -q curl jq)
4
4
 
5
5
  set -e
6
6
 
7
- KEYWORDS='marketing|growth hack|SEO|advertising|branding|brand strategy|content strategy|content marketing|acquisition|retention|funnel|conversion|GTM|go-to-market|product.led|newsletter|copywriting|social media|influencer|public relations|DTC|B2B marketing|B2C|demand gen|lead gen|campaign'
7
+ CSV="${HN_CSV:-./hn-top10.csv}"
8
+ TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
8
9
 
9
- echo "=== HN Marketing Digest ==="
10
- echo "Fetching top stories from Hacker News..."
11
- echo ""
12
-
13
- IDS=$(curl -sf "https://hacker-news.firebaseio.com/v0/topstories.json" | jq -r '.[0:100] | .[]')
14
-
15
- if [ -z "$IDS" ]; then
16
- echo "Error: could not fetch story IDs"
17
- exit 1
10
+ # Write CSV header if file doesn't exist
11
+ if [ ! -f "$CSV" ]; then
12
+ echo "timestamp,rank,score,title,url,hn_link" > "$CSV"
18
13
  fi
19
14
 
20
- COUNT=0
15
+ echo "=== HN Top 10 ==="
16
+ echo "Fetched at: $TIMESTAMP"
17
+ echo ""
18
+
19
+ # Get top 10 story IDs
20
+ IDS=$(curl -sf "https://hacker-news.firebaseio.com/v0/topstories.json" | jq -r '.[0:10] | .[]')
21
21
 
22
+ RANK=0
22
23
  for ID in $IDS; do
24
+ RANK=$((RANK + 1))
23
25
  ITEM=$(curl -sf "https://hacker-news.firebaseio.com/v0/item/${ID}.json")
24
- TITLE=$(echo "$ITEM" | jq -r '.title // empty')
25
26
 
26
- if [ -z "$TITLE" ]; then
27
- continue
28
- fi
27
+ TITLE=$(echo "$ITEM" | jq -r '.title // "untitled"' | tr -d '\n\r' | sed 's/,/;/g')
28
+ URL=$(echo "$ITEM" | jq -r '.url // ""' | tr -d '\n\r')
29
+ SCORE=$(echo "$ITEM" | jq -r '.score // 0')
30
+ HN_LINK="https://news.ycombinator.com/item?id=$ID"
29
31
 
30
- MATCH=$(echo "$TITLE" | grep -iE "$KEYWORDS" || true)
32
+ [ -z "$URL" ] && URL="$HN_LINK"
31
33
 
32
- if [ -n "$MATCH" ]; then
33
- URL=$(echo "$ITEM" | jq -r '.url // "https://news.ycombinator.com/item?id='"$ID"'"')
34
- SCORE=$(echo "$ITEM" | jq -r '.score // 0')
35
- TIME=$(echo "$ITEM" | jq -r '.time // 0')
36
- COUNT=$((COUNT + 1))
34
+ echo "[$RANK] $TITLE"
35
+ echo " Score: $SCORE | $URL"
36
+ echo ""
37
37
 
38
- echo "[$COUNT] $TITLE"
39
- echo " Score: $SCORE | Link: $URL"
40
- echo " HN: https://news.ycombinator.com/item?id=$ID"
41
- echo ""
42
- fi
38
+ echo "$TIMESTAMP,$RANK,$SCORE,$TITLE,$URL,$HN_LINK" >> "$CSV"
43
39
  done
44
40
 
45
- if [ "$COUNT" -eq 0 ]; then
46
- echo "No marketing-related stories found in the current top 100."
47
- else
48
- echo "---"
49
- echo "Found $COUNT marketing-related stories."
50
- fi
41
+ echo "---"
42
+ echo "Saved to $CSV"
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <meta name="api-base" content="https://api.magpiecloud.com" />
12
12
  <meta name="auth-base" content="https://api.magpiecloud.com" />
13
- <link rel="stylesheet" href="styles.css?v=2" />
13
+ <link rel="stylesheet" href="styles.css?v=6" />
14
14
  <script src="env.js"></script>
15
15
  </head>
16
16
  <body>
@@ -24,13 +24,11 @@
24
24
  </div>
25
25
  <nav class="nav-links">
26
26
  <a href="index.html">Home</a>
27
- <a href="index.html#quickstart">Quickstart</a>
28
- <a href="index.html#cli">CLI</a>
29
- <a href="index.html#api">API</a>
30
- <a href="login.html">Login</a>
31
- <a href="usage.html">Usage</a>
32
- <a href="tokens.html">Tokens</a>
27
+ <a href="index.html#quickstart">Docs</a>
28
+ <a href="api.html">API</a>
33
29
  <a href="claude-skill.html">Claude Skill</a>
30
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
31
+ <a href="login.html">Login</a>
34
32
  </nav>
35
33
  <div class="nav-cta">
36
34
  <a class="button ghost" href="index.html#quickstart">Get started</a>
@@ -171,18 +169,25 @@ mags logs &lt;job-id&gt;</code></pre>
171
169
  </article>
172
170
  <article class="recipe-item" data-reveal>
173
171
  <div class="recipe-header">
174
- <h3>HN Marketing Digest</h3>
172
+ <h3>HN Top 10 Digest</h3>
175
173
  <span class="pill">Cron</span>
176
174
  </div>
177
- <pre><code># Upload script + install deps
178
- mags run -f cookbook/hn-marketing.sh -w hn-digest \
179
- 'cp hn-marketing.sh /root/ && chmod +x /root/hn-marketing.sh && apk add -q curl jq'
175
+ <pre><code># Upload script + install deps (file lands in /root/)
176
+ mags run -f hn-marketing.sh -w hn-digest \
177
+ 'chmod +x /root/hn-marketing.sh && apk add -q curl jq'
178
+
179
+ # Run it once (saves to /root/hn-top10.csv)
180
+ mags run -w hn-digest 'HN_CSV=/root/hn-top10.csv sh /root/hn-marketing.sh'
180
181
 
181
- # Activate cron (every 2 hours)
182
- mags cron add --name "hn-marketing" \
182
+ # Schedule it (every 2 hours, results append to CSV)
183
+ mags cron add --name "hn-top10" \
183
184
  --schedule "0 */2 * * *" -w hn-digest \
184
- 'sh /root/hn-marketing.sh'</code></pre>
185
- <p class="label"><a href="cookbook/hn-marketing.html">Full recipe + live demo →</a></p>
185
+ 'HN_CSV=/root/hn-top10.csv sh /root/hn-marketing.sh'</code></pre>
186
+ <p class="label">
187
+ <a href="cookbook/hn-marketing.sh" download>Download hn-marketing.sh</a>
188
+ &nbsp;&middot;&nbsp;
189
+ <a href="cookbook/hn-marketing.html">Full recipe + live demo &rarr;</a>
190
+ </p>
186
191
  </article>
187
192
  </div>
188
193
  </div>
@@ -264,15 +269,14 @@ curl -X POST https://api.magpiecloud.com/api/v1/mags-cron \
264
269
  </div>
265
270
  <div class="footer-links">
266
271
  <a href="index.html">Home</a>
267
- <a href="login.html">Login</a>
268
- <a href="usage.html">Usage</a>
269
- <a href="tokens.html">Tokens</a>
272
+ <a href="api.html">API</a>
270
273
  <a href="claude-skill.html">Claude Skill</a>
271
- <a href="https://magpiecloud.com/docs/mags" rel="noreferrer">Documentation</a>
274
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
275
+ <a href="login.html">Login</a>
272
276
  </div>
273
277
  </div>
274
278
  </footer>
275
279
 
276
- <script src="script.js"></script>
280
+ <script src="script.js?v=7"></script>
277
281
  </body>
278
282
  </html>
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <meta name="api-base" content="https://api.magpiecloud.com" />
12
12
  <meta name="auth-base" content="https://api.magpiecloud.com" />
13
- <link rel="stylesheet" href="styles.css?v=5" />
13
+ <link rel="stylesheet" href="styles.css?v=6" />
14
14
  <script src="env.js"></script>
15
15
  </head>
16
16
  <body>
@@ -23,11 +23,10 @@
23
23
  <span class="tag">Secure cloud sandboxes for the AI age</span>
24
24
  </div>
25
25
  <nav class="nav-links">
26
- <a href="#overview">Overview</a>
27
- <a href="#quickstart">Quickstart</a>
28
- <a href="#patterns">Usage</a>
29
- <a href="#sdk">SDKs</a>
30
- <a href="api.html">API Docs</a>
26
+ <a href="#quickstart">Docs</a>
27
+ <a href="api.html">API</a>
28
+ <a href="cookbook.html">Cookbook</a>
29
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
31
30
  <a href="login.html" id="nav-auth-link">Login</a>
32
31
  </nav>
33
32
  <div class="nav-cta">
@@ -41,22 +40,16 @@
41
40
  <section class="hero" id="overview">
42
41
  <div class="container hero-grid">
43
42
  <div class="hero-copy">
44
- <span class="pill">New: Python SDK</span>
45
43
  <h1>Secure sandboxes that boot in milliseconds. Your data stays.</h1>
46
44
  <p class="lead">
47
- Give your AI agents and scripts a safe place to run. Every sandbox is
48
- completely isolated, boots instantly, and syncs your files to the cloud
49
- automatically. Perfect for agents, scheduled jobs, and anything that
50
- needs to run without risk.
45
+ Give your AI agents an instant home. Every sandbox is
46
+ completely isolated, boots in ~300ms, and syncs your files to the cloud
47
+ automatically. CLI, Python, and Node.js &mdash; pick your tool.
51
48
  </p>
52
49
  <div class="hero-actions">
53
50
  <a class="button" href="#quickstart">Quickstart</a>
54
51
  <a class="button ghost" href="api.html">API Reference</a>
55
52
  </div>
56
- <div class="hero-meta">
57
- <span>CLI, Python, and Node.js &mdash; pick your tool.</span>
58
- <span>Sandboxes in ~300ms. Files persist to the cloud between runs.</span>
59
- </div>
60
53
  </div>
61
54
  <div class="hero-card tab-group" aria-label="Mags example">
62
55
  <div class="tab-bar">
@@ -205,6 +198,7 @@ mags login</code></pre>
205
198
  <tr><td><code>mags status &lt;id&gt;</code></td><td>Get job status</td></tr>
206
199
  <tr><td><code>mags logs &lt;id&gt;</code></td><td>Get job output</td></tr>
207
200
  <tr><td><code>mags stop &lt;id&gt;</code></td><td>Stop a running job</td></tr>
201
+ <tr><td><code>mags set &lt;id&gt; [options]</code></td><td>Update VM settings (e.g. <code>--no-sleep</code>, <code>--sleep</code>)</td></tr>
208
202
  <tr><td><code>mags sync &lt;workspace&gt;</code></td><td>Sync workspace to the cloud now</td></tr>
209
203
  <tr><td><code>mags url &lt;id&gt; [port]</code></td><td>Enable public URL access</td></tr>
210
204
  <tr><td><code>mags workspace list</code></td><td>List persistent workspaces</td></tr>
@@ -513,6 +507,45 @@ await mags.cronCreate({
513
507
  </div>
514
508
  </section>
515
509
 
510
+ <!-- ── Always-On Servers ──────────────────────────── -->
511
+ <section class="section" id="always-on">
512
+ <div class="container">
513
+ <div class="section-title">
514
+ <p>Always-On Servers</p>
515
+ <h2>Keep your sandboxes running forever.</h2>
516
+ </div>
517
+ <div class="grid split">
518
+ <article class="panel" data-reveal>
519
+ <h3>Never auto-sleep</h3>
520
+ <p>By default, persistent sandboxes auto-sleep after 10 minutes of inactivity to save resources. With the <code>--no-sleep</code> flag, your VM stays running 24/7 &mdash; perfect for web servers, workers, and background processes.</p>
521
+ <pre><code># CLI
522
+ mags run -w my-api -p --no-sleep --url --port 3000 'node server.js'
523
+
524
+ # Python
525
+ m.run("node server.js",
526
+ workspace_id="my-api", persistent=True, no_sleep=True)
527
+
528
+ # Node.js
529
+ await mags.run('node server.js', {
530
+ workspaceId: 'my-api', persistent: true, noSleep: true,
531
+ });</code></pre>
532
+ </article>
533
+ <article class="panel" data-reveal>
534
+ <h3>Auto-recovery</h3>
535
+ <p>Always-on sandboxes are automatically monitored. If the host goes down, your VM is re-provisioned on a healthy server within ~60 seconds &mdash; no manual intervention needed.</p>
536
+ <h3 style="margin-top:1.2rem">How it works</h3>
537
+ <ul class="list">
538
+ <li>Requires <code>-p</code> (persistent) flag</li>
539
+ <li>VM stays in <code>running</code> state indefinitely</li>
540
+ <li>Combine with <code>--url</code> to expose a public HTTPS endpoint</li>
541
+ <li>Use <code>--startup-command</code> to auto-restart your process if the VM recovers</li>
542
+ <li>Files persist to the cloud via workspace sync</li>
543
+ </ul>
544
+ </article>
545
+ </div>
546
+ </div>
547
+ </section>
548
+
516
549
  <!-- ── SDKs + API ──────────────────────────────────── -->
517
550
  <section class="section" id="sdk">
518
551
  <div class="container">
@@ -692,20 +725,21 @@ console.log(result.logs);</code></pre>
692
725
  <a href="api.html">API Reference</a>
693
726
  <a href="claude-skill.html">Claude Skill</a>
694
727
  <a href="cookbook.html">Cookbook</a>
728
+ <a href="https://discord.gg/3avpC2nS" rel="noreferrer" target="_blank">Discord</a>
695
729
  <a href="https://github.com/magpiecloud/mags/issues" rel="noreferrer">Issues</a>
696
730
  </div>
697
731
  </div>
698
732
  </footer>
699
733
 
700
- <script src="script.js?v=5"></script>
734
+ <script src="script.js?v=7"></script>
701
735
  <script>
702
736
  (function() {
703
737
  var token = localStorage.getItem('microvm-access-token');
704
738
  if (token) {
705
739
  var nav = document.getElementById('nav-auth-link');
706
740
  var cta = document.getElementById('cta-auth-link');
707
- if (nav) { nav.textContent = 'Dashboard'; nav.href = '/dashboard'; }
708
- if (cta) { cta.textContent = 'Dashboard'; cta.href = '/dashboard'; }
741
+ if (nav) { nav.textContent = 'Usage'; nav.href = 'usage.html'; }
742
+ if (cta) { cta.textContent = 'Dashboard'; cta.href = 'usage.html'; }
709
743
  }
710
744
  })();
711
745
  </script>
package/website/llms.txt CHANGED
@@ -35,6 +35,8 @@ mags run 'echo Hello World'
35
35
  - `mags logs <job-id>` — View job logs
36
36
  - `mags status <job-id>` — Check job status
37
37
  - `mags stop <job-id>` — Stop a running job
38
+ - `mags set <name|id> --no-sleep` — Never auto-sleep this VM
39
+ - `mags set <name|id> --sleep` — Re-enable auto-sleep
38
40
 
39
41
  ## Run Options
40
42
 
@@ -4,10 +4,10 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Mags Login</title>
7
- <meta name="description" content="Sign in to Mags with Google or email." />
7
+ <meta name="description" content="Sign in to Mags with Google." />
8
8
  <meta name="api-base" content="https://api.magpiecloud.com" />
9
9
  <meta name="auth-base" content="https://api.magpiecloud.com" />
10
- <link rel="stylesheet" href="styles.css?v=2" />
10
+ <link rel="stylesheet" href="styles.css?v=6" />
11
11
  <script src="env.js"></script>
12
12
  </head>
13
13
  <body>
@@ -21,9 +21,10 @@
21
21
  </div>
22
22
  <nav class="nav-links">
23
23
  <a href="index.html">Home</a>
24
- <a href="index.html#quickstart">Quickstart</a>
25
- <a href="index.html#cli">CLI</a>
26
- <a href="index.html#api">API</a>
24
+ <a href="index.html#quickstart">Docs</a>
25
+ <a href="api.html">API</a>
26
+ <a href="cookbook.html">Cookbook</a>
27
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
27
28
  </nav>
28
29
  <div class="nav-cta">
29
30
  <a class="button ghost" href="index.html">Back to home</a>
@@ -37,30 +38,10 @@
37
38
  <div>
38
39
  <span class="pill">Login</span>
39
40
  <h1 class="auth-title">Sign in to Mags.</h1>
40
- <p class="auth-subtitle">Use Google or your email and password.</p>
41
+ <p class="auth-subtitle">Sign in with your Google account to get started.</p>
41
42
  </div>
42
43
 
43
44
  <button class="button full" type="button" data-google-login>Continue with Google</button>
44
-
45
- <div class="auth-divider"><span>or</span></div>
46
-
47
- <form class="login-form" data-login-form>
48
- <div class="form-group">
49
- <label class="form-label" for="login-email">Email</label>
50
- <input class="input" type="email" id="login-email" name="email" required />
51
- </div>
52
- <div class="form-group">
53
- <label class="form-label" for="login-password">Password</label>
54
- <input class="input" type="password" id="login-password" name="password" required />
55
- </div>
56
- <button class="button full" type="submit">Sign in</button>
57
- <p class="form-message" data-login-message></p>
58
- </form>
59
-
60
- <div class="form-links">
61
- <a data-auth-link="/auth/forgot-password" href="#">Forgot password?</a>
62
- <a data-auth-link="/auth/register" href="#">Create an account</a>
63
- </div>
64
45
  </div>
65
46
  </div>
66
47
  </main>
@@ -76,13 +57,52 @@
76
57
  </div>
77
58
  <div class="footer-links">
78
59
  <a href="index.html">Home</a>
79
- <a href="usage.html">Usage</a>
80
- <a href="tokens.html">Tokens</a>
81
- <a href="claude-skill.html">Claude Skill</a>
60
+ <a href="api.html">API</a>
61
+ <a href="cookbook.html">Cookbook</a>
62
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
82
63
  </div>
83
64
  </div>
84
65
  </footer>
85
66
 
86
- <script src="script.js"></script>
67
+ <script src="script.js?v=7"></script>
68
+ <script>
69
+ (function() {
70
+ // Handle logout: clear tokens when ?logout=1 is present
71
+ var params = new URLSearchParams(window.location.search);
72
+ if (params.get('logout') === '1') {
73
+ localStorage.removeItem('microvm-access-token');
74
+ localStorage.removeItem('microvm-refresh-token');
75
+ // Clean up the URL
76
+ window.history.replaceState({}, '', 'login.html');
77
+ return;
78
+ }
79
+
80
+ // If already logged in, redirect to next or show dashboard link
81
+ var token = localStorage.getItem('microvm-access-token');
82
+ if (token) {
83
+ var next = params.get('next');
84
+ if (next && next.startsWith('/')) {
85
+ window.location.replace(next);
86
+ return;
87
+ }
88
+ var shell = document.querySelector('.auth-shell');
89
+ if (shell) {
90
+ shell.innerHTML =
91
+ '<div>' +
92
+ '<span class="pill">Logged in</span>' +
93
+ '<h1 class="auth-title">You\'re signed in.</h1>' +
94
+ '<p class="auth-subtitle">You\'re already logged in to Mags.</p>' +
95
+ '</div>' +
96
+ '<a class="button full" href="usage.html">Go to Dashboard</a>' +
97
+ '<button class="button ghost full" type="button" id="logout-btn" style="margin-top:0.75rem">Sign out</button>';
98
+ document.getElementById('logout-btn').addEventListener('click', function() {
99
+ localStorage.removeItem('microvm-access-token');
100
+ localStorage.removeItem('microvm-refresh-token');
101
+ window.location.reload();
102
+ });
103
+ }
104
+ }
105
+ })();
106
+ </script>
87
107
  </body>
88
108
  </html>
package/website/mags.md CHANGED
@@ -71,6 +71,12 @@ mags url <name-or-id> [port]
71
71
  mags stop <name-or-id>
72
72
  ```
73
73
 
74
+ ### Update VM settings
75
+ ```bash
76
+ mags set <name-or-id> --no-sleep # Never auto-sleep
77
+ mags set <name-or-id> --sleep # Re-enable auto-sleep
78
+ ```
79
+
74
80
  ### Run without workspace (ephemeral, fastest)
75
81
  ```bash
76
82
  mags run -e '<script>'
package/website/script.js CHANGED
@@ -87,6 +87,30 @@ const tokenStore = {
87
87
  },
88
88
  };
89
89
 
90
+ /* ── Extract tokens from URL after OAuth callback redirect ── */
91
+ (function () {
92
+ var params = new URLSearchParams(window.location.search);
93
+ var token = params.get('token');
94
+ var refresh = params.get('refresh');
95
+ if (token && refresh) {
96
+ tokenStore.setTokens({ accessToken: token, refreshToken: refresh });
97
+ params.delete('token');
98
+ params.delete('refresh');
99
+ var clean = window.location.pathname;
100
+ var remaining = params.toString();
101
+ if (remaining) clean += '?' + remaining;
102
+ window.history.replaceState({}, '', clean);
103
+ }
104
+ })();
105
+
106
+ /* ── Redirect to login for protected pages when not authed ── */
107
+ (function () {
108
+ if (!document.querySelector('[data-auth-required]')) return;
109
+ if (tokenStore.getAccessToken()) return;
110
+ var next = window.location.pathname + window.location.search;
111
+ window.location.replace('login.html?next=' + encodeURIComponent(next));
112
+ })();
113
+
90
114
  const refreshTokens = async () => {
91
115
  const refreshToken = tokenStore.getRefreshToken();
92
116
  if (!refreshToken) return null;
@@ -183,7 +207,10 @@ copyButtons.forEach((button) => {
183
207
  const googleLogin = document.querySelector('[data-google-login]');
184
208
  if (googleLogin) {
185
209
  googleLogin.addEventListener('click', () => {
186
- window.location.href = withBase('/auth/google', AUTH_BASE);
210
+ var url = withBase('/auth/google', AUTH_BASE);
211
+ var next = new URLSearchParams(window.location.search).get('next');
212
+ if (next) url += (url.includes('?') ? '&' : '?') + 'next=' + encodeURIComponent(next);
213
+ window.location.href = url;
187
214
  });
188
215
  }
189
216
 
@@ -223,11 +250,12 @@ if (loginForm) {
223
250
  refreshToken: data.refresh_token,
224
251
  });
225
252
  if (message) {
226
- message.textContent = 'Signed in. Redirecting to usage...';
253
+ message.textContent = 'Signed in. Redirecting...';
227
254
  }
228
255
  loginForm.reset();
256
+ var dest = new URLSearchParams(window.location.search).get('next') || 'usage.html';
229
257
  setTimeout(() => {
230
- window.location.href = 'usage.html';
258
+ window.location.href = dest;
231
259
  }, 600);
232
260
  } catch (error) {
233
261
  if (message) {
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <meta name="api-base" content="https://api.magpiecloud.com" />
12
12
  <meta name="auth-base" content="https://api.magpiecloud.com" />
13
- <link rel="stylesheet" href="styles.css?v=2" />
13
+ <link rel="stylesheet" href="styles.css?v=6" />
14
14
  <script src="env.js"></script>
15
15
  </head>
16
16
  <body>
@@ -24,16 +24,14 @@
24
24
  </div>
25
25
  <nav class="nav-links">
26
26
  <a href="index.html">Home</a>
27
- <a href="index.html#quickstart">Quickstart</a>
28
- <a href="index.html#cli">CLI</a>
29
- <a href="index.html#api">API</a>
30
- <a href="login.html">Login</a>
31
27
  <a href="usage.html">Usage</a>
32
- <a href="claude-skill.html">Claude Skill</a>
28
+ <a href="api.html">API</a>
33
29
  <a href="cookbook.html">Cookbook</a>
30
+ <a href="claude-skill.html">Claude Skill</a>
31
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
34
32
  </nav>
35
33
  <div class="nav-cta">
36
- <a class="button ghost" href="/api-keys">Manage tokens</a>
34
+ <a class="button ghost" href="usage.html">Usage</a>
37
35
  </div>
38
36
  </div>
39
37
  </header>
@@ -49,7 +47,7 @@
49
47
  the dashboard and store them securely.
50
48
  </p>
51
49
  <div class="hero-actions">
52
- <a class="button" href="/api-keys">Open token manager</a>
50
+ <a class="button" href="#tokens">Create a token</a>
53
51
  <a class="button ghost" href="#usage">Usage in CLI</a>
54
52
  </div>
55
53
  </div>
@@ -93,7 +91,7 @@ mags whoami</code></pre>
93
91
  <h2>Manage active tokens.</h2>
94
92
  </div>
95
93
  <div class="callout" data-auth-required>
96
- <p>Sign in to view and create tokens. Use <a data-auth-link="/auth/login" href="#">email login</a> or <a data-auth-link="/auth/google" href="#">Google sign-in</a>.</p>
94
+ <p>Sign in to view and create tokens. <a href="login.html?next=/tokens.html">Sign in with Google</a>.</p>
97
95
  </div>
98
96
  <div class="grid split" style="margin-bottom: 1.6rem;">
99
97
  <article class="panel" data-reveal>
@@ -158,14 +156,14 @@ mags whoami</code></pre>
158
156
  </div>
159
157
  <div class="footer-links">
160
158
  <a href="index.html">Home</a>
161
- <a href="login.html">Login</a>
162
159
  <a href="usage.html">Usage</a>
163
- <a href="claude-skill.html">Claude Skill</a>
160
+ <a href="api.html">API</a>
164
161
  <a href="cookbook.html">Cookbook</a>
162
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
165
163
  </div>
166
164
  </div>
167
165
  </footer>
168
166
 
169
- <script src="script.js"></script>
167
+ <script src="script.js?v=7"></script>
170
168
  </body>
171
169
  </html>
@@ -10,7 +10,7 @@
10
10
  />
11
11
  <meta name="api-base" content="https://api.magpiecloud.com" />
12
12
  <meta name="auth-base" content="https://api.magpiecloud.com" />
13
- <link rel="stylesheet" href="styles.css?v=2" />
13
+ <link rel="stylesheet" href="styles.css?v=6" />
14
14
  <script src="env.js"></script>
15
15
  </head>
16
16
  <body>
@@ -24,16 +24,14 @@
24
24
  </div>
25
25
  <nav class="nav-links">
26
26
  <a href="index.html">Home</a>
27
- <a href="index.html#quickstart">Quickstart</a>
28
- <a href="index.html#cli">CLI</a>
29
- <a href="index.html#api">API</a>
30
- <a href="login.html">Login</a>
31
27
  <a href="tokens.html">Tokens</a>
32
- <a href="claude-skill.html">Claude Skill</a>
28
+ <a href="api.html">API</a>
33
29
  <a href="cookbook.html">Cookbook</a>
30
+ <a href="claude-skill.html">Claude Skill</a>
31
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
34
32
  </nav>
35
33
  <div class="nav-cta">
36
- <a class="button ghost" href="/mags">Open console</a>
34
+ <a class="button ghost" href="tokens.html">Tokens</a>
37
35
  </div>
38
36
  </div>
39
37
  </header>
@@ -74,7 +72,7 @@ mags logs &lt;job-id&gt;</code></pre>
74
72
  <h2>Your most recent activity.</h2>
75
73
  </div>
76
74
  <div class="callout" data-auth-required>
77
- <p>Sign in to see usage data. Use <a data-auth-link="/auth/login" href="#">email login</a> or <a data-auth-link="/auth/google" href="#">Google sign-in</a>.</p>
75
+ <p>Sign in to see usage data. <a href="login.html?next=/usage.html">Sign in with Google</a>.</p>
78
76
  </div>
79
77
  <div class="stats-grid" data-reveal>
80
78
  <div class="stat-card">
@@ -174,14 +172,14 @@ mags cron add --name "backup" \
174
172
  </div>
175
173
  <div class="footer-links">
176
174
  <a href="index.html">Home</a>
177
- <a href="login.html">Login</a>
178
175
  <a href="tokens.html">Tokens</a>
179
- <a href="claude-skill.html">Claude Skill</a>
176
+ <a href="api.html">API</a>
180
177
  <a href="cookbook.html">Cookbook</a>
178
+ <a href="https://discord.gg/3avpC2nS" target="_blank" rel="noreferrer">Discord</a>
181
179
  </div>
182
180
  </div>
183
181
  </footer>
184
182
 
185
- <script src="script.js"></script>
183
+ <script src="script.js?v=7"></script>
186
184
  </body>
187
185
  </html>