@magpiecloud/mags 1.5.1 → 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,326 @@
1
+ /**
2
+ * Mags SDK - Execute scripts on Magpie's instant VM infrastructure
3
+ * @module @magpiecloud/mags
4
+ */
5
+
6
+ const https = require('https');
7
+ const http = require('http');
8
+ const { URL } = require('url');
9
+
10
+ class Mags {
11
+ /**
12
+ * Create a Mags client
13
+ * @param {object} options - Configuration options
14
+ * @param {string} options.apiUrl - API endpoint (default: https://api.magpiecloud.com)
15
+ * @param {string} options.apiToken - API token (required, or set MAGS_API_TOKEN env var)
16
+ */
17
+ constructor(options = {}) {
18
+ this.apiUrl = options.apiUrl || process.env.MAGS_API_URL || 'https://api.magpiecloud.com';
19
+ this.apiToken = options.apiToken || process.env.MAGS_API_TOKEN;
20
+
21
+ if (!this.apiToken) {
22
+ throw new Error('API token required. Set MAGS_API_TOKEN or pass apiToken option.');
23
+ }
24
+ }
25
+
26
+ _request(method, path, body = null) {
27
+ return new Promise((resolve, reject) => {
28
+ const url = new URL(path, this.apiUrl);
29
+ const isHttps = url.protocol === 'https:';
30
+ const lib = isHttps ? https : http;
31
+
32
+ const options = {
33
+ hostname: url.hostname,
34
+ port: url.port || (isHttps ? 443 : 80),
35
+ path: url.pathname + url.search,
36
+ method,
37
+ headers: {
38
+ 'Authorization': `Bearer ${this.apiToken}`,
39
+ 'Content-Type': 'application/json'
40
+ }
41
+ };
42
+
43
+ const req = lib.request(options, (res) => {
44
+ let data = '';
45
+ res.on('data', chunk => data += chunk);
46
+ res.on('end', () => {
47
+ try {
48
+ const parsed = JSON.parse(data);
49
+ if (res.statusCode >= 400) {
50
+ reject(new Error(parsed.error || parsed.message || `HTTP ${res.statusCode}`));
51
+ } else {
52
+ resolve(parsed);
53
+ }
54
+ } catch {
55
+ resolve(data);
56
+ }
57
+ });
58
+ });
59
+
60
+ req.on('error', reject);
61
+ if (body) req.write(JSON.stringify(body));
62
+ req.end();
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Submit a job for execution
68
+ * @param {string} script - Script to execute
69
+ * @param {object} options - Job options
70
+ * @param {string} options.name - Job name
71
+ * @param {string} options.workspaceId - Persistent workspace ID
72
+ * @param {boolean} options.persistent - Keep VM alive after script
73
+ * @param {boolean} options.ephemeral - No workspace/S3 sync (fastest)
74
+ * @param {string} options.startupCommand - Command to run when waking from sleep
75
+ * @param {object} options.environment - Environment variables
76
+ * @param {string[]} options.fileIds - File IDs from uploadFiles()
77
+ * @returns {Promise<{requestId: string, status: string}>}
78
+ */
79
+ async run(script, options = {}) {
80
+ if (options.ephemeral && options.workspaceId) {
81
+ throw new Error('Cannot use ephemeral with workspaceId');
82
+ }
83
+ if (options.ephemeral && options.persistent) {
84
+ throw new Error('Cannot use ephemeral with persistent');
85
+ }
86
+
87
+ const payload = {
88
+ script,
89
+ type: 'inline',
90
+ name: options.name,
91
+ persistent: options.persistent || false,
92
+ startup_command: options.startupCommand,
93
+ environment: options.environment
94
+ };
95
+
96
+ // Only set workspace_id if not ephemeral
97
+ if (!options.ephemeral) {
98
+ payload.workspace_id = options.workspaceId;
99
+ }
100
+
101
+ if (options.fileIds && options.fileIds.length > 0) {
102
+ payload.file_ids = options.fileIds;
103
+ }
104
+
105
+ const response = await this._request('POST', '/api/v1/mags-jobs', payload);
106
+ return {
107
+ requestId: response.request_id,
108
+ status: response.status
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Get job status
114
+ * @param {string} requestId - Job request ID
115
+ * @returns {Promise<object>}
116
+ */
117
+ async status(requestId) {
118
+ return this._request('GET', `/api/v1/mags-jobs/${requestId}/status`);
119
+ }
120
+
121
+ /**
122
+ * Get job logs
123
+ * @param {string} requestId - Job request ID
124
+ * @returns {Promise<{logs: Array}>}
125
+ */
126
+ async logs(requestId) {
127
+ return this._request('GET', `/api/v1/mags-jobs/${requestId}/logs`);
128
+ }
129
+
130
+ /**
131
+ * List recent jobs
132
+ * @param {object} options - Pagination options
133
+ * @param {number} options.page - Page number (default: 1)
134
+ * @param {number} options.pageSize - Page size (default: 20)
135
+ * @returns {Promise<{jobs: Array, total: number}>}
136
+ */
137
+ async list(options = {}) {
138
+ const page = options.page || 1;
139
+ const pageSize = options.pageSize || 20;
140
+ return this._request('GET', `/api/v1/mags-jobs?page=${page}&page_size=${pageSize}`);
141
+ }
142
+
143
+ /**
144
+ * Enable URL access for a job
145
+ * @param {string} requestId - Job request ID
146
+ * @param {number} port - Port to expose (default: 8080)
147
+ * @returns {Promise<object>}
148
+ */
149
+ async enableUrl(requestId, port = 8080) {
150
+ return this._request('POST', `/api/v1/mags-jobs/${requestId}/access`, { port });
151
+ }
152
+
153
+ /**
154
+ * Stop a running job
155
+ * @param {string} requestId - Job request ID
156
+ * @returns {Promise<object>}
157
+ */
158
+ async stop(requestId) {
159
+ return this._request('POST', `/api/v1/mags-jobs/${requestId}/stop`);
160
+ }
161
+
162
+ /**
163
+ * Run a job and wait for completion
164
+ * @param {string} script - Script to execute
165
+ * @param {object} options - Job options
166
+ * @param {number} options.timeout - Timeout in ms (default: 60000)
167
+ * @returns {Promise<{status: string, exitCode: number, logs: Array}>}
168
+ */
169
+ async runAndWait(script, options = {}) {
170
+ const timeout = options.timeout || 60000;
171
+ const { requestId } = await this.run(script, options);
172
+
173
+ const startTime = Date.now();
174
+ while (Date.now() - startTime < timeout) {
175
+ const status = await this.status(requestId);
176
+
177
+ if (status.status === 'completed' || status.status === 'error') {
178
+ const logsResp = await this.logs(requestId);
179
+ return {
180
+ requestId,
181
+ status: status.status,
182
+ exitCode: status.exit_code,
183
+ durationMs: status.script_duration_ms,
184
+ logs: logsResp.logs || []
185
+ };
186
+ }
187
+
188
+ await new Promise(resolve => setTimeout(resolve, 1000));
189
+ }
190
+
191
+ throw new Error(`Job ${requestId} timed out after ${timeout}ms`);
192
+ }
193
+
194
+ /**
195
+ * Upload files for use in a job
196
+ * @param {string[]} filePaths - Array of local file paths
197
+ * @returns {Promise<string[]>} Array of file IDs
198
+ */
199
+ async uploadFiles(filePaths) {
200
+ const fs = require('fs');
201
+ const path = require('path');
202
+ const fileIds = [];
203
+
204
+ for (const filePath of filePaths) {
205
+ const fileName = path.basename(filePath);
206
+ const fileData = fs.readFileSync(filePath);
207
+ const boundary = '----MagsBoundary' + Date.now().toString(16);
208
+
209
+ const parts = [];
210
+ parts.push(`--${boundary}\r\n`);
211
+ parts.push(`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`);
212
+ parts.push(`Content-Type: application/octet-stream\r\n\r\n`);
213
+ const header = Buffer.from(parts.join(''));
214
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
215
+ const body = Buffer.concat([header, fileData, footer]);
216
+
217
+ const response = await this._multipartRequest('/api/v1/mags-files', body, boundary);
218
+ if (response.file_id) {
219
+ fileIds.push(response.file_id);
220
+ } else {
221
+ throw new Error(`Failed to upload file: ${fileName}`);
222
+ }
223
+ }
224
+
225
+ return fileIds;
226
+ }
227
+
228
+ _multipartRequest(apiPath, body, boundary) {
229
+ return new Promise((resolve, reject) => {
230
+ const url = new URL(apiPath, this.apiUrl);
231
+ const isHttps = url.protocol === 'https:';
232
+ const lib = isHttps ? https : http;
233
+
234
+ const options = {
235
+ hostname: url.hostname,
236
+ port: url.port || (isHttps ? 443 : 80),
237
+ path: url.pathname,
238
+ method: 'POST',
239
+ headers: {
240
+ 'Authorization': `Bearer ${this.apiToken}`,
241
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
242
+ 'Content-Length': body.length
243
+ }
244
+ };
245
+
246
+ const req = lib.request(options, (res) => {
247
+ let data = '';
248
+ res.on('data', chunk => data += chunk);
249
+ res.on('end', () => {
250
+ try {
251
+ resolve(JSON.parse(data));
252
+ } catch {
253
+ resolve(data);
254
+ }
255
+ });
256
+ });
257
+
258
+ req.on('error', reject);
259
+ req.write(body);
260
+ req.end();
261
+ });
262
+ }
263
+
264
+ // Cron job methods
265
+
266
+ /**
267
+ * Create a cron job
268
+ * @param {object} options - Cron job options
269
+ * @param {string} options.name - Cron job name
270
+ * @param {string} options.cronExpression - Cron expression (e.g., "0 * * * *")
271
+ * @param {string} options.script - Script to execute
272
+ * @param {string} options.workspaceId - Workspace ID
273
+ * @param {boolean} options.persistent - Keep VM alive
274
+ * @returns {Promise<object>}
275
+ */
276
+ async cronCreate(options) {
277
+ const payload = {
278
+ name: options.name,
279
+ cron_expression: options.cronExpression,
280
+ script: options.script,
281
+ workspace_id: options.workspaceId,
282
+ persistent: options.persistent || false
283
+ };
284
+ return this._request('POST', '/api/v1/mags-cron', payload);
285
+ }
286
+
287
+ /**
288
+ * List cron jobs
289
+ * @returns {Promise<{cron_jobs: Array}>}
290
+ */
291
+ async cronList() {
292
+ return this._request('GET', '/api/v1/mags-cron');
293
+ }
294
+
295
+ /**
296
+ * Get a cron job
297
+ * @param {string} id - Cron job ID
298
+ * @returns {Promise<object>}
299
+ */
300
+ async cronGet(id) {
301
+ return this._request('GET', `/api/v1/mags-cron/${id}`);
302
+ }
303
+
304
+ /**
305
+ * Update a cron job
306
+ * @param {string} id - Cron job ID
307
+ * @param {object} updates - Fields to update
308
+ * @returns {Promise<object>}
309
+ */
310
+ async cronUpdate(id, updates) {
311
+ return this._request('PATCH', `/api/v1/mags-cron/${id}`, updates);
312
+ }
313
+
314
+ /**
315
+ * Delete a cron job
316
+ * @param {string} id - Cron job ID
317
+ * @returns {Promise<object>}
318
+ */
319
+ async cronDelete(id) {
320
+ return this._request('DELETE', `/api/v1/mags-cron/${id}`);
321
+ }
322
+ }
323
+
324
+ module.exports = Mags;
325
+ module.exports.Mags = Mags;
326
+ module.exports.default = Mags;
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@magpiecloud/mags",
3
+ "version": "1.5.1",
4
+ "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "mags": "./bin/mags.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/mags.js --help"
11
+ },
12
+ "keywords": [
13
+ "magpie",
14
+ "mags",
15
+ "vm",
16
+ "microvm",
17
+ "cloud",
18
+ "serverless",
19
+ "execution",
20
+ "cli",
21
+ "claude",
22
+ "claude-code"
23
+ ],
24
+ "author": "Magpie Cloud",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/magpiecloud/mags"
29
+ },
30
+ "homepage": "https://mags.run",
31
+ "bugs": {
32
+ "url": "https://github.com/magpiecloud/mags/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=14.0.0"
36
+ },
37
+ "files": [
38
+ "index.js",
39
+ "bin/mags.js",
40
+ "README.md"
41
+ ]
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,10 +16,7 @@
16
16
  "microvm",
17
17
  "cloud",
18
18
  "serverless",
19
- "execution",
20
- "cli",
21
- "claude",
22
- "claude-code"
19
+ "execution"
23
20
  ],
24
21
  "author": "Magpie Cloud",
25
22
  "license": "MIT",
@@ -27,16 +24,8 @@
27
24
  "type": "git",
28
25
  "url": "https://github.com/magpiecloud/mags"
29
26
  },
30
- "homepage": "https://mags.run",
31
- "bugs": {
32
- "url": "https://github.com/magpiecloud/mags/issues"
33
- },
27
+ "homepage": "https://magpiecloud.com/docs/mags",
34
28
  "engines": {
35
29
  "node": ">=14.0.0"
36
- },
37
- "files": [
38
- "index.js",
39
- "bin/mags.js",
40
- "README.md"
41
- ]
30
+ }
42
31
  }