@magpiecloud/mags 1.8.16 → 1.8.17

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 (44) hide show
  1. package/API.md +388 -0
  2. package/Mags-API.postman_collection.json +374 -0
  3. package/QUICKSTART.md +295 -0
  4. package/README.md +378 -95
  5. package/bin/mags.js +23 -2
  6. package/deploy-page.sh +171 -0
  7. package/index.js +0 -2
  8. package/mags +0 -0
  9. package/mags.sh +270 -0
  10. package/nodejs/README.md +197 -0
  11. package/nodejs/bin/mags.js +1882 -0
  12. package/nodejs/index.js +603 -0
  13. package/nodejs/package.json +45 -0
  14. package/package.json +3 -18
  15. package/python/INTEGRATION.md +800 -0
  16. package/python/README.md +161 -0
  17. package/python/dist/magpie_mags-1.3.8-py3-none-any.whl +0 -0
  18. package/python/dist/magpie_mags-1.3.8.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 +186 -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 +527 -0
  28. package/python/test_sdk.py +78 -0
  29. package/skill.md +153 -0
  30. package/website/api.html +1095 -0
  31. package/website/claude-skill.html +481 -0
  32. package/website/cookbook/hn-marketing.html +410 -0
  33. package/website/cookbook/hn-marketing.sh +42 -0
  34. package/website/cookbook.html +282 -0
  35. package/website/docs.html +677 -0
  36. package/website/env.js +4 -0
  37. package/website/index.html +801 -0
  38. package/website/llms.txt +334 -0
  39. package/website/login.html +108 -0
  40. package/website/mags.md +210 -0
  41. package/website/script.js +453 -0
  42. package/website/styles.css +1075 -0
  43. package/website/tokens.html +169 -0
  44. package/website/usage.html +185 -0
