@magpiecloud/mags 1.8.13 → 1.8.15

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 (43) hide show
  1. package/README.md +95 -378
  2. package/bin/mags.js +196 -104
  3. package/index.js +6 -52
  4. package/package.json +22 -4
  5. package/API.md +0 -388
  6. package/Mags-API.postman_collection.json +0 -374
  7. package/QUICKSTART.md +0 -295
  8. package/deploy-page.sh +0 -171
  9. package/mags +0 -0
  10. package/mags.sh +0 -270
  11. package/nodejs/README.md +0 -197
  12. package/nodejs/bin/mags.js +0 -1146
  13. package/nodejs/index.js +0 -642
  14. package/nodejs/package.json +0 -42
  15. package/python/INTEGRATION.md +0 -800
  16. package/python/README.md +0 -161
  17. package/python/dist/magpie_mags-1.3.5-py3-none-any.whl +0 -0
  18. package/python/dist/magpie_mags-1.3.5.tar.gz +0 -0
  19. package/python/examples/demo.py +0 -181
  20. package/python/pyproject.toml +0 -39
  21. package/python/src/magpie_mags.egg-info/PKG-INFO +0 -182
  22. package/python/src/magpie_mags.egg-info/SOURCES.txt +0 -9
  23. package/python/src/magpie_mags.egg-info/dependency_links.txt +0 -1
  24. package/python/src/magpie_mags.egg-info/requires.txt +0 -1
  25. package/python/src/magpie_mags.egg-info/top_level.txt +0 -1
  26. package/python/src/mags/__init__.py +0 -6
  27. package/python/src/mags/client.py +0 -573
  28. package/python/test_sdk.py +0 -78
  29. package/skill.md +0 -153
  30. package/website/api.html +0 -1095
  31. package/website/claude-skill.html +0 -481
  32. package/website/cookbook/hn-marketing.html +0 -410
  33. package/website/cookbook/hn-marketing.sh +0 -42
  34. package/website/cookbook.html +0 -282
  35. package/website/env.js +0 -4
  36. package/website/index.html +0 -801
  37. package/website/llms.txt +0 -334
  38. package/website/login.html +0 -108
  39. package/website/mags.md +0 -210
  40. package/website/script.js +0 -453
  41. package/website/styles.css +0 -908
  42. package/website/tokens.html +0 -169
  43. package/website/usage.html +0 -185
package/bin/mags.js CHANGED
@@ -1466,123 +1466,225 @@ async function browserSession(args) {
1466
1466
  log('gray', `Stop with: mags stop ${workspace || requestId}`);
1467
1467
  }
1468
1468
 
