@darksol/terminal 0.4.1 → 0.4.3

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/README.md CHANGED
@@ -30,15 +30,30 @@ darksol
30
30
  # Create a wallet (AES-256-GCM encrypted)
31
31
  darksol wallet create main
32
32
 
33
- # Check balance
33
+ # Check balance + multi-chain portfolio
34
34
  darksol wallet balance
35
+ darksol portfolio
35
36
 
36
- # Swap tokens
37
+ # Token prices & live monitoring
38
+ darksol price ETH AERO VIRTUAL
39
+ darksol watch AERO --above 2.0
40
+
41
+ # Gas estimates
42
+ darksol gas base
43
+
44
+ # Swap tokens (Uniswap V3 with slippage protection)
37
45
  darksol trade swap -i ETH -o USDC -a 0.1
38
46
 
39
47
  # AI trading assistant
40
48
  darksol ai chat
41
49
 
50
+ # Agent email
51
+ darksol mail setup
52
+ darksol mail send --to user@example.com --subject "Hello"
53
+
54
+ # Web terminal in browser
55
+ darksol serve
56
+
42
57
  # Start agent signer for OpenClaw
43
58
  darksol agent start main
44
59
  ```
@@ -58,6 +73,16 @@ darksol agent start main
58
73
  | `market` | Market intel, top movers, token analysis | x402 micropayments |
59
74
  | `oracle` | On-chain random number oracle | $0.05–$0.25 |
60
75
  | `casino` | The Clawsino — on-chain betting | $1 flat bets |
76
+ | `portfolio` | Multi-chain balance view (5 EVM chains) | Free |
77
+ | `gas` | Gas prices & cost estimates | Free |
78
+ | `price` | Quick token price check (DexScreener) | Free |
79
+ | `watch` | Live price monitoring with alerts | Free |
80
+ | `history` | Transaction history via block explorers | Free |
81
+ | `mail` | AgentMail — email for AI agents | Free tier |
82
+ | `serve` | Web terminal in browser (xterm.js) | Free |
83
+ | `facilitator` | x402 payment facilitator | Free |
84
+ | `cards` | Prepaid Visa/MC cards | Service fees |
85
+ | `builders` | ERC-8021 builder code directory | Free |
61
86
  | `cards` | Crypto → prepaid Visa/MC (no KYC) | 3% markup |
62
87
  | `builders` | ERC-8021 builder leaderboard | Free |
63
88
  | `facilitator` | x402 payment verification & settlement | Free |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,8 @@
43
43
  "open": "^11.0.0",
44
44
  "ora": "^8.1.0",
45
45
  "terminal-link": "^3.0.0",
46
- "update-notifier": "^7.3.1"
46
+ "update-notifier": "^7.3.1",
47
+ "ws": "^8.19.0"
47
48
  },
48
49
  "engines": {
49
50
  "node": ">=18.0.0"
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import { showHistory } from './wallet/history.js';
9
9
  import { showGas } from './services/gas.js';
10
10
  import { watchPrice, checkPrices } from './services/watch.js';
11
11
  import { mailSetup, mailCreate, mailInboxes, mailSend, mailList, mailRead, mailReply, mailForward, mailThreads, mailDelete, mailUse, mailStats, mailStatus } from './services/mail.js';
12
+ import { startWebShell } from './web/server.js';
12
13
  import { executeSwap } from './trading/swap.js';
13
14
  import { snipeToken, watchSnipe } from './trading/snipe.js';
14
15
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
@@ -25,6 +26,9 @@ import { parseIntent, startChat, adviseStrategy, analyzeToken, executeIntent } f
25
26
  import { startAgentSigner, showAgentDocs } from './wallet/agent-signer.js';
26
27
  import { listSkills, installSkill, skillInfo, uninstallSkill } from './services/skills.js';
27
28
  import { runSetupWizard, checkFirstRun } from './setup/wizard.js';
29
+ import { createRequire } from 'module';
30
+ const require = createRequire(import.meta.url);
31
+ const { version: PKG_VERSION } = require('../package.json');
28
32
 
29
33
  export function cli(argv) {
30
34
  const program = new Command();
@@ -32,7 +36,7 @@ export function cli(argv) {
32
36
  program
33
37
  .name('darksol')
34
38
  .description(theme.gold('DARKSOL Terminal') + theme.dim(' — Ghost in the machine with teeth 🌑'))
35
- .version('0.4.1')
39
+ .version(PKG_VERSION)
36
40
  ;
37
41
 
38
42
  // ═══════════════════════════════════════
@@ -398,6 +402,16 @@ export function cli(argv) {
398
402
  .description('Delete an inbox')
399
403
  .action((id) => mailDelete(id));
400
404
 
405
+ // ═══════════════════════════════════════
406
+ // WEB SHELL
407
+ // ═══════════════════════════════════════
408
+ program
409
+ .command('serve')
410
+ .description('🌐 Launch web terminal in browser')
411
+ .option('-p, --port <port>', 'Server port', '18791')
412
+ .option('--no-open', 'Don\'t auto-open browser')
413
+ .action((opts) => startWebShell(opts));
414
+
401
415
  // ═══════════════════════════════════════
402
416
  // PORTFOLIO SHORTCUT
403
417
  // ═══════════════════════════════════════
@@ -710,6 +724,8 @@ export function cli(argv) {
710
724
  ['Output', cfg.output],
711
725
  ['Slippage', `${cfg.slippage}%`],
712
726
  ['Gas Multiplier', `${cfg.gasMultiplier}x`],
727
+ ['Mail', cfg.mailEmail || theme.dim('(not set)')],
728
+ ['Version', PKG_VERSION],
713
729
  ['Config File', configPath()],
714
730
  ]);
715
731
  console.log('');
@@ -776,6 +792,42 @@ export function cli(argv) {
776
792
  }
777
793
  });
778
794
 
795
+ // ═══════════════════════════════════════
796
+ // FUZZY / NATURAL LANGUAGE FALLBACK
797
+ // ═══════════════════════════════════════
798
+ // If someone types something Commander doesn't recognize,
799
+ // try routing it through AI before saying "unknown command"
800
+ program.on('command:*', async (operands) => {
801
+ const input = operands.join(' ');
802
+ const { hasAnyLLM } = await import('./config/keys.js');
803
+
804
+ if (hasAnyLLM()) {
805
+ const { parseIntent } = await import('./llm/intent.js');
806
+ const { info, error: showError } = await import('./ui/components.js');
807
+
808
+ console.log('');
809
+ info(`"${input}" isn't a command — asking AI...`);
810
+ console.log('');
811
+
812
+ try {
813
+ const result = await parseIntent(input);
814
+ if (result && result.action !== 'error' && result.action !== 'unknown') {
815
+ const { executeIntent } = await import('./llm/intent.js');
816
+ await executeIntent(result);
817
+ return;
818
+ }
819
+ } catch {}
820
+
821
+ showError(`Unknown command: ${input}`);
822
+ info('Try: darksol help');
823
+ } else {
824
+ const { error: showError, info } = await import('./ui/components.js');
825
+ showError(`Unknown command: ${input}`);
826
+ info('Tip: Set up AI (darksol setup) for natural language commands');
827
+ info('Run: darksol help');
828
+ }
829
+ });
830
+
779
831
  program.parse(argv);