@@ -0,0 +1,603 @@
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 MagsError extends Error {
11
+ constructor(message, statusCode) {
12
+ super(message);
13
+ this.name = 'MagsError';
14
+ this.statusCode = statusCode || null;
15
+ }
16
+ }
17
+
18
+ class Mags {
19
+ /**
20
+ * Create a Mags client
21
+ * @param {object} options - Configuration options
22
+ * @param {string} options.apiUrl - API endpoint (default: https://api.magpiecloud.com)
23
+ * @param {string} options.apiToken - API token (required, or set MAGS_API_TOKEN env var)
24
+ * @param {number} options.timeout - Default request timeout in ms (default: 30000)
25
+ */
26
+ constructor(options = {}) {
27
+ this.apiUrl = (options.apiUrl || process.env.MAGS_API_URL || 'https://api.magpiecloud.com').replace(/\/+$/, '');
28
+ this.apiToken = options.apiToken || process.env.MAGS_API_TOKEN || process.env.MAGS_TOKEN;
29
+ this.timeout = options.timeout || 30000;
30
+
31
+ if (!this.apiToken) {
32
+ throw new MagsError('API token required. Set MAGS_API_TOKEN or pass apiToken option.');
33
+ }
34
+ }
35
+
36
+ _request(method, path, body = null, params = null) {
37
+ return new Promise((resolve, reject) => {
38
+ const url = new URL(path, this.apiUrl);
39
+ if (params) {
40
+ for (const [k, v] of Object.entries(params)) {
41
+ url.searchParams.set(k, v);
42
+ }
43
+ }
44
+ const isHttps = url.protocol === 'https:';
45
+ const lib = isHttps ? https : http;
46
+
47
+ const options = {
48
+ hostname: url.hostname,
49
+ port: url.port || (isHttps ? 443 : 80),
50
+ path: url.pathname + url.search,
51
+ method,
52
+ headers: {
53
+ 'Authorization': `Bearer ${this.apiToken}`,
54
+ 'Content-Type': 'application/json'
55
+ },
56
+ timeout: this.timeout
57
+ };
58
+
59
+ const req = lib.request(options, (res) => {
60
+ let data = '';
61
+ res.on('data', chunk => data += chunk);
62
+ res.on('end', () => {
63
+ try {
64
+ const parsed = JSON.parse(data);
65
+ if (res.statusCode >= 400) {
66
+ reject(new MagsError(parsed.error || parsed.message || `HTTP ${res.statusCode}`, res.statusCode));
67
+ } else {
68
+ resolve(parsed);
69
+ }
70
+ } catch {
71
+ if (res.statusCode >= 400) {
72
+ reject(new MagsError(data || `HTTP ${res.statusCode}`, res.statusCode));
73
+ } else {
74
+ resolve(data);
75
+ }
76
+ }
77
+ });
78
+ });
79
+
80
+ req.on('error', reject);
81
+ req.on('timeout', () => {
82
+ req.destroy();
83
+ reject(new MagsError('Request timed out'));
84
+ });
85
+ if (body) req.write(JSON.stringify(body));
86
+ req.end();
87
+ });
88
+ }
89
+
90
+ // ── Jobs ──────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Submit a job for execution
94
+ * @param {string} script - Script to execute
95
+ * @param {object} options - Job options
96
+ * @param {string} options.name - Job name
97
+ * @param {string} options.workspaceId - Persistent workspace ID
98
+ * @param {string} options.baseWorkspaceId - Read-only base workspace to mount
99
+ * @param {boolean} options.persistent - Keep VM alive after script
100
+ * @param {boolean} options.noSleep - Never auto-sleep (requires persistent)
101
+ * @param {boolean} options.ephemeral - No workspace/S3 sync (fastest)
102
+ * @param {string} options.startupCommand - Command to run when waking from sleep
103
+ * @param {object} options.environment - Environment variables
104
+ * @param {string[]} options.fileIds - File IDs from uploadFiles()
105
+ * @param {number} options.diskGb - Custom disk size in GB (default 2)
106
+ * @param {string} options.rootfsType - VM rootfs type: standard, claude, pi
107
+ * @returns {Promise<{request_id: string, status: string}>}
108
+ */
109
+ async run(script, options = {}) {
110
+ if (options.ephemeral && options.workspaceId) {
111
+ throw new MagsError('Cannot use ephemeral with workspaceId');
112
+ }
113
+ if (options.ephemeral && options.persistent) {
114
+ throw new MagsError('Cannot use ephemeral with persistent');
115
+ }
116
+ if (options.noSleep && !options.persistent) {
117
+ throw new MagsError('noSleep requires persistent=true');
118
+ }
119
+
120
+ const payload = {
121
+ script,
122
+ type: 'inline',
123
+ persistent: options.persistent || false,
124
+ };
125
+
126
+ if (options.noSleep) payload.no_sleep = true;
127
+ if (options.noSync) payload.no_sync = true;
128
+ if (options.name) payload.name = options.name;
129
+ if (!options.ephemeral && options.workspaceId) payload.workspace_id = options.workspaceId;
130
+ if (options.baseWorkspaceId) payload.base_workspace_id = options.baseWorkspaceId;
131
+ if (options.startupCommand) payload.startup_command = options.startupCommand;
132
+ if (options.environment) payload.environment = options.environment;
133
+ if (options.fileIds && options.fileIds.length > 0) payload.file_ids = options.fileIds;
134
+ if (options.diskGb) payload.disk_gb = options.diskGb;
135
+ if (options.rootfsType) payload.rootfs_type = options.rootfsType;
136
+
137
+ return this._request('POST', '/api/v1/mags-jobs', payload);
138
+ }
139
+
140
+ /**
141
+ * Run a job and wait for completion
142
+ * @param {string} script - Script to execute
143
+ * @param {object} options - Job options (same as run() plus timeout/pollInterval)
144
+ * @param {number} options.timeout - Timeout in ms (default: 60000)
145
+ * @param {number} options.pollInterval - Poll interval in ms (default: 1000)
146
+ * @returns {Promise<{requestId: string, status: string, exitCode: number, durationMs: number, logs: Array}>}
147
+ */
148
+ async runAndWait(script, options = {}) {
149
+ const timeout = options.timeout || 60000;
150
+ const pollInterval = options.pollInterval || 1000;
151
+ const result = await this.run(script, options);
152
+ const requestId = result.request_id;
153
+
154
+ const startTime = Date.now();
155
+ while (Date.now() - startTime < timeout) {
156
+ const status = await this.status(requestId);
157
+
158
+ if (status.status === 'completed' || status.status === 'error') {
159
+ const logsResp = await this.logs(requestId);
160
+ return {
161
+ requestId,
162
+ status: status.status,
163
+ exitCode: status.exit_code,
164
+ durationMs: status.script_duration_ms,
165
+ logs: logsResp.logs || []
166
+ };
167
+ }
168
+
169
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
170
+ }
171
+
172
+ throw new MagsError(`Job ${requestId} timed out after ${timeout}ms`);
173
+ }
174
+
175
+ /**
176
+ * Get job status
177
+ * @param {string} requestId - Job request ID
178
+ * @returns {Promise<object>}
179
+ */
180
+ async status(requestId) {
181
+ return this._request('GET', `/api/v1/mags-jobs/${requestId}/status`);
182
+ }
183
+
184
+ /**
185
+ * Get job logs
186
+ * @param {string} requestId - Job request ID
187
+ * @returns {Promise<{logs: Array}>}
188
+ */
189
+ async logs(requestId) {
190
+ return this._request('GET', `/api/v1/mags-jobs/${requestId}/logs`);
191
+ }
192
+
193
+ /**
194
+ * List recent jobs
195
+ * @param {object} options - Pagination options
196
+ * @param {number} options.page - Page number (default: 1)
197
+ * @param {number} options.pageSize - Page size (default: 20)
198
+ * @returns {Promise<{jobs: Array, total: number}>}
199
+ */
200
+ async list(options = {}) {
201
+ const page = options.page || 1;
202
+ const pageSize = options.pageSize || 20;
203
+ return this._request('GET', `/api/v1/mags-jobs`, null, { page, page_size: pageSize });
204
+ }
205
+
206
+ /**
207
+ * Update a job's settings
208
+ * @param {string} requestId - Job request ID
209
+ * @param {object} options - Settings to update
210
+ * @param {string} options.startupCommand - Command to run when VM wakes from sleep
211
+ * @param {boolean} options.noSleep - If true, VM never auto-sleeps. If false, re-enables auto-sleep.
212
+ * @returns {Promise<object>}
213
+ */
214
+ async updateJob(requestId, options = {}) {
215
+ const payload = {};
216
+ if (options.startupCommand !== undefined) payload.startup_command = options.startupCommand;
217
+ if (options.noSleep !== undefined) payload.no_sleep = options.noSleep;
218
+ return this._request('PATCH', `/api/v1/mags-jobs/${requestId}`, payload);
219
+ }
220
+
221
+ /**
222
+ * Enable URL or SSH access for a job
223
+ * @param {string} requestId - Job request ID
224
+ * @param {number} port - Port to expose (default: 8080, use 22 for SSH)
225
+ * @returns {Promise<object>}
226
+ */
227
+ async enableAccess(requestId, port = 8080) {
228
+ return this._request('POST', `/api/v1/mags-jobs/${requestId}/access`, { port });
229
+ }
230
+
231
+ /**
232
+ * Enable public URL access for a job's VM
233
+ * @param {string} nameOrId - Job name, workspace ID, or request ID
234
+ * @param {number} port - Port to expose (default: 8080)
235
+ * @returns {Promise<object>} Object with url and access details
236
+ */
237
+ async url(nameOrId, port = 8080) {
238
+ const requestId = await this._resolveJobId(nameOrId);
239
+ const st = await this.status(requestId);
240
+ const resp = await this.enableAccess(requestId, port);
241
+ const subdomain = st.subdomain || resp.subdomain;
242
+ if (subdomain) {
243
+ resp.url = `https://${subdomain}.apps.magpiecloud.com`;
244
+ }
245
+ return resp;
246
+ }
247
+
248
+ /**
249
+ * Stop a running job. Accepts a job ID, job name, or workspace ID.
250
+ * @param {string} nameOrId - Job name, workspace ID, or request ID
251
+ * @returns {Promise<object>}
252
+ */
253
+ async stop(nameOrId) {
254
+ const requestId = await this._resolveJobId(nameOrId);
255
+ return this._request('POST', `/api/v1/mags-jobs/${requestId}/stop`);
256
+ }
257
+
258
+ /**
259
+ * Sync a running job's workspace to S3 without stopping the VM
260
+ * @param {string} requestId - Job request ID
261
+ * @returns {Promise<object>}
262
+ */
263
+ async sync(requestId) {
264
+ return this._request('POST', `/api/v1/mags-jobs/${requestId}/sync`);
265
+ }
266
+
267
+ /**
268
+ * Create a new VM sandbox and wait until it's running.
269
+ * By default, data lives on local disk only (no S3 sync).
270
+ * Pass persistent: true to enable S3 data persistence.
271
+ * @param {string} name - Workspace name
272
+ * @param {object} options - Options
273
+ * @param {boolean} options.persistent - Enable S3 data persistence (default: false)
274
+ * @param {string} options.baseWorkspaceId - Read-only base workspace to mount
275
+ * @param {number} options.diskGb - Custom disk size in GB
276
+ * @param {number} options.timeout - Timeout in ms (default: 30000)
277
+ * @param {number} options.pollInterval - Poll interval in ms (default: 1000)
278
+ * @returns {Promise<{request_id: string, status: string}>}
279
+ */
280
+ async new(name, options = {}) {
281
+ const timeout = options.timeout || 30000;
282
+ const pollInterval = options.pollInterval || 1000;
283
+
284
+ const result = await this.run('sleep infinity', {
285
+ workspaceId: name,
286
+ persistent: true,
287
+ noSync: !options.persistent,
288
+ baseWorkspaceId: options.baseWorkspaceId,
289
+ diskGb: options.diskGb,
290
+ });
291
+ const requestId = result.request_id;
292
+
293
+ const startTime = Date.now();
294
+ while (Date.now() - startTime < timeout) {
295
+ const st = await this.status(requestId);
296
+ if (st.status === 'running' && st.vm_id) {
297
+ return { request_id: requestId, status: 'running' };
298
+ }
299
+ if (st.status === 'completed' || st.status === 'error') {
300
+ throw new MagsError(`Job ${requestId} ended unexpectedly: ${st.status}`);
301
+ }
302
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
303
+ }
304
+
305
+ throw new MagsError(`Job ${requestId} did not start within ${timeout}ms`);
306
+ }
307
+
308
+ /**
309
+ * Find a running or sleeping job by name, workspace ID, or job ID
310
+ * @param {string} nameOrId - Job name, workspace ID, or request ID
311
+ * @returns {Promise<object|null>} The job object, or null if not found
312
+ */
313
+ async findJob(nameOrId) {
314
+ const resp = await this.list({ pageSize: 50 });
315
+ const jobs = resp.jobs || [];
316
+
317
+ // Priority 1: exact name match, running/sleeping
318
+ for (const j of jobs) {
319
+ if (j.name === nameOrId && (j.status === 'running' || j.status === 'sleeping')) return j;
320
+ }
321
+ // Priority 2: workspace_id match, running/sleeping
322
+ for (const j of jobs) {
323
+ if (j.workspace_id === nameOrId && (j.status === 'running' || j.status === 'sleeping')) return j;
324
+ }
325
+ // Priority 3: exact name match, any status
326
+ for (const j of jobs) {
327
+ if (j.name === nameOrId) return j;
328
+ }
329
+ // Priority 4: workspace_id match, any status
330
+ for (const j of jobs) {
331
+ if (j.workspace_id === nameOrId) return j;
332
+ }
333
+
334
+ return null;
335
+ }
336
+
337
+ /**
338
+ * Execute a command on an existing running/sleeping VM via HTTP exec endpoint
339
+ * @param {string} nameOrId - Job name, workspace ID, or request ID
340
+ * @param {string} command - Command to execute
341
+ * @param {object} options - Options
342
+ * @param {number} options.timeout - Timeout in ms (default: 30000)
343
+ * @returns {Promise<{exitCode: number, output: string, stderr: string}>}
344
+ */
345
+ async exec(nameOrId, command, options = {}) {
346
+ const timeout = options.timeout || 30000;
347
+
348
+ const job = await this.findJob(nameOrId);
349
+ if (!job) throw new MagsError(`No running or sleeping VM found for '${nameOrId}'`);
350
+ if (job.status !== 'running' && job.status !== 'sleeping') {
351
+ throw new MagsError(`VM for '${nameOrId}' is ${job.status}, needs to be running or sleeping`);
352
+ }
353
+
354
+ const requestId = job.request_id || job.id;
355
+ const resp = await this._request('POST', `/api/v1/mags-jobs/${requestId}/exec`, {
356
+ command,
357
+ timeout: Math.ceil(timeout / 1000),
358
+ });
359
+
360
+ return { exitCode: resp.exit_code, output: resp.stdout, stderr: resp.stderr };
361
+ }
362
+
363
+ /**
364
+ * Resize a workspace's disk. Stops the existing VM, then creates a new one.
365
+ * @param {string} workspace - Workspace name
366
+ * @param {number} diskGb - New disk size in GB
367
+ * @param {object} options - Options
368
+ * @param {number} options.timeout - Timeout in ms (default: 30000)
369
+ * @param {number} options.pollInterval - Poll interval in ms (default: 1000)
370
+ * @returns {Promise<{request_id: string, status: string}>}
371
+ */
372
+ async resize(workspace, diskGb, options = {}) {
373
+ const existing = await this.findJob(workspace);
374
+ if (existing && existing.status === 'running') {
375
+ await this._request('POST', `/api/v1/mags-jobs/${existing.request_id}/sync`);
376
+ await this._request('POST', `/api/v1/mags-jobs/${existing.request_id}/stop`);
377
+ await new Promise(resolve => setTimeout(resolve, 1000));
378
+ } else if (existing && existing.status === 'sleeping') {
379
+ await this._request('POST', `/api/v1/mags-jobs/${existing.request_id}/stop`);
380
+ await new Promise(resolve => setTimeout(resolve, 1000));
381
+ }
382
+
383
+ return this.new(workspace, { persistent: true, diskGb, timeout: options.timeout, pollInterval: options.pollInterval });
384
+ }
385
+
386
+ /**
387
+ * Get aggregated usage summary
388
+ * @param {object} options - Options
389
+ * @param {number} options.windowDays - Time window in days (default: 30)
390
+ * @returns {Promise<object>}
391
+ */
392
+ async usage(options = {}) {
393
+ return this._request('GET', '/api/v1/mags-jobs/usage', null, { window_days: options.windowDays || 30 });
394
+ }
395
+
396
+ // ── Workspaces ────────────────────────────────────────────────────
397
+
398
+ /**
399
+ * List all workspaces
400
+ * @returns {Promise<{workspaces: Array, total: number}>}
401
+ */
402
+ async listWorkspaces() {
403
+ return this._request('GET', '/api/v1/mags-workspaces');
404
+ }
405
+
406
+ /**
407
+ * Delete a workspace and all its stored data
408
+ * @param {string} workspaceId - Workspace ID to delete
409
+ * @returns {Promise<object>}
410
+ */
411
+ async deleteWorkspace(workspaceId) {
412
+ return this._request('DELETE', `/api/v1/mags-workspaces/${workspaceId}`);
413
+ }
414
+
415
+ // ── File uploads ──────────────────────────────────────────────────
416
+
417
+ /**
418
+ * Upload files for use in a job
419
+ * @param {string[]} filePaths - Array of local file paths
420
+ * @returns {Promise<string[]>} Array of file IDs
421
+ */
422
+ async uploadFiles(filePaths) {
423
+ const fs = require('fs');
424
+ const path = require('path');
425
+ const fileIds = [];
426
+
427
+ for (const filePath of filePaths) {
428
+ const fileName = path.basename(filePath);
429
+ const fileData = fs.readFileSync(filePath);
430
+ const boundary = '----MagsBoundary' + Date.now().toString(16);
431
+
432
+ const parts = [];
433
+ parts.push(`--${boundary}\r\n`);
434
+ parts.push(`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`);
435
+ parts.push(`Content-Type: application/octet-stream\r\n\r\n`);
436
+ const header = Buffer.from(parts.join(''));
437
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
438
+ const body = Buffer.concat([header, fileData, footer]);
439
+
440
+ const response = await this._multipartRequest('/api/v1/mags-files', body, boundary);
441
+ if (response.file_id) {
442
+ fileIds.push(response.file_id);
443
+ } else {
444
+ throw new MagsError(`Failed to upload file: ${fileName}`);
445
+ }
446
+ }
447
+
448
+ return fileIds;
449
+ }
450
+
451
+ _multipartRequest(apiPath, body, boundary) {
452
+ return new Promise((resolve, reject) => {
453
+ const url = new URL(apiPath, this.apiUrl);
454
+ const isHttps = url.protocol === 'https:';
455
+ const lib = isHttps ? https : http;
456
+
457
+ const options = {
458
+ hostname: url.hostname,
459
+ port: url.port || (isHttps ? 443 : 80),
460
+ path: url.pathname,
461
+ method: 'POST',
462
+ headers: {
463
+ 'Authorization': `Bearer ${this.apiToken}`,
464
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
465
+ 'Content-Length': body.length
466
+ }
467
+ };
468
+
469
+ const req = lib.request(options, (res) => {
470
+ let data = '';
471
+ res.on('data', chunk => data += chunk);
472
+ res.on('end', () => {
473
+ try {
474
+ resolve(JSON.parse(data));
475
+ } catch {
476
+ resolve(data);
477
+ }
478
+ });
479
+ });
480
+
481
+ req.on('error', reject);
482
+ req.write(body);
483
+ req.end();
484
+ });
485
+ }
486
+
487
+ // ── Cron jobs ─────────────────────────────────────────────────────
488
+
489
+ /**
490
+ * Create a cron job
491
+ * @param {object} options - Cron job options
492
+ * @param {string} options.name - Cron job name
493
+ * @param {string} options.cronExpression - Cron expression (e.g., "0 * * * *")
494
+ * @param {string} options.script - Script to execute
495
+ * @param {string} options.workspaceId - Workspace ID
496
+ * @param {object} options.environment - Environment variables
497
+ * @param {boolean} options.persistent - Keep VM alive
498
+ * @returns {Promise<object>}
499
+ */
500
+ async cronCreate(options) {
501
+ const payload = {
502
+ name: options.name,
503
+ cron_expression: options.cronExpression,
504
+ script: options.script,
505
+ persistent: options.persistent || false
506
+ };
507
+ if (options.workspaceId) payload.workspace_id = options.workspaceId;
508
+ if (options.environment) payload.environment = options.environment;
509
+ return this._request('POST', '/api/v1/mags-cron', payload);
510
+ }
511
+
512
+ /**
513
+ * List cron jobs
514
+ * @returns {Promise<{cron_jobs: Array}>}
515
+ */
516
+ async cronList() {
517
+ return this._request('GET', '/api/v1/mags-cron');
518
+ }
519
+
520
+ /**
521
+ * Get a cron job
522
+ * @param {string} id - Cron job ID
523
+ * @returns {Promise<object>}
524
+ */
525
+ async cronGet(id) {
526
+ return this._request('GET', `/api/v1/mags-cron/${id}`);
527
+ }
528
+
529
+ /**
530
+ * Update a cron job
531
+ * @param {string} id - Cron job ID
532
+ * @param {object} updates - Fields to update
533
+ * @returns {Promise<object>}
534
+ */
535
+ async cronUpdate(id, updates) {
536
+ return this._request('PATCH', `/api/v1/mags-cron/${id}`, updates);
537
+ }
538
+
539
+ /**
540
+ * Delete a cron job
541
+ * @param {string} id - Cron job ID
542
+ * @returns {Promise<object>}
543
+ */
544
+ async cronDelete(id) {
545
+ return this._request('DELETE', `/api/v1/mags-cron/${id}`);
546
+ }
547
+
548
+ // ── URL aliases ───────────────────────────────────────────────────
549
+
550
+ /**
551
+ * Create a stable URL alias for a workspace
552
+ * @param {string} subdomain - Subdomain for the alias
553
+ * @param {string} workspaceId - Workspace to point to
554
+ * @param {string} domain - Domain (default: apps.magpiecloud.com)
555
+ * @returns {Promise<{id: string, subdomain: string, url: string}>}
556
+ */
557
+ async urlAliasCreate(subdomain, workspaceId, domain = 'apps.magpiecloud.com') {
558
+ return this._request('POST', '/api/v1/mags-url-aliases', {
559
+ subdomain,
560
+ workspace_id: workspaceId,
561
+ domain,
562
+ });
563
+ }
564
+
565
+ /**
566
+ * List all URL aliases
567
+ * @returns {Promise<{aliases: Array, total: number}>}
568
+ */
569
+ async urlAliasList() {
570
+ return this._request('GET', '/api/v1/mags-url-aliases');
571
+ }
572
+
573
+ /**
574
+ * Delete a URL alias by subdomain
575
+ * @param {string} subdomain - Subdomain to delete
576
+ * @returns {Promise<object>}
577
+ */
578
+ async urlAliasDelete(subdomain) {
579
+ return this._request('DELETE', `/api/v1/mags-url-aliases/${subdomain}`);
580
+ }
581
+
582
+ // ── Internal helpers ──────────────────────────────────────────────
583
+
584
+ /**
585
+ * Resolve a job name, workspace ID, or UUID to a request_id
586
+ * @param {string} nameOrId - Name, workspace ID, or request ID
587
+ * @returns {Promise<string>} The resolved request_id
588
+ */
589
+ async _resolveJobId(nameOrId) {
590
+ // If it looks like a UUID, use directly
591
+ if (nameOrId.length >= 32 && nameOrId.includes('-')) {
592
+ return nameOrId;
593
+ }
594
+ const job = await this.findJob(nameOrId);
595
+ if (!job) throw new MagsError(`No job found for '${nameOrId}'`);
596
+ return job.request_id || job.id;
597
+ }
598
+ }
599
+
600
+ module.exports = Mags;
601
+ module.exports.Mags = Mags;
602
+ module.exports.MagsError = MagsError;
603
+ module.exports.default = Mags;
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@magpiecloud/mags",
3
+ "version": "1.8.16",
4
+ "description": "Mags CLI & SDK - Execute commands and scripts on Mags Sandboxes",
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
+ "dependencies": {
38
+ "ws": "^8.18.0"
39
+ },
40
+ "files": [
41
+ "index.js",
42
+ "bin/mags.js",
43
+ "README.md"
44
+ ]
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.8.16",
3
+ "version": "1.8.17",
4
4
  "description": "Mags CLI & SDK - Execute commands and scripts on Mags Sandboxes",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,30 +16,15 @@
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",
26
- "repository": {
27
- "type": "git",
28
- "url": "https://github.com/magpiecloud/mags"
29
- },
30
23
  "homepage": "https://mags.run",
31
- "bugs": {
32
- "url": "https://github.com/magpiecloud/mags/issues"
33
- },
34
24
  "engines": {
35
25
  "node": ">=14.0.0"
36
26
  },
37
27
  "dependencies": {
38
28
  "ws": "^8.18.0"
39
- },
40
- "files": [
41
- "index.js",
42
- "bin/mags.js",
43
- "README.md"
44
- ]
29
+ }
45
30
  }