1469
- async function sshToJob(nameOrId) {
1470
- if (!nameOrId) {
1471
- log('red', 'Error: Workspace, job name, or job ID required');
1472
- console.log(`\nUsage: mags ssh <workspace|name|id>\n`);
1473
- console.log('Examples:');
1474
- console.log(' mags ssh myproject # Auto-starts VM if needed');
1475
- console.log(' mags ssh 7bd12031-25ff...');
1476
- console.log('');
1477
- console.log('Get job names/IDs with: mags list');
1478
- process.exit(1);
1469
+ // WebSocket tunnel proxy — pipes stdin/stdout through a WebSocket to the agent SSH proxy.
1470
+ // Used as SSH ProxyCommand: ssh -o ProxyCommand="mags proxy <jobID>" root@mags-vm
1471
+ async function proxyTunnel(jobID) {
1472
+ const url = new URL(`/api/v1/mags-jobs/${jobID}/tunnel`, API_URL);
1473
+ const wsUrl = url.toString().replace(/^http/, 'ws');
1474
+
1475
+ // Dynamic import for WebSocket
1476
+ let WebSocket;
1477
+ try {
1478
+ WebSocket = require('ws');
1479
+ } catch {
1480
+ if (typeof globalThis.WebSocket !== 'undefined') {
1481
+ WebSocket = globalThis.WebSocket;
1482
+ } else {
1483
+ process.stderr.write('Error: ws package required. Run: npm install -g ws\n');
1484
+ process.exit(1);
1485
+ }
1479
1486
  }
1480
1487
 
1481
- // First, try to find a running/sleeping job for this workspace
1482
- const existingJob = await findWorkspaceJob(nameOrId);
1488
+ const ws = new WebSocket(wsUrl, {
1489
+ headers: {
1490
+ 'Authorization': `Bearer ${API_TOKEN}`,
1491
+ },
1492
+ perMessageDeflate: false,
1493
+ rejectUnauthorized: false,
1494
+ });
1483
1495
 
1484
- let jobID;
1496
+ // Buffer stdin data until WebSocket is open AND key is received
1497
+ const pendingData = [];
1498
+ let bridgeReady = false;
1499
+ let firstMessage = true;
1500
+
1501
+ process.stdin.on('data', (data) => {
1502
+ if (bridgeReady && ws.readyState === WebSocket.OPEN) {
1503
+ ws.send(data);
1504
+ } else {
1505
+ pendingData.push(Buffer.from(data));
1506
+ }
1507
+ });
1508
+
1509
+ ws.on('open', () => {
1510
+ // Don't flush yet — wait for first message (SSH key)
1511
+ });
1512
+
1513
+ ws.on('message', (data, isBinary) => {
1514
+ if (firstMessage && !isBinary) {
1515
+ // First text message is the SSH private key — save to temp file
1516
+ firstMessage = false;
1517
+ const keyData = Buffer.from(data).toString('utf8');
1518
+ if (keyData.startsWith('-----BEGIN')) {
1519
+ const keyPath = path.join(os.tmpdir(), `mags_tunnel_key_${jobID}`);
1520
+ fs.writeFileSync(keyPath, keyData, { mode: 0o600 });
1521
+ }
1522
+ // Now start the SSH data bridge
1523
+ bridgeReady = true;
1524
+ for (const buf of pendingData) {
1525
+ ws.send(buf);
1526
+ }
1527
+ pendingData.length = 0;
1528
+ return;
1529
+ }
1530
+ process.stdout.write(Buffer.from(data));
1531
+ });
1532
+
1533
+ ws.on('close', () => {
1534
+ process.exit(0);
1535
+ });
1536
+
1537
+ ws.on('error', (err) => {
1538
+ process.stderr.write(`Tunnel error: ${err.message}\n`);
1539
+ process.exit(1);
1540
+ });
1541
+
1542
+ process.stdin.on('end', () => {
1543
+ ws.close();
1544
+ });
1545
+ }
1546
+
1547
+ // Fetch SSH key from tunnel endpoint (quick WS handshake)
1548
+ async function fetchTunnelKey(jobID) {
1549
+ const WebSocket = require('ws');
1550
+ const url = new URL(`/api/v1/mags-jobs/${jobID}/tunnel`, API_URL);
1551
+ const wsUrl = url.toString().replace(/^http/, 'ws');
1552
+
1553
+ return new Promise((resolve, reject) => {
1554
+ const ws = new WebSocket(wsUrl, {
1555
+ headers: { 'Authorization': `Bearer ${API_TOKEN}` },
1556
+ perMessageDeflate: false,
1557
+ rejectUnauthorized: false,
1558
+ });
1559
+
1560
+ const timeout = setTimeout(() => {
1561
+ ws.close();
1562
+ reject(new Error('Tunnel key fetch timeout'));
1563
+ }, 60000);
1564
+
1565
+ ws.on('message', (data, isBinary) => {
1566
+ if (!isBinary) {
1567
+ const key = Buffer.from(data).toString('utf8');
1568
+ if (key.startsWith('-----BEGIN')) {
1569
+ clearTimeout(timeout);
1570
+ ws.close();
1571
+ resolve(key);
1572
+ return;
1573
+ }
1574
+ }
1575
+ // Not a key message — shouldn't happen as first message
1576
+ clearTimeout(timeout);
1577
+ ws.close();
1578
+ reject(new Error('No SSH key received from tunnel'));
1579
+ });
1580
+
1581
+ ws.on('error', (err) => {
1582
+ clearTimeout(timeout);
1583
+ reject(err);
1584
+ });
1585
+ });
1586
+ }
1587
+
1588
+ // Resolve a workspace name to a job ID, creating a new VM if needed
1589
+ async function resolveOrCreateJob(nameOrId) {
1590
+ const existingJob = await findWorkspaceJob(nameOrId);
1485
1591
 
1486
1592
  if (existingJob && existingJob.status === 'running') {
1487
1593
  log('green', `Found running VM for '${nameOrId}'`);
1488
- jobID = existingJob.request_id;
1594
+ return existingJob.request_id;
1489
1595
  } else if (existingJob && existingJob.status === 'sleeping') {
1490
1596
  log('yellow', `Waking sleeping VM for '${nameOrId}'...`);
1491
- jobID = existingJob.request_id;
1492
- } else {
1493
- // No running/sleeping VM — start a new persistent one
1494
- log('blue', `Starting VM with workspace '${nameOrId}'...`);
1495
-
1496
- const payload = {
1497
- script: 'echo "SSH session ready" && sleep 3600',
1498
- type: 'inline',
1499
- workspace_id: nameOrId,
1500
- persistent: true,
1501
- startup_command: 'sleep 3600'
1502
- };
1597
+ return existingJob.request_id;
1598
+ }
1503
1599
 
1504
- const response = await request('POST', '/api/v1/mags-jobs', payload);
1600
+ // No running/sleeping VM start a new persistent one
1601
+ log('blue', `Starting VM with workspace '${nameOrId}'...`);
1602
+ const payload = {
1603
+ script: 'echo "SSH session ready" && sleep 3600',
1604
+ type: 'inline',
1605
+ workspace_id: nameOrId,
1606
+ persistent: true,
1607
+ startup_command: 'sleep 3600'
1608
+ };
1505
1609
 
1506
- if (!response.request_id) {
1507
- log('red', 'Failed to start VM:');
1508
- console.log(JSON.stringify(response, null, 2));
1509
- process.exit(1);
1510
- }
1610
+ const response = await request('POST', '/api/v1/mags-jobs', payload);
1611
+ if (!response.request_id) {
1612
+ log('red', 'Failed to start VM:');
1613
+ console.log(JSON.stringify(response, null, 2));
1614
+ process.exit(1);
1615
+ }
1511
1616
 
1512
- jobID = response.request_id;
1513
- log('gray', `Job: ${jobID}`);
1617
+ const jobID = response.request_id;
1618
+ log('gray', `Job: ${jobID}`);
1514
1619
 
1515
- // Wait for VM to be ready (status=running AND vm_id assigned)
1516
- log('blue', 'Waiting for VM...');
1517
- for (let i = 0; i < 60; i++) {
1518
- const status = await request('GET', `/api/v1/mags-jobs/${jobID}/status`);
1519
- if (status.status === 'running' && status.vm_id) break;
1520
- if (status.status === 'error') {
1521
- log('red', 'VM failed to start');
1522
- process.exit(1);
1523
- }
1524
- process.stdout.write('.');
1525
- await sleep(300);
1620
+ log('blue', 'Waiting for VM...');
1621
+ for (let i = 0; i < 60; i++) {
1622
+ const status = await request('GET', `/api/v1/mags-jobs/${jobID}/status`);
1623
+ if (status.status === 'running' && status.vm_id) break;
1624
+ if (status.status === 'error') {
1625
+ log('red', 'VM failed to start');
1626
+ process.exit(1);
1526
1627
  }
1527
- console.log('');
1628
+ process.stdout.write('.');
1629
+ await sleep(300);
1528
1630
  }
1631
+ console.log('');
1632
+ return jobID;
1633
+ }
1529
1634
 
1530
- // Enable SSH access (port 22)
1531
- log('blue', 'Enabling SSH access...');
1635
+ // Get SSH key for a job (calls EnableAccess to set up agent proxy and get key)
1636
+ async function getSSHKey(jobID) {
1532
1637
  const accessResp = await request('POST', `/api/v1/mags-jobs/${jobID}/access`, { port: 22 });
1533
-
1534
1638
  if (!accessResp.success) {
1535
1639
  log('red', 'Failed to enable SSH access');
1536
- if (accessResp.error) {
1537
- log('red', accessResp.error);
1538
- }
1640
+ if (accessResp.error) log('red', accessResp.error);
1539
1641
  process.exit(1);
1540
1642
  }
1643
+ return accessResp.ssh_private_key;
1644
+ }
1541
1645
 
1542
- const sshHost = accessResp.ssh_host;
1543
- const sshPort = accessResp.ssh_port;
1544
- const sshKey = accessResp.ssh_private_key;
1545
-
1546
- if (!sshHost || !sshPort) {
1547
- log('red', 'SSH access enabled but no connection details returned');
1548
- console.log(JSON.stringify(accessResp, null, 2));
1646
+ async function sshToJob(nameOrId) {
1647
+ if (!nameOrId) {
1648
+ log('red', 'Error: Workspace, job name, or job ID required');
1649
+ console.log(`\nUsage: mags ssh <workspace|name|id>\n`);
1650
+ console.log('Examples:');
1651
+ console.log(' mags ssh myproject # Auto-starts VM if needed');
1652
+ console.log(' mags ssh 7bd12031-25ff...');
1653
+ console.log('');
1654
+ console.log('Get job names/IDs with: mags list');
1549
1655
  process.exit(1);
1550
1656
  }
1551
1657
 
1552
- log('green', `Connecting to ${sshHost}:${sshPort}...`);
1658
+ const jobID = await resolveOrCreateJob(nameOrId);
1659
+
1660
+ // Fetch SSH key from tunnel endpoint (also warms up the agent proxy)
1661
+ log('blue', 'Setting up tunnel...');
1662
+ const sshKey = await fetchTunnelKey(jobID);
1663
+ const magsPath = process.argv[1];
1664
+ const keyFile = path.join(os.tmpdir(), `mags_tunnel_key_${jobID}`);
1665
+ fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
1666
+
1667
+ log('green', `Connecting via secure tunnel...`);
1553
1668
  console.log(`${colors.gray}(Use Ctrl+D or 'exit' to disconnect)${colors.reset}\n`);
1554
1669
 
1555
- // Build SSH arguments
1556
1670
  const sshArgs = [
1557
- '-tt', // Force TTY allocation even when stdin isn't a terminal
1671
+ '-tt',
1558
1672
  '-o', 'StrictHostKeyChecking=no',
1559
1673
  '-o', 'UserKnownHostsFile=/dev/null',
1560
1674
  '-o', 'LogLevel=ERROR',
1561
- '-p', sshPort.toString()
1675
+ '-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
1676
+ '-i', keyFile,
1677
+ 'root@mags-vm',
1562
1678
  ];
1563
1679
 
1564
- // If API returned an SSH key, write it to a temp file and use it
1565
- let keyFile = null;
1566
- if (sshKey) {
1567
- keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
1568
- fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
1569
- sshArgs.push('-i', keyFile);
1570
- }
1571
-
1572
- sshArgs.push(`root@${sshHost}`);
1573
-
1574
1680
  const ssh = spawn('ssh', sshArgs, {
1575
- stdio: 'inherit' // Inherit stdin/stdout/stderr for interactive session
1681
+ stdio: 'inherit'
1576
1682
  });
1577
1683
 
1578
1684
  ssh.on('error', (err) => {
1579
- if (keyFile) {
1580
- try { fs.unlinkSync(keyFile); } catch (e) {}
1581
- }
1685
+ try { fs.unlinkSync(keyFile); } catch (e) {}
1582
1686
  if (err.code === 'ENOENT') {
1583
1687
  log('red', 'SSH client not found. Please install OpenSSH.');
1584
- log('gray', 'On macOS/Linux: ssh is usually pre-installed');
1585
- log('gray', 'On Windows: Install OpenSSH or use WSL');
1586
1688
  } else {
1587
1689
  log('red', `SSH error: ${err.message}`);
1588
1690
  }
@@ -1590,10 +1692,7 @@ async function sshToJob(nameOrId) {
1590
1692
  });
1591
1693
 
1592
1694
  ssh.on('close', (code) => {
1593
- // Clean up temp key file
1594
- if (keyFile) {
1595
- try { fs.unlinkSync(keyFile); } catch (e) {}
1596
- }
1695
+ try { fs.unlinkSync(keyFile); } catch (e) {}
1597
1696
  if (code === 0) {
1598
1697
  log('green', '\nSSH session ended');
1599
1698
  } else {
@@ -1629,31 +1728,21 @@ async function execOnJob(nameOrId, command) {
1629
1728
  process.exit(1);
1630
1729
  }
1631
1730
 
1632
- // Enable SSH access
1633
- log('blue', 'Enabling SSH access...');
1634
- const accessResp = await request('POST', `/api/v1/mags-jobs/${jobID}/access`, { port: 22 });
1731
+ // Fetch SSH key from tunnel endpoint (also warms up the agent proxy)
1732
+ log('blue', 'Setting up tunnel...');
1733
+ const sshKey = await fetchTunnelKey(jobID);
1734
+ const magsPath = process.argv[1];
1735
+ const keyFile = path.join(os.tmpdir(), `mags_tunnel_key_${jobID}`);
1736
+ fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
1635
1737
 
1636
- if (!accessResp.success || !accessResp.ssh_host || !accessResp.ssh_port) {
1637
- log('red', 'Failed to enable SSH access');
1638
- if (accessResp.error) log('red', accessResp.error);
1639
- process.exit(1);
1640
- }
1641
-
1642
- // Write SSH key to temp file
1643
- let keyFile = null;
1644
1738
  const sshArgs = [
1645
1739
  '-o', 'StrictHostKeyChecking=no',
1646
1740
  '-o', 'UserKnownHostsFile=/dev/null',
1647
1741
  '-o', 'LogLevel=ERROR',
1648
- '-p', accessResp.ssh_port.toString()
1742
+ '-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
1743
+ '-i', keyFile,
1649
1744
  ];
1650
1745
 
1651
- if (accessResp.ssh_private_key) {
1652
- keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
1653
- fs.writeFileSync(keyFile, accessResp.ssh_private_key, { mode: 0o600 });
1654
- sshArgs.push('-i', keyFile);
1655
- }
1656
-
1657
1746
  // Wrap command to use chroot if overlay is mounted, with proper env
1658
1747
  const escaped = command.replace(/'/g, "'\\''");
1659
1748
  const wrappedCmd = `if [ -d /overlay/bin ]; then chroot /overlay /bin/sh -l -c 'cd /root 2>/dev/null; ${escaped}'; else cd /root 2>/dev/null; ${escaped}; fi`;
@@ -1661,18 +1750,18 @@ async function execOnJob(nameOrId, command) {
1661
1750
  if (process.stdin.isTTY) {
1662
1751
  sshArgs.push('-t');
1663
1752
  }
1664
- sshArgs.push(`root@${accessResp.ssh_host}`, wrappedCmd);
1753
+ sshArgs.push('root@mags-vm', wrappedCmd);
1665
1754
 
1666
1755
  const ssh = spawn('ssh', sshArgs, { stdio: 'inherit' });
1667
1756
 
1668
1757
  ssh.on('error', (err) => {
1669
- if (keyFile) try { fs.unlinkSync(keyFile); } catch (e) {}
1758
+ try { fs.unlinkSync(keyFile); } catch (e) {}
1670
1759
  log('red', `SSH error: ${err.message}`);
1671
1760
  process.exit(1);
1672
1761
  });
1673
1762
 
1674
1763
  ssh.on('close', (code) => {
1675
- if (keyFile) try { fs.unlinkSync(keyFile); } catch (e) {}
1764
+ try { fs.unlinkSync(keyFile); } catch (e) {}
1676
1765
  process.exit(code || 0);
1677
1766
  });
1678
1767
  }
@@ -1768,6 +1857,9 @@ async function main() {
1768
1857
  case 'setup-claude':
1769
1858
  await setupClaude();
1770
1859
  break;
1860
+ case 'proxy':
1861
+ await proxyTunnel(args[1]);
1862
+ break;
1771
1863
  default:
1772
1864
  if (!command) {
1773
1865
  // No command - check if logged in
package/index.js CHANGED
@@ -333,7 +333,7 @@ class Mags {
333
333
  }
334
334
 
335
335
  /**
336
- * Execute a command on an existing running/sleeping VM via SSH
336
+ * Execute a command on an existing running/sleeping VM via HTTP exec endpoint
337
337
  * @param {string} nameOrId - Job name, workspace ID, or request ID
338
338
  * @param {string} command - Command to execute
339
339
  * @param {object} options - Options
@@ -350,58 +350,12 @@ class Mags {
350
350
  }
351
351
 
352
352
  const requestId = job.request_id || job.id;
353
- const access = await this.enableAccess(requestId, 22);
354
-
355
- if (!access.success || !access.ssh_host) {
356
- throw new MagsError(`Failed to enable SSH access: ${access.error || 'unknown error'}`);
357
- }
358
-
359
- const { execFile } = require('child_process');
360
- const fs = require('fs');
361
- const os = require('os');
362
- const path = require('path');
363
-
364
- const escaped = command.replace(/'/g, "'\\''");
365
- const wrapped =
366
- `if [ -d /overlay/bin ]; then ` +
367
- `chroot /overlay /bin/sh -l -c 'cd /root 2>/dev/null; ${escaped}'; ` +
368
- `else cd /root 2>/dev/null; ${escaped}; fi`;
369
-
370
- let keyFile = null;
371
- try {
372
- const sshArgs = [
373
- '-o', 'StrictHostKeyChecking=no',
374
- '-o', 'UserKnownHostsFile=/dev/null',
375
- '-o', 'LogLevel=ERROR',
376
- '-p', String(access.ssh_port),
377
- ];
378
-
379
- if (access.ssh_private_key) {
380
- keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
381
- fs.writeFileSync(keyFile, access.ssh_private_key, { mode: 0o600 });
382
- sshArgs.push('-i', keyFile);
383
- }
353
+ const resp = await this._request('POST', `/api/v1/mags-jobs/${requestId}/exec`, {
354
+ command,
355
+ timeout: Math.ceil(timeout / 1000),
356
+ });
384
357
 
385
- sshArgs.push(`root@${access.ssh_host}`, wrapped);
386
-
387
- return new Promise((resolve, reject) => {
388
- execFile('ssh', sshArgs, { timeout, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
389
- if (keyFile) try { fs.unlinkSync(keyFile); } catch {}
390
- if (err && err.killed) {
391
- reject(new MagsError(`Command timed out after ${timeout}ms`));
392
- } else {
393
- resolve({
394
- exitCode: err ? err.code || 1 : 0,
395
- output: stdout,
396
- stderr: stderr,
397
- });
398
- }
399
- });
400
- });
401
- } catch (e) {
402
- if (keyFile) try { require('fs').unlinkSync(keyFile); } catch {}
403
- throw e;
404
- }
358
+ return { exitCode: resp.exit_code, output: resp.stdout, stderr: resp.stderr };
405
359
  }
406
360
 
407
361
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.8.13",
4
- "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
3
+ "version": "1.8.15",
4
+ "description": "Mags CLI & SDK - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "mags": "./bin/mags.js"
@@ -16,12 +16,30 @@
16
16
  "microvm",
17
17
  "cloud",
18
18
  "serverless",
19
- "execution"
19
+ "execution",
20
+ "cli",
21
+ "claude",
22
+ "claude-code"
20
23
  ],
21
24
  "author": "Magpie Cloud",
22
25
  "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/magpiecloud/mags"
29
+ },
23
30
  "homepage": "https://mags.run",
31
+ "bugs": {
32
+ "url": "https://github.com/magpiecloud/mags/issues"
33
+ },
24
34
  "engines": {
25
35
  "node": ">=14.0.0"
26
- }
36
+ },
37
+ "dependencies": {
38
+ "ws": "^8.18.0"
39
+ },
40
+ "files": [
41
+ "index.js",
42
+ "bin/mags.js",
43
+ "README.md"
44
+ ]
27
45
  }