780
832
  }
781
833
 
@@ -932,6 +984,7 @@ function showCommandList() {
932
984
  ['mail', 'AgentMail — email for your agent'],
933
985
  ['facilitator', 'x402 payment facilitator'],
934
986
  ['skills', 'Agent skill directory'],
987
+ ['serve', 'Launch web terminal in browser'],
935
988
  ['setup', 'Re-run setup wizard'],
936
989
  ['config', 'Terminal configuration'],
937
990
  ];
@@ -1,4 +1,4 @@
1
- import fetch from 'node-fetch';
1
+ import { fetchJSON } from '../utils/fetch.js';
2
2
  import { getServiceURL } from '../config/store.js';
3
3
  import { theme } from '../ui/theme.js';
4
4
  import { spinner, table, kvDisplay, error } from '../ui/components.js';
@@ -9,8 +9,7 @@ const getURL = () => getServiceURL('builders') || 'https://builders.darksol.net'
9
9
  export async function buildersLeaderboard(opts = {}) {
10
10
  const spin = spinner('Loading builder leaderboard...').start();
11
11
  try {
12
- const resp = await fetch(`${getURL()}/api/leaderboard?limit=${opts.limit || 20}`);
13
- const data = await resp.json();
12
+ const data = await fetchJSON(`${getURL()}/api/leaderboard?limit=${opts.limit || 20}`);
14
13
  spin.succeed('Leaderboard loaded');
15
14
 
16
15
  showSection('ERC-8021 BUILDER LEADERBOARD');
@@ -34,8 +33,7 @@ export async function buildersLeaderboard(opts = {}) {
34
33
  export async function buildersLookup(code) {
35
34
  const spin = spinner(`Looking up builder: ${code}...`).start();
36
35
  try {
37
- const resp = await fetch(`${getURL()}/api/builders/${code}`);
38
- const data = await resp.json();
36
+ const data = await fetchJSON(`${getURL()}/api/builders/${code}`);
39
37
  spin.succeed('Builder found');
40
38
 
41
39
  showSection(`BUILDER — ${code}`);
@@ -49,8 +47,7 @@ export async function buildersLookup(code) {
49
47
  export async function buildersFeed(opts = {}) {
50
48
  const spin = spinner('Loading builder feed...').start();
51
49
  try {
52
- const resp = await fetch(`${getURL()}/api/feed?limit=${opts.limit || 20}`);
53
- const data = await resp.json();
50
+ const data = await fetchJSON(`${getURL()}/api/feed?limit=${opts.limit || 20}`);
54
51
  spin.succeed('Feed loaded');
55
52
 
56
53
  showSection('BUILDER FEED');
@@ -1,7 +1,7 @@
1
- import fetch from 'node-fetch';
1
+ import { fetchJSON } from '../utils/fetch.js';
2
2
  import { getServiceURL } from '../config/store.js';
3
3
  import { theme } from '../ui/theme.js';
4
- import { spinner, kvDisplay, error, table } from '../ui/components.js';
4
+ import { spinner, kvDisplay, error, info, table } from '../ui/components.js';
5
5
  import { showSection } from '../ui/banner.js';
6
6
 
7
7
  const getURL = () => getServiceURL('cards') || 'https://acp.darksol.net/cards';
@@ -9,8 +9,7 @@ const getURL = () => getServiceURL('cards') || 'https://acp.darksol.net/cards';
9
9
  export async function cardsCatalog() {
10
10
  const spin = spinner('Loading card catalog...').start();
11
11
  try {
12
- const resp = await fetch(`${getURL()}/api/cards/catalog`);
13
- const data = await resp.json();
12
+ const data = await fetchJSON(`${getURL()}/api/cards/catalog`);
14
13
  spin.succeed('Catalog loaded');
15
14
 
16
15
  showSection('PREPAID CARDS');
@@ -29,18 +28,18 @@ export async function cardsCatalog() {
29
28
  } catch (err) {
30
29
  spin.fail('Catalog failed');
31
30
  error(err.message);
31
+ info('Cards service: https://acp.darksol.net/cards');
32
32
  }
33
33
  }
34
34
 
35
35
  export async function cardsOrder(provider, amount) {
36
36
  const spin = spinner('Processing card order...').start();
37
37
  try {
38
- const resp = await fetch(`${getURL()}/api/cards/order`, {
38
+ const data = await fetchJSON(`${getURL()}/api/cards/order`, {
39
39
  method: 'POST',
40
40
  headers: { 'Content-Type': 'application/json' },
41
41
  body: JSON.stringify({ provider, amount }),
42
42
  });
43
- const data = await resp.json();
44
43
  spin.succeed('Order placed');
45
44
 
46
45
  showSection('CARD ORDER');
@@ -48,14 +47,14 @@ export async function cardsOrder(provider, amount) {
48
47
  } catch (err) {
49
48
  spin.fail('Order failed');
50
49
  error(err.message);
50
+ info('The cards order API may not be live yet. Check: https://acp.darksol.net/cards');
51
51
  }
52
52
  }
53
53
 
54
54
  export async function cardsStatus(orderId) {
55
55
  const spin = spinner('Checking order...').start();
56
56
  try {
57
- const resp = await fetch(`${getURL()}/api/cards/status?orderId=${orderId}`);
58
- const data = await resp.json();
57
+ const data = await fetchJSON(`${getURL()}/api/cards/status?orderId=${orderId}`);
59
58
  spin.succeed('Status loaded');
60
59
 
61
60
  showSection(`CARD ORDER — ${orderId}`);
@@ -1,3 +1,4 @@
1
+ import { fetchJSON } from '../utils/fetch.js';
1
2
  import fetch from 'node-fetch';
2
3
  import { getServiceURL } from '../config/store.js';
3
4
  import { theme } from '../ui/theme.js';
@@ -9,7 +10,7 @@ const getURL = () => getServiceURL('casino') || 'https://casino.darksol.net';
9
10
  export async function casinoBet(game, opts = {}) {
10
11
  const spin = spinner(`Placing ${game} bet...`).start();
11
12
  try {
12
- const resp = await fetch(`${getURL()}/api/bet`, {
13
+ const data = await fetchJSON(`${getURL()}/api/bet`, {
13
14
  method: 'POST',
14
15
  headers: { 'Content-Type': 'application/json' },
15
16
  body: JSON.stringify({
@@ -19,7 +20,6 @@ export async function casinoBet(game, opts = {}) {
19
20
  wallet: opts.wallet,
20
21
  }),
21
22
  });
22
- const data = await resp.json();
23
23
  spin.succeed('Bet placed');
24
24
 
25
25
  showSection(`CASINO — ${game.toUpperCase()}`);
@@ -40,8 +40,7 @@ export async function casinoBet(game, opts = {}) {
40
40
  export async function casinoTables() {
41
41
  const spin = spinner('Loading tables...').start();
42
42
  try {
43
- const resp = await fetch(`${getURL()}/api/tables`);
44
- const data = await resp.json();
43
+ const data = await fetchJSON(`${getURL()}/api/tables`);
45
44
  spin.succeed('Tables loaded');
46
45
 
47
46
  showSection('CASINO TABLES');
@@ -66,8 +65,7 @@ export async function casinoTables() {
66
65
  export async function casinoStats() {
67
66
  const spin = spinner('Loading stats...').start();
68
67
  try {
69
- const resp = await fetch(`${getURL()}/api/stats`);
70
- const data = await resp.json();
68
+ const data = await fetchJSON(`${getURL()}/api/stats`);
71
69
  spin.succeed('Stats loaded');
72
70
 
73
71
  showSection('CASINO STATS');
@@ -81,8 +79,7 @@ export async function casinoStats() {
81
79
  export async function casinoReceipt(id) {
82
80
  const spin = spinner(`Loading receipt ${id}...`).start();
83
81
  try {
84
- const resp = await fetch(`${getURL()}/api/receipt/${id}`);
85
- const data = await resp.json();
82
+ const data = await fetchJSON(`${getURL()}/api/receipt/${id}`);
86
83
  spin.succeed('Receipt loaded');
87
84
 
88
85
  showSection(`CASINO RECEIPT — ${id}`);
@@ -92,3 +89,4 @@ export async function casinoReceipt(id) {
92
89
  error(err.message);
93
90
  }
94
91
  }
92
+
@@ -1,4 +1,4 @@
1
- import fetch from 'node-fetch';
1
+ import { fetchJSON } from '../utils/fetch.js';
2
2
  import { getServiceURL } from '../config/store.js';
3
3
  import { theme } from '../ui/theme.js';
4
4
  import { spinner, kvDisplay, error } from '../ui/components.js';
@@ -9,8 +9,7 @@ const getURL = () => getServiceURL('facilitator') || 'https://facilitator.darkso
9
9
  export async function facilitatorHealth() {
10
10
  const spin = spinner('Checking facilitator...').start();
11
11
  try {
12
- const resp = await fetch(`${getURL()}/api/health`);
13
- const data = await resp.json();
12
+ const data = await fetchJSON(`${getURL()}/api/health`);
14
13
  spin.succeed('Facilitator online');
15
14
 
16
15
  showSection('FACILITATOR STATUS');
@@ -24,12 +23,11 @@ export async function facilitatorHealth() {
24
23
  export async function facilitatorVerify(payment) {
25
24
  const spin = spinner('Verifying payment...').start();
26
25
  try {
27
- const resp = await fetch(`${getURL()}/api/verify`, {
26
+ const data = await fetchJSON(`${getURL()}/api/verify`, {
28
27
  method: 'POST',
29
28
  headers: { 'Content-Type': 'application/json' },
30
29
  body: JSON.stringify(typeof payment === 'string' ? JSON.parse(payment) : payment),
31
30
  });
32
- const data = await resp.json();
33
31
  spin.succeed(data.valid ? 'Payment valid' : 'Payment invalid');
34
32
 
35
33
  showSection('PAYMENT VERIFICATION');
@@ -43,12 +41,11 @@ export async function facilitatorVerify(payment) {
43
41
  export async function facilitatorSettle(payment) {
44
42
  const spin = spinner('Settling on-chain...').start();
45
43
  try {
46
- const resp = await fetch(`${getURL()}/api/settle`, {
44
+ const data = await fetchJSON(`${getURL()}/api/settle`, {
47
45
  method: 'POST',
48
46
  headers: { 'Content-Type': 'application/json' },
49
47
  body: JSON.stringify(typeof payment === 'string' ? JSON.parse(payment) : payment),
50
48
  });
51
- const data = await resp.json();
52
49
  spin.succeed('Settlement complete');
53
50
 
54
51
  showSection('SETTLEMENT');
@@ -1,3 +1,4 @@
1
+ import { fetchJSON } from '../utils/fetch.js';
1
2
  import fetch from 'node-fetch';
2
3
  import { getServiceURL } from '../config/store.js';
3
4
  import { theme } from '../ui/theme.js';
@@ -9,8 +10,7 @@ const getURL = () => getServiceURL('oracle') || 'https://acp.darksol.net/oracle'
9
10
  export async function oracleFlip() {
10
11
  const spin = spinner('Flipping coin...').start();
11
12
  try {
12
- const resp = await fetch(`${getURL()}/api/coin`);
13
- const data = await resp.json();
13
+ const data = await fetchJSON(`${getURL()}/api/coin`);
14
14
  spin.succeed('Coin flipped');
15
15
  showSection('ORACLE — COIN FLIP');
16
16
  kvDisplay([
@@ -26,8 +26,7 @@ export async function oracleFlip() {
26
26
  export async function oracleDice(sides = 6) {
27
27
  const spin = spinner(`Rolling d${sides}...`).start();
28
28
  try {
29
- const resp = await fetch(`${getURL()}/api/dice?sides=${sides}`);
30
- const data = await resp.json();
29
+ const data = await fetchJSON(`${getURL()}/api/dice?sides=${sides}`);
31
30
  spin.succeed('Dice rolled');
32
31
  showSection(`ORACLE — D${sides}`);
33
32
  kvDisplay([
@@ -44,8 +43,7 @@ export async function oracleDice(sides = 6) {
44
43
  export async function oracleNumber(min = 1, max = 100) {
45
44
  const spin = spinner(`Generating number ${min}-${max}...`).start();
46
45
  try {
47
- const resp = await fetch(`${getURL()}/api/number?min=${min}&max=${max}`);
48
- const data = await resp.json();
46
+ const data = await fetchJSON(`${getURL()}/api/number?min=${min}&max=${max}`);
49
47
  spin.succeed('Number generated');
50
48
  showSection('ORACLE — RANDOM NUMBER');
51
49
  kvDisplay([
@@ -62,12 +60,11 @@ export async function oracleNumber(min = 1, max = 100) {
62
60
  export async function oracleShuffle(items) {
63
61
  const spin = spinner('Shuffling...').start();
64
62
  try {
65
- const resp = await fetch(`${getURL()}/api/shuffle`, {
63
+ const data = await fetchJSON(`${getURL()}/api/shuffle`, {
66
64
  method: 'POST',
67
65
  headers: { 'Content-Type': 'application/json' },
68
66
  body: JSON.stringify({ items }),
69
67
  });
70
- const data = await resp.json();
71
68
  spin.succeed('Shuffled');
72
69
  showSection('ORACLE — SHUFFLE');
73
70
  console.log(theme.gold(' Result: ') + (data.result || data.value || []).join(', '));
@@ -80,8 +77,7 @@ export async function oracleShuffle(items) {
80
77
  export async function oracleHealth() {
81
78
  const spin = spinner('Checking oracle...').start();
82
79
  try {
83
- const resp = await fetch(`${getURL()}/api/health`);
84
- const data = await resp.json();
80
+ const data = await fetchJSON(`${getURL()}/api/health`);
85
81
  spin.succeed('Oracle online');
86
82
  showSection('ORACLE STATUS');
87
83
  kvDisplay(Object.entries(data).map(([k, v]) => [k, String(v)]));
@@ -18,6 +18,12 @@ const ROUTERS = {
18
18
  arbitrum: {
19
19
  uniswapV3: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
20
20
  },
21
+ optimism: {
22
+ uniswapV3: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
23
+ },
24
+ polygon: {
25
+ uniswapV3: '0xE592427A0AEce92De3Edee1F18E0157C05861564',
26
+ },
21
27
  };
22
28
 
23
29
  // Common token addresses per chain
@@ -38,6 +44,26 @@ const TOKENS = {
38
44
  USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
39
45
  DAI: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
40
46
  },
47
+ arbitrum: {
48
+ ETH: ethers.ZeroAddress,
49
+ WETH: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
50
+ USDC: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
51
+ USDT: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
52
+ ARB: '0x912CE59144191C1204E64559FE8253a0e49E6548',
53
+ },
54
+ optimism: {
55
+ ETH: ethers.ZeroAddress,
56
+ WETH: '0x4200000000000000000000000000000000000006',
57
+ USDC: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
58
+ OP: '0x4200000000000000000000000000000000000042',
59
+ },
60
+ polygon: {
61
+ ETH: ethers.ZeroAddress,
62
+ WETH: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619',
63
+ WMATIC: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
64
+ USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
65
+ USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
66
+ },
41
67
  };
42
68
 
43
69
  // ERC20 ABI for approvals and balance checks
@@ -66,6 +92,8 @@ const QUOTERS = {
66
92
  base: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a',
67
93
  ethereum: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
68
94
  arbitrum: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
95
+ optimism: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
96
+ polygon: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
69
97
  };
70
98
 
71
99
  // Resolve token symbol to address
package/src/ui/banner.js CHANGED
@@ -2,6 +2,9 @@ import figlet from 'figlet';
2
2
  import gradient from 'gradient-string';
3
3
  import chalk from 'chalk';
4
4
  import { theme } from './theme.js';
5
+ import { createRequire } from 'module';
6
+ const require = createRequire(import.meta.url);
7
+ const { version } = require('../../package.json');
5
8
 
6
9
  const darksol_gradient = gradient(['#B8860B', '#FFD700', '#FFF8DC', '#FFD700', '#B8860B']);
7
10
 
@@ -11,6 +14,9 @@ export function showBanner(opts = {}) {
11
14
  horizontalLayout: 'fitted',
12
15
  });
13
16
 
17
+ const vStr = `v${version}`;
18
+ const pad = ' '.repeat(Math.max(0, 48 - vStr.length));
19
+
14
20
  console.log('');
15
21
  console.log(darksol_gradient(banner));
16
22
  console.log('');
@@ -26,8 +32,8 @@ export function showBanner(opts = {}) {
26
32
  );
27
33
  console.log(
28
34
  theme.dim(' ║ ') +
29
- theme.subtle(' v0.4.0') +
30
- theme.dim(' ') +
35
+ theme.subtle(` ${vStr}`) +
36
+ theme.dim(pad) +
31
37
  theme.gold('🌑') +
32
38
  theme.dim(' ║')
33
39
  );
@@ -44,7 +50,7 @@ export function showBanner(opts = {}) {
44
50
 
45
51
  export function showMiniBanner() {
46
52
  console.log('');
47
- console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(' v0.4.0'));
53
+ console.log(theme.gold.bold(' 🌑 DARKSOL TERMINAL') + theme.dim(` v${version}`));
48
54
  console.log(theme.dim(' ─────────────────────────────'));
49
55
  console.log('');
50
56
  }
@@ -58,17 +64,3 @@ export function showSection(title) {
58
64
  export function showDivider() {
59
65
  console.log(theme.dim(' ' + '─'.repeat(50)));
60
66
  }
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
@@ -0,0 +1,28 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ /**
4
+ * Fetch JSON safely — handles HTML error pages, non-JSON responses,
5
+ * and invalid JSON gracefully instead of crashing with cryptic errors.
6
+ */
7
+ export async function fetchJSON(url, options = {}) {
8
+ const resp = await fetch(url, options);
9
+ const contentType = resp.headers.get('content-type') || '';
10
+ const text = await resp.text();
11
+
12
+ if (!contentType.includes('application/json') && !contentType.includes('text/json')) {
13
+ const preview = text.substring(0, 60).replace(/\n/g, ' ');
14
+ throw new Error(
15
+ `Expected JSON but got ${contentType.split(';')[0] || 'unknown'} (HTTP ${resp.status}). ` +
16
+ `Response: "${preview}..."`
17
+ );
18
+ }
19
+
20
+ try {
21
+ return JSON.parse(text);
22
+ } catch {
23
+ const preview = text.substring(0, 60).replace(/\n/g, ' ');
24
+ throw new Error(`Invalid JSON (HTTP ${resp.status}): "${preview}..."`);
25
+ }
26
+ }
27
+
28
+ export default fetchJSON;