@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.
@@ -0,0 +1,532 @@
1
+ import fetch from 'node-fetch';
2
+ import { getConfig, setConfig } from '../config/store.js';
3
+ import { hasKey, getKeyAuto } from '../config/keys.js';
4
+ import { ethers } from 'ethers';
5
+
6
+ // ══════════════════════════════════════════════════
7
+ // WEB SHELL COMMAND HANDLER
8
+ // ══════════════════════════════════════════════════
9
+ // Routes text commands to functions and streams
10
+ // ANSI-formatted output back via WebSocket.
11
+ // ══════════════════════════════════════════════════
12
+
13
+ const ANSI = {
14
+ gold: '\x1b[38;2;255;215;0m',
15
+ dim: '\x1b[38;2;102;102;102m',
16
+ green: '\x1b[38;2;0;255;136m',
17
+ red: '\x1b[38;2;255;68;68m',
18
+ blue: '\x1b[38;2;68;136;255m',
19
+ white: '\x1b[1;37m',
20
+ reset: '\x1b[0m',
21
+ darkGold: '\x1b[38;2;184;134;11m',
22
+ };
23
+
24
+ const RPCS = {
25
+ base: 'https://mainnet.base.org',
26
+ ethereum: 'https://eth.llamarpc.com',
27
+ arbitrum: 'https://arb1.arbitrum.io/rpc',
28
+ optimism: 'https://mainnet.optimism.io',
29
+ polygon: 'https://polygon-rpc.com',
30
+ };
31
+
32
+ const USDC_ADDRESSES = {
33
+ base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
34
+ ethereum: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
35
+ arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
36
+ optimism: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
37
+ polygon: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
38
+ };
39
+
40
+ const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
41
+
42
+ /**
43
+ * Handle a command string, return { output } or stream via ws helpers
44
+ */
45
+ export async function handleCommand(cmd, ws) {
46
+ const parts = cmd.trim().split(/\s+/);
47
+ const main = parts[0]?.toLowerCase();
48
+ const args = parts.slice(1);
49
+
50
+ switch (main) {
51
+ case 'price':
52
+ return await cmdPrice(args, ws);
53
+ case 'watch':
54
+ return await cmdWatch(args, ws);
55
+ case 'gas':
56
+ return await cmdGas(args, ws);
57
+ case 'portfolio':
58
+ return await cmdPortfolio(args, ws);
59
+ case 'history':
60
+ return await cmdHistory(args, ws);
61
+ case 'market':
62
+ return await cmdMarket(args, ws);
63
+ case 'wallet':
64
+ return await cmdWallet(args, ws);
65
+ case 'mail':
66
+ return await cmdMail(args, ws);
67
+ case 'config':
68
+ return await cmdConfig(ws);
69
+ case 'oracle':
70
+ return await cmdOracle(args, ws);
71
+ case 'casino':
72
+ return await cmdCasino(args, ws);
73
+ case 'facilitator':
74
+ return await cmdFacilitator(args, ws);
75
+ default:
76
+ return {
77
+ output: `\r\n ${ANSI.red}✗ Unknown command: ${cmd}${ANSI.reset}\r\n ${ANSI.dim}Type ${ANSI.gold}help${ANSI.dim} for available commands.${ANSI.reset}\r\n\r\n`,
78
+ };
79
+ }
80
+ }
81
+
82
+ // ══════════════════════════════════════════════════
83
+ // PRICE
84
+ // ══════════════════════════════════════════════════
85
+ async function cmdPrice(tokens, ws) {
86
+ if (!tokens.length) {
87
+ return { output: ` ${ANSI.dim}Usage: price ETH AERO VIRTUAL${ANSI.reset}\r\n` };
88
+ }
89
+
90
+ ws.sendLine(`${ANSI.gold} ◆ PRICE CHECK${ANSI.reset}`);
91
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
92
+
93
+ for (const token of tokens) {
94
+ try {
95
+ const resp = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${token}`);
96
+ const data = await resp.json();
97
+ const pair = data.pairs?.[0];
98
+
99
+ if (!pair) {
100
+ ws.sendLine(` ${ANSI.dim}${token.toUpperCase().padEnd(10)} Not found${ANSI.reset}`);
101
+ continue;
102
+ }
103
+
104
+ const price = parseFloat(pair.priceUsd);
105
+ const change = pair.priceChange?.h24 || 0;
106
+ const changeStr = change >= 0
107
+ ? `${ANSI.green}+${change.toFixed(2)}%${ANSI.reset}`
108
+ : `${ANSI.red}${change.toFixed(2)}%${ANSI.reset}`;
109
+ const priceStr = formatPrice(price);
110
+
111
+ ws.sendLine(` ${ANSI.gold}${pair.baseToken.symbol.padEnd(10)}${ANSI.reset} ${priceStr.padEnd(14)} ${changeStr}`);
112
+ } catch {
113
+ ws.sendLine(` ${ANSI.dim}${token.padEnd(10)} Error${ANSI.reset}`);
114
+ }
115
+ }
116
+
117
+ ws.sendLine('');
118
+ return {};
119
+ }
120
+
121
+ // ══════════════════════════════════════════════════
122
+ // WATCH (streaming)
123
+ // ══════════════════════════════════════════════════
124
+ async function cmdWatch(args, ws) {
125
+ const token = args[0];
126
+ if (!token) {
127
+ return { output: ` ${ANSI.dim}Usage: watch ETH${ANSI.reset}\r\n` };
128
+ }
129
+
130
+ ws.sendLine(`${ANSI.gold} ◆ WATCHING ${token.toUpperCase()}${ANSI.reset}`);
131
+ ws.sendLine(`${ANSI.dim} Polling every 10s — send any command to stop${ANSI.reset}`);
132
+ ws.sendLine('');
133
+
134
+ // Do 5 ticks then stop (web shell context — don't run forever)
135
+ let lastPrice = null;
136
+ for (let i = 0; i < 5; i++) {
137
+ try {
138
+ const resp = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${token}`);
139
+ const data = await resp.json();
140
+ const pair = data.pairs?.[0];
141
+
142
+ if (!pair) {
143
+ ws.sendLine(` ${ANSI.dim}${timestamp()} No data${ANSI.reset}`);
144
+ } else {
145
+ const price = parseFloat(pair.priceUsd);
146
+ let arrow = ' ';
147
+ if (lastPrice !== null) {
148
+ arrow = price > lastPrice ? `${ANSI.green}▲ ${ANSI.reset}` : price < lastPrice ? `${ANSI.red}▼ ${ANSI.reset}` : `${ANSI.dim}= ${ANSI.reset}`;
149
+ }
150
+ const change = pair.priceChange?.h24 || 0;
151
+ const changeStr = change >= 0 ? `${ANSI.green}+${change.toFixed(2)}%${ANSI.reset}` : `${ANSI.red}${change.toFixed(2)}%${ANSI.reset}`;
152
+ ws.sendLine(` ${ANSI.dim}${timestamp()}${ANSI.reset} ${arrow}${ANSI.gold}${formatPrice(price).padEnd(14)}${ANSI.reset} ${changeStr}`);
153
+ lastPrice = price;
154
+ }
155
+ } catch {
156
+ ws.sendLine(` ${ANSI.dim}${timestamp()} Error${ANSI.reset}`);
157
+ }
158
+
159
+ if (i < 4) await sleep(10000);
160
+ }
161
+
162
+ ws.sendLine('');
163
+ ws.sendLine(` ${ANSI.dim}Watch complete (5 ticks). Run again: watch ${token}${ANSI.reset}`);
164
+ ws.sendLine('');
165
+ return {};
166
+ }
167
+
168
+ // ══════════════════════════════════════════════════
169
+ // GAS
170
+ // ══════════════════════════════════════════════════
171
+ async function cmdGas(args, ws) {
172
+ const chain = args[0] || 'base';
173
+ const rpc = RPCS[chain];
174
+ if (!rpc) {
175
+ return { output: ` ${ANSI.red}Unknown chain: ${chain}. Try: base, ethereum, arbitrum, optimism, polygon${ANSI.reset}\r\n` };
176
+ }
177
+
178
+ ws.sendLine(`${ANSI.gold} ◆ GAS — ${chain.toUpperCase()}${ANSI.reset}`);
179
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
180
+
181
+ try {
182
+ const provider = new ethers.JsonRpcProvider(rpc);
183
+ const feeData = await provider.getFeeData();
184
+ const gasPrice = feeData.gasPrice;
185
+
186
+ const gwei = parseFloat(ethers.formatUnits(gasPrice, 'gwei'));
187
+
188
+ // Estimate costs
189
+ const ethPrice = await getEthPrice();
190
+ const ops = [
191
+ ['ETH Transfer', 21000],
192
+ ['ERC-20 Transfer', 65000],
193
+ ['Uniswap Swap', 180000],
194
+ ['Contract Deploy', 500000],
195
+ ];
196
+
197
+ for (const [name, gas] of ops) {
198
+ const costWei = gasPrice * BigInt(gas);
199
+ const costEth = parseFloat(ethers.formatEther(costWei));
200
+ const costUsd = (costEth * ethPrice).toFixed(4);
201
+
202
+ ws.sendLine(` ${ANSI.white}${name.padEnd(20)}${ANSI.reset} ${ANSI.dim}${costEth.toFixed(6)} ETH${ANSI.reset} ${ANSI.green}$${costUsd}${ANSI.reset}`);
203
+ }
204
+
205
+ ws.sendLine('');
206
+ ws.sendLine(` ${ANSI.dim}Gas price: ${gwei.toFixed(2)} gwei | ETH: $${ethPrice.toFixed(0)}${ANSI.reset}`);
207
+ ws.sendLine('');
208
+ } catch (err) {
209
+ ws.sendLine(` ${ANSI.red}Error: ${err.message}${ANSI.reset}`);
210
+ ws.sendLine('');
211
+ }
212
+
213
+ return {};
214
+ }
215
+
216
+ // ══════════════════════════════════════════════════
217
+ // PORTFOLIO
218
+ // ══════════════════════════════════════════════════
219
+ async function cmdPortfolio(args, ws) {
220
+ const activeWallet = getConfig('activeWallet');
221
+ if (!activeWallet) {
222
+ return { output: ` ${ANSI.red}No active wallet. Use: wallet list${ANSI.reset}\r\n` };
223
+ }
224
+
225
+ // Need to load wallet to get address
226
+ const { loadWallet } = await import('../wallet/keystore.js');
227
+ let address;
228
+ try {
229
+ const w = loadWallet(activeWallet);
230
+ address = w.address;
231
+ } catch {
232
+ return { output: ` ${ANSI.red}Cannot load wallet: ${activeWallet}${ANSI.reset}\r\n` };
233
+ }
234
+
235
+ ws.sendLine(`${ANSI.gold} ◆ PORTFOLIO — ${activeWallet}${ANSI.reset}`);
236
+ ws.sendLine(`${ANSI.dim} ${address}${ANSI.reset}`);
237
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
238
+
239
+ const ethPrice = await getEthPrice();
240
+ let totalUsd = 0;
241
+
242
+ for (const [chain, rpc] of Object.entries(RPCS)) {
243
+ try {
244
+ const provider = new ethers.JsonRpcProvider(rpc);
245
+ const ethBal = parseFloat(ethers.formatEther(await provider.getBalance(address)));
246
+
247
+ let usdcBal = 0;
248
+ const usdcAddr = USDC_ADDRESSES[chain];
249
+ if (usdcAddr) {
250
+ try {
251
+ const usdc = new ethers.Contract(usdcAddr, ERC20_ABI, provider);
252
+ usdcBal = parseFloat(ethers.formatUnits(await usdc.balanceOf(address), 6));
253
+ } catch {}
254
+ }
255
+
256
+ const chainUsd = ethBal * ethPrice + usdcBal;
257
+ totalUsd += chainUsd;
258
+
259
+ const ethStr = ethBal > 0 ? `${ethBal.toFixed(4)} ETH` : `${ANSI.dim}0 ETH${ANSI.reset}`;
260
+ const usdcStr = usdcBal > 0 ? `${usdcBal.toFixed(2)} USDC` : '';
261
+ const usdStr = chainUsd > 0.01 ? `${ANSI.green}$${chainUsd.toFixed(2)}${ANSI.reset}` : `${ANSI.dim}$0.00${ANSI.reset}`;
262
+
263
+ ws.sendLine(` ${ANSI.white}${chain.padEnd(12)}${ANSI.reset} ${ethStr.padEnd(20)} ${usdcStr.padEnd(16)} ${usdStr}`);
264
+ } catch {
265
+ ws.sendLine(` ${ANSI.white}${chain.padEnd(12)}${ANSI.reset} ${ANSI.dim}Error${ANSI.reset}`);
266
+ }
267
+ }
268
+
269
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
270
+ ws.sendLine(` ${ANSI.gold}TOTAL${ANSI.reset} ${ANSI.white}$${totalUsd.toFixed(2)} USD${ANSI.reset}`);
271
+ ws.sendLine('');
272
+ return {};
273
+ }
274
+
275
+ // ══════════════════════════════════════════════════
276
+ // MARKET
277
+ // ══════════════════════════════════════════════════
278
+ async function cmdMarket(args, ws) {
279
+ const token = args[0];
280
+ if (!token) {
281
+ return { output: ` ${ANSI.dim}Usage: market ETH${ANSI.reset}\r\n` };
282
+ }
283
+
284
+ try {
285
+ const resp = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${token}`);
286
+ const data = await resp.json();
287
+ const pair = data.pairs?.[0];
288
+
289
+ if (!pair) {
290
+ return { output: ` ${ANSI.dim}No data for ${token}${ANSI.reset}\r\n` };
291
+ }
292
+
293
+ ws.sendLine(`${ANSI.gold} ◆ MARKET — ${pair.baseToken.symbol}${ANSI.reset}`);
294
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
295
+
296
+ const fields = [
297
+ ['Price', `$${parseFloat(pair.priceUsd).toFixed(6)}`],
298
+ ['24h Change', `${(pair.priceChange?.h24 || 0) >= 0 ? '+' : ''}${(pair.priceChange?.h24 || 0).toFixed(2)}%`],
299
+ ['24h Volume', pair.volume?.h24 ? `$${(pair.volume.h24 / 1e6).toFixed(2)}M` : '—'],
300
+ ['Liquidity', pair.liquidity?.usd ? `$${(pair.liquidity.usd / 1e6).toFixed(2)}M` : '—'],
301
+ ['Chain', pair.chainId || '—'],
302
+ ['DEX', pair.dexId || '—'],
303
+ ['Pair', `${pair.baseToken.symbol}/${pair.quoteToken.symbol}`],
304
+ ['Address', pair.baseToken.address?.slice(0, 20) + '...' || '—'],
305
+ ];
306
+
307
+ for (const [label, value] of fields) {
308
+ ws.sendLine(` ${ANSI.darkGold}${label.padEnd(14)}${ANSI.reset} ${ANSI.white}${value}${ANSI.reset}`);
309
+ }
310
+
311
+ ws.sendLine('');
312
+ } catch (err) {
313
+ return { output: ` ${ANSI.red}Error: ${err.message}${ANSI.reset}\r\n` };
314
+ }
315
+
316
+ return {};
317
+ }
318
+
319
+ // ══════════════════════════════════════════════════
320
+ // WALLET
321
+ // ══════════════════════════════════════════════════
322
+ async function cmdWallet(args, ws) {
323
+ const sub = args[0] || 'list';
324
+
325
+ if (sub === 'list') {
326
+ const { listWallets } = await import('../wallet/keystore.js');
327
+ const wallets = listWallets();
328
+ const active = getConfig('activeWallet');
329
+
330
+ ws.sendLine(`${ANSI.gold} ◆ WALLETS${ANSI.reset}`);
331
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
332
+
333
+ if (wallets.length === 0) {
334
+ ws.sendLine(` ${ANSI.dim}No wallets. Create one in the CLI: darksol wallet create${ANSI.reset}`);
335
+ } else {
336
+ for (const w of wallets) {
337
+ const indicator = w === active ? `${ANSI.gold}► ${ANSI.reset}` : ' ';
338
+ ws.sendLine(` ${indicator}${ANSI.white}${w}${ANSI.reset}`);
339
+ }
340
+ }
341
+
342
+ ws.sendLine('');
343
+ return {};
344
+ }
345
+
346
+ if (sub === 'balance') {
347
+ const name = args[1] || getConfig('activeWallet');
348
+ if (!name) return { output: ` ${ANSI.red}No active wallet${ANSI.reset}\r\n` };
349
+
350
+ const { loadWallet } = await import('../wallet/keystore.js');
351
+ const w = loadWallet(name);
352
+ const chain = getConfig('chain') || 'base';
353
+ const provider = new ethers.JsonRpcProvider(RPCS[chain]);
354
+ const bal = parseFloat(ethers.formatEther(await provider.getBalance(w.address)));
355
+
356
+ ws.sendLine(`${ANSI.gold} ◆ BALANCE — ${name}${ANSI.reset}`);
357
+ ws.sendLine(`${ANSI.dim} ${w.address}${ANSI.reset}`);
358
+ ws.sendLine(` ${ANSI.white}${bal.toFixed(6)} ETH${ANSI.reset} on ${chain}`);
359
+ ws.sendLine('');
360
+ return {};
361
+ }
362
+
363
+ return { output: ` ${ANSI.dim}Wallet commands: list, balance${ANSI.reset}\r\n` };
364
+ }
365
+
366
+ // ══════════════════════════════════════════════════
367
+ // MAIL
368
+ // ══════════════════════════════════════════════════
369
+ async function cmdMail(args, ws) {
370
+ const sub = args[0] || 'status';
371
+ const hasApiKey = hasKey('agentmail') || !!process.env.AGENTMAIL_API_KEY;
372
+
373
+ if (sub === 'status') {
374
+ const email = getConfig('mailEmail');
375
+
376
+ ws.sendLine(`${ANSI.gold} ◆ AGENTMAIL STATUS${ANSI.reset}`);
377
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
378
+ ws.sendLine(` ${ANSI.darkGold}API Key${ANSI.reset} ${hasApiKey ? `${ANSI.green}● Connected${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
379
+ ws.sendLine(` ${ANSI.darkGold}Inbox${ANSI.reset} ${email || `${ANSI.dim}(none)${ANSI.reset}`}`);
380
+ ws.sendLine(` ${ANSI.darkGold}Console${ANSI.reset} ${ANSI.blue}console.agentmail.to${ANSI.reset}`);
381
+
382
+ if (!hasApiKey) {
383
+ ws.sendLine('');
384
+ ws.sendLine(` ${ANSI.dim}Set up in CLI: darksol mail setup${ANSI.reset}`);
385
+ }
386
+
387
+ ws.sendLine('');
388
+ return {};
389
+ }
390
+
391
+ if (sub === 'inbox' && hasApiKey) {
392
+ try {
393
+ const { AgentMailClient } = await import('agentmail');
394
+ const apiKey = getKeyAuto('agentmail') || process.env.AGENTMAIL_API_KEY;
395
+ const client = new AgentMailClient({ apiKey });
396
+ const inboxId = getConfig('mailInboxId');
397
+
398
+ if (!inboxId) {
399
+ return { output: ` ${ANSI.dim}No active inbox. Create one in CLI: darksol mail create${ANSI.reset}\r\n` };
400
+ }
401
+
402
+ const result = await client.inboxes.messages.list(inboxId, { limit: 5 });
403
+ const messages = result.messages || [];
404
+
405
+ ws.sendLine(`${ANSI.gold} ◆ INBOX — ${getConfig('mailEmail') || 'messages'}${ANSI.reset}`);
406
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
407
+
408
+ if (messages.length === 0) {
409
+ ws.sendLine(` ${ANSI.dim}No messages${ANSI.reset}`);
410
+ } else {
411
+ for (const [i, m] of messages.entries()) {
412
+ const from = m.from?.address || m.from || '?';
413
+ const shortFrom = from.length > 22 ? from.slice(0, 19) + '...' : from;
414
+ const subject = (m.subject || '(no subject)').slice(0, 30);
415
+ ws.sendLine(` ${ANSI.green}${(i + 1).toString().padEnd(3)}${ANSI.reset}${shortFrom.padEnd(24)} ${ANSI.white}${subject}${ANSI.reset}`);
416
+ }
417
+ }
418
+
419
+ ws.sendLine('');
420
+ return {};
421
+ } catch (err) {
422
+ return { output: ` ${ANSI.red}Error: ${err.message}${ANSI.reset}\r\n` };
423
+ }
424
+ }
425
+
426
+ return { output: ` ${ANSI.dim}Mail commands: status, inbox. Full features in CLI.${ANSI.reset}\r\n` };
427
+ }
428
+
429
+ // ══════════════════════════════════════════════════
430
+ // HISTORY
431
+ // ══════════════════════════════════════════════════
432
+ async function cmdHistory(args, ws) {
433
+ return { output: ` ${ANSI.dim}Transaction history requires CLI: darksol wallet history${ANSI.reset}\r\n` };
434
+ }
435
+
436
+ // ══════════════════════════════════════════════════
437
+ // SERVICE COMMANDS (thin wrappers)
438
+ // ══════════════════════════════════════════════════
439
+ async function cmdOracle(args, ws) {
440
+ try {
441
+ const resp = await fetch('https://acp.darksol.net/oracle');
442
+ const data = await resp.json();
443
+
444
+ ws.sendLine(`${ANSI.gold} ◆ ORACLE${ANSI.reset}`);
445
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
446
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${data.status || 'unknown'}`);
447
+ ws.sendLine(` ${ANSI.darkGold}Endpoint${ANSI.reset} ${ANSI.blue}acp.darksol.net/oracle${ANSI.reset}`);
448
+ ws.sendLine('');
449
+ } catch {
450
+ ws.sendLine(` ${ANSI.dim}Oracle unreachable${ANSI.reset}`);
451
+ ws.sendLine('');
452
+ }
453
+ return {};
454
+ }
455
+
456
+ async function cmdCasino(args, ws) {
457
+ try {
458
+ const resp = await fetch('https://casino.darksol.net/health');
459
+ const data = await resp.json();
460
+
461
+ ws.sendLine(`${ANSI.gold} ◆ CASINO${ANSI.reset}`);
462
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
463
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${data.status || 'unknown'}`);
464
+ ws.sendLine(` ${ANSI.darkGold}Endpoint${ANSI.reset} ${ANSI.blue}casino.darksol.net${ANSI.reset}`);
465
+ ws.sendLine('');
466
+ } catch {
467
+ ws.sendLine(` ${ANSI.dim}Casino unreachable${ANSI.reset}`);
468
+ ws.sendLine('');
469
+ }
470
+ return {};
471
+ }
472
+
473
+ async function cmdFacilitator(args, ws) {
474
+ try {
475
+ const resp = await fetch('https://facilitator.darksol.net/health');
476
+ const data = await resp.json();
477
+
478
+ ws.sendLine(`${ANSI.gold} ◆ FACILITATOR${ANSI.reset}`);
479
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
480
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${data.status || 'unknown'}`);
481
+ ws.sendLine(` ${ANSI.darkGold}Endpoint${ANSI.reset} ${ANSI.blue}facilitator.darksol.net${ANSI.reset}`);
482
+ ws.sendLine('');
483
+ } catch {
484
+ ws.sendLine(` ${ANSI.dim}Facilitator unreachable${ANSI.reset}`);
485
+ ws.sendLine('');
486
+ }
487
+ return {};
488
+ }
489
+
490
+ async function cmdConfig(ws) {
491
+ const chain = getConfig('chain') || 'base';
492
+ const wallet = getConfig('activeWallet') || '(none)';
493
+ const slippage = getConfig('slippage') || '0.5';
494
+ const email = getConfig('mailEmail') || '(none)';
495
+
496
+ ws.sendLine(`${ANSI.gold} ◆ CONFIG${ANSI.reset}`);
497
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
498
+ ws.sendLine(` ${ANSI.darkGold}Chain${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
499
+ ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset}`);
500
+ ws.sendLine(` ${ANSI.darkGold}Slippage${ANSI.reset} ${ANSI.white}${slippage}%${ANSI.reset}`);
501
+ ws.sendLine(` ${ANSI.darkGold}Mail${ANSI.reset} ${ANSI.white}${email}${ANSI.reset}`);
502
+ ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasKey('openai') || hasKey('anthropic') || hasKey('openrouter') || hasKey('ollama') ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
503
+ ws.sendLine('');
504
+ return {};
505
+ }
506
+
507
+ // ══════════════════════════════════════════════════
508
+ // HELPERS
509
+ // ══════════════════════════════════════════════════
510
+ function formatPrice(price) {
511
+ if (price >= 1) return `$${price.toFixed(2)}`;
512
+ if (price >= 0.01) return `$${price.toFixed(4)}`;
513
+ return `$${price.toFixed(8)}`;
514
+ }
515
+
516
+ function timestamp() {
517
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
518
+ }
519
+
520
+ function sleep(ms) {
521
+ return new Promise((r) => setTimeout(r, ms));
522
+ }
523
+
524
+ async function getEthPrice() {
525
+ try {
526
+ const resp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
527
+ const data = await resp.json();
528
+ return data.ethereum?.usd || 3000;
529
+ } catch {
530
+ return 3000;
531
+ }
532
+ }
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>DARKSOL Terminal</title>
7
+ <meta name="description" content="DARKSOL Terminal — Ghost in the machine with teeth. All services. One terminal." />
8
+ <meta name="theme-color" content="#0a0a1a" />
9
+
10
+ <!-- Favicon -->
11
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌑</text></svg>" />
12
+
13
+ <!-- xterm.js from CDN -->
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" />
15
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
17
+
18
+ <!-- Terminal theme -->
19
+ <link rel="stylesheet" href="/terminal.css" />
20
+ </head>
21
+ <body>
22
+ <!-- Top bar (macOS-style window chrome) -->
23
+ <div class="top-bar">
24
+ <div class="dots">
25
+ <div class="dot red"></div>
26
+ <div class="dot yellow"></div>
27
+ <div class="dot green"></div>
28
+ </div>
29
+ <span class="title">DARKSOL TERMINAL</span>
30
+ <span class="badge">v0.4.0</span>
31
+ </div>
32
+
33
+ <!-- Terminal -->
34
+ <div id="terminal-container"></div>
35
+
36
+ <!-- Status bar -->
37
+ <div class="status-bar">
38
+ <span class="status-dot"></span>
39
+ <span class="status-text">Connecting...</span>
40
+ <span style="flex: 1;"></span>
41
+ <span>127.0.0.1</span>
42
+ <span>•</span>
43
+ <span>ws</span>
44
+ </div>
45
+
46
+ <!-- Terminal client -->
47
+ <script src="/terminal.js"></script>
48
+ </body>
49
+ </html>