@darksol/terminal 0.4.5 → 0.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
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": {
@@ -76,10 +76,20 @@ export async function handleCommand(cmd, ws) {
76
76
  return await cmdSend(args, ws);
77
77
  case 'receive':
78
78
  return await cmdReceive(ws);
79
- default:
79
+ case 'ai':
80
+ case 'ask':
81
+ case 'chat':
82
+ return await cmdAI(args, ws);
83
+ default: {
84
+ // Fuzzy: if it looks like natural language, route to AI
85
+ const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca)\b/i;
86
+ if (nlKeywords.test(cmd)) {
87
+ return await cmdAI(cmd.split(/\s+/), ws);
88
+ }
80
89
  return {
81
- 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`,
90
+ output: `\r\n ${ANSI.red}✗ Unknown command: ${cmd}${ANSI.reset}\r\n ${ANSI.dim}Type ${ANSI.gold}help${ANSI.dim} for commands, or ${ANSI.gold}ai <question>${ANSI.dim} to chat.${ANSI.reset}\r\n\r\n`,
82
91
  };
92
+ }
83
93
  }
84
94
  }
85
95
 
@@ -508,6 +518,178 @@ async function cmdConfig(ws) {
508
518
  return {};
509
519
  }
510
520
 
521
+ // ══════════════════════════════════════════════════
522
+ // AI CHAT — LLM-powered assistant in the web shell
523
+ // ══════════════════════════════════════════════════
524
+
525
+ // Persistent chat engine per WebSocket connection
526
+ const chatEngines = new WeakMap();
527
+
528
+ async function cmdAI(args, ws) {
529
+ const input = args.join(' ').trim();
530
+
531
+ if (!input || input === 'help') {
532
+ ws.sendLine(`${ANSI.gold} ◆ AI TRADING ASSISTANT${ANSI.reset}`);
533
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
534
+ ws.sendLine('');
535
+ ws.sendLine(` ${ANSI.white}Natural language trading — just describe what you want.${ANSI.reset}`);
536
+ ws.sendLine('');
537
+ ws.sendLine(` ${ANSI.darkGold}Usage:${ANSI.reset}`);
538
+ ws.sendLine(` ${ANSI.gold}ai swap 0.1 ETH to USDC${ANSI.reset}`);
539
+ ws.sendLine(` ${ANSI.gold}ai what's the price of AERO?${ANSI.reset}`);
540
+ ws.sendLine(` ${ANSI.gold}ai analyze VIRTUAL on base${ANSI.reset}`);
541
+ ws.sendLine(` ${ANSI.gold}ai should I DCA into ETH?${ANSI.reset}`);
542
+ ws.sendLine(` ${ANSI.gold}ai send 10 USDC to 0x1234...${ANSI.reset}`);
543
+ ws.sendLine(` ${ANSI.gold}ai gas on base${ANSI.reset}`);
544
+ ws.sendLine('');
545
+ ws.sendLine(` ${ANSI.dim}Conversation history is kept for the session.${ANSI.reset}`);
546
+ ws.sendLine(` ${ANSI.dim}Type ${ANSI.gold}ai clear${ANSI.dim} to reset history.${ANSI.reset}`);
547
+ ws.sendLine('');
548
+ return {};
549
+ }
550
+
551
+ if (input === 'clear' || input === 'reset') {
552
+ if (chatEngines.has(ws)) {
553
+ chatEngines.get(ws).clearHistory();
554
+ }
555
+ ws.sendLine(` ${ANSI.green}✓ Chat history cleared${ANSI.reset}`);
556
+ ws.sendLine('');
557
+ return {};
558
+ }
559
+
560
+ if (input === 'status') {
561
+ const engine = chatEngines.get(ws);
562
+ if (engine) {
563
+ const usage = engine.getUsage();
564
+ ws.sendLine(`${ANSI.gold} ◆ AI STATUS${ANSI.reset}`);
565
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
566
+ ws.sendLine(` ${ANSI.darkGold}Provider${ANSI.reset} ${ANSI.white}${usage.provider}${ANSI.reset}`);
567
+ ws.sendLine(` ${ANSI.darkGold}Model${ANSI.reset} ${ANSI.white}${usage.model}${ANSI.reset}`);
568
+ ws.sendLine(` ${ANSI.darkGold}Messages${ANSI.reset} ${ANSI.white}${usage.calls}${ANSI.reset}`);
569
+ ws.sendLine(` ${ANSI.darkGold}Tokens${ANSI.reset} ${ANSI.white}${usage.totalTokens}${ANSI.reset}`);
570
+ ws.sendLine('');
571
+ } else {
572
+ ws.sendLine(` ${ANSI.dim}No active AI session${ANSI.reset}`);
573
+ ws.sendLine('');
574
+ }
575
+ return {};
576
+ }
577
+
578
+ // Initialize or retrieve the LLM engine
579
+ let engine = chatEngines.get(ws);
580
+ if (!engine) {
581
+ try {
582
+ const { createLLM } = await import('../llm/engine.js');
583
+ engine = await createLLM({});
584
+
585
+ const chain = getConfig('chain') || 'base';
586
+ const wallet = getConfig('activeWallet') || '(not set)';
587
+ const slippage = getConfig('slippage') || 0.5;
588
+
589
+ engine.setSystemPrompt(`You are DARKSOL Terminal's AI trading assistant running in a web terminal.
590
+
591
+ You help users with:
592
+ - Token swaps, sends, and transfers
593
+ - Price checks and market analysis
594
+ - DCA strategy recommendations
595
+ - Gas estimates and chain info
596
+ - Portfolio analysis
597
+ - General crypto/DeFi questions
598
+
599
+ USER CONTEXT:
600
+ - Active chain: ${chain}
601
+ - Active wallet: ${wallet}
602
+ - Slippage: ${slippage}%
603
+ - Supported chains: Base (default), Ethereum, Polygon, Arbitrum, Optimism
604
+
605
+ RULES:
606
+ - Be concise — this is a terminal, not a blog
607
+ - Use short paragraphs, bullet points where helpful
608
+ - Include risk warnings for any trade suggestions
609
+ - Never reveal private keys or sensitive info
610
+ - When suggesting trades, give the exact darksol CLI command
611
+ - If you detect an actionable intent (swap, send, price, etc), include the command at the end
612
+
613
+ COMMAND REFERENCE:
614
+ - darksol trade swap -i ETH -o USDC -a 0.1
615
+ - darksol send --to 0x... --amount 0.1 --token ETH
616
+ - darksol price ETH AERO VIRTUAL
617
+ - darksol gas base
618
+ - darksol wallet balance
619
+ - darksol portfolio
620
+ - darksol dca create -t ETH -a 0.01 -i 1h -n 24
621
+ - darksol ai analyze <token>`);
622
+
623
+ chatEngines.set(ws, engine);
624
+ ws.sendLine(` ${ANSI.green}● AI connected${ANSI.reset} ${ANSI.dim}(${engine.provider}/${engine.model})${ANSI.reset}`);
625
+ ws.sendLine('');
626
+ } catch (err) {
627
+ ws.sendLine(` ${ANSI.red}✗ AI initialization failed: ${err.message}${ANSI.reset}`);
628
+ ws.sendLine(` ${ANSI.dim}Configure an API key: darksol keys add openai${ANSI.reset}`);
629
+ ws.sendLine('');
630
+ return {};
631
+ }
632
+ }
633
+
634
+ // Enrich with live price data
635
+ let enriched = input;
636
+ const tokenPattern = /\b([A-Z]{2,10})\b/g;
637
+ const tokens = [...new Set(input.toUpperCase().match(tokenPattern) || [])];
638
+ const skipWords = ['ETH', 'THE', 'FOR', 'AND', 'BUY', 'SELL', 'DCA', 'SWAP', 'WHAT', 'PRICE', 'HOW', 'MUCH', 'SEND', 'SHOULD', 'CAN', 'ANALYZE', 'CHECK'];
639
+
640
+ const priceData = [];
641
+ for (const t of tokens.filter(t => !skipWords.includes(t)).slice(0, 3)) {
642
+ try {
643
+ const { quickPrice } = await import('../utils/helpers.js');
644
+ const p = await quickPrice(t);
645
+ if (p) priceData.push(`${p.symbol}: $${p.price} (24h: ${p.change24h}%)`);
646
+ } catch {}
647
+ }
648
+ if (priceData.length > 0) {
649
+ enriched += `\n\n[Live market data: ${priceData.join(', ')}]`;
650
+ }
651
+
652
+ // Send to LLM
653
+ ws.sendLine(` ${ANSI.dim}Thinking...${ANSI.reset}`);
654
+
655
+ try {
656
+ const result = await engine.chat(enriched);
657
+ const usage = engine.getUsage();
658
+
659
+ // Display response with formatting
660
+ ws.sendLine('');
661
+ ws.sendLine(`${ANSI.gold} ◆ DARKSOL AI${ANSI.reset}`);
662
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
663
+
664
+ const lines = result.content.split('\n');
665
+ for (const line of lines) {
666
+ // Highlight code blocks
667
+ if (line.trim().startsWith('```')) {
668
+ ws.sendLine(` ${ANSI.dim}${line}${ANSI.reset}`);
669
+ } else if (line.trim().startsWith('darksol ') || line.trim().startsWith('$ darksol')) {
670
+ // Highlight CLI commands
671
+ ws.sendLine(` ${ANSI.gold}${line}${ANSI.reset}`);
672
+ } else if (line.trim().startsWith('⚠') || line.trim().startsWith('Warning') || line.trim().toLowerCase().startsWith('risk')) {
673
+ ws.sendLine(` ${ANSI.red}${line}${ANSI.reset}`);
674
+ } else if (line.trim().startsWith('•') || line.trim().startsWith('-') || line.trim().startsWith('*')) {
675
+ ws.sendLine(` ${ANSI.white}${line}${ANSI.reset}`);
676
+ } else {
677
+ ws.sendLine(` ${line}`);
678
+ }
679
+ }
680
+
681
+ ws.sendLine('');
682
+ ws.sendLine(` ${ANSI.dim}[${usage.calls} msgs | ${usage.totalTokens} tokens | ${engine.provider}/${engine.model}]${ANSI.reset}`);
683
+ ws.sendLine('');
684
+
685
+ } catch (err) {
686
+ ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
687
+ ws.sendLine('');
688
+ }
689
+
690
+ return {};
691
+ }
692
+
511
693
  // ══════════════════════════════════════════════════
512
694
  // SEND / RECEIVE (web shell — info only, actual sends require CLI)
513
695
  // ══════════════════════════════════════════════════
package/src/web/server.js CHANGED
@@ -5,6 +5,9 @@ import { fileURLToPath } from 'url';
5
5
  import { dirname, join } from 'path';
6
6
  import open from 'open';
7
7
  import { theme } from '../ui/theme.js';
8
+ import { createRequire } from 'module';
9
+ const require = createRequire(import.meta.url);
10
+ const { version: PKG_VERSION } = require('../../package.json');
8
11
 
9
12
  const __filename = fileURLToPath(import.meta.url);
10
13
  const __dirname = dirname(__filename);
@@ -46,7 +49,7 @@ export async function startWebShell(opts = {}) {
46
49
  res.end(js);
47
50
  } else if (pathname === '/health') {
48
51
  res.writeHead(200, { 'Content-Type': 'application/json' });
49
- res.end(JSON.stringify({ status: 'ok', version: '0.4.0' }));
52
+ res.end(JSON.stringify({ status: 'ok', version: PKG_VERSION }));
50
53
  } else {
51
54
  res.writeHead(404);
52
55
  res.end('Not found');
@@ -194,12 +197,13 @@ function getBanner() {
194
197
  '',
195
198
  `${dim} ╔══════════════════════════════════════════════════════════╗${reset}`,
196
199
  `${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} — ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
197
- `${dim} ║${reset}${dim} v0.4.0 ${reset}${gold}🌑${reset}${dim} ║${reset}`,
200
+ `${dim} ║${reset}${dim} v${PKG_VERSION}${' '.repeat(Math.max(0, 52 - PKG_VERSION.length))}${reset}${gold}🌑${reset}${dim} ║${reset}`,
198
201
  `${dim} ╚══════════════════════════════════════════════════════════╝${reset}`,
199
202
  '',
200
203
  `${dim} All services. One terminal. Zero trust required.${reset}`,
201
204
  '',
202
- `${dim} Type ${gold}help${dim} for commands. Tab to autocomplete.${reset}`,
205
+ `${dim} Type ${gold}ai <question>${dim} to chat with the trading AI.${reset}`,
206
+ `${dim} Type ${gold}help${dim} for all commands.${reset}`,
203
207
  '',
204
208
  ].join('\r\n');
205
209
  }
@@ -211,20 +215,31 @@ function getHelp() {
211
215
  const reset = '\x1b[0m';
212
216
 
213
217
  const cmds = [
218
+ ['', `${gold}AI ASSISTANT${reset}`],
219
+ ['ai <question>', 'Chat with trading AI'],
220
+ ['ai clear', 'Reset chat history'],
221
+ ['ai status', 'Show AI session info'],
222
+ ['', ''],
223
+ ['', `${gold}TRADING & WALLET${reset}`],
214
224
  ['price <token...>', 'Quick price check'],
215
225
  ['watch <token>', 'Live price monitor'],
216
226
  ['gas [chain]', 'Gas prices & estimates'],
217
227
  ['portfolio', 'Multi-chain balances'],
228
+ ['send', 'Send ETH or tokens'],
229
+ ['receive', 'Show address to receive'],
230
+ ['wallet list', 'List wallets'],
231
+ ['wallet balance', 'Wallet balance'],
218
232
  ['history', 'Transaction history'],
233
+ ['', ''],
234
+ ['', `${gold}SERVICES${reset}`],
219
235
  ['market <token>', 'Market intel & data'],
220
236
  ['mail status', 'AgentMail status'],
221
237
  ['mail inbox', 'Check email inbox'],
222
- ['mail send', 'Send an email'],
223
238
  ['oracle roll', 'On-chain random oracle'],
224
239
  ['casino status', 'Casino status'],
225
- ['wallet list', 'List wallets'],
226
- ['wallet balance', 'Wallet balance'],
227
240
  ['config', 'Show configuration'],
241
+ ['', ''],
242
+ ['', `${gold}GENERAL${reset}`],
228
243
  ['banner', 'Show banner'],
229
244
  ['clear', 'Clear screen'],
230
245
  ['help', 'This help message'],
@@ -236,7 +251,13 @@ function getHelp() {
236
251
  out += `${dim} ${'─'.repeat(50)}${reset}\r\n`;
237
252
 
238
253
  for (const [cmd, desc] of cmds) {
239
- out += ` ${green}${cmd.padEnd(22)}${reset}${dim}${desc}${reset}\r\n`;
254
+ if (!cmd && !desc) {
255
+ out += '\r\n';
256
+ } else if (!cmd) {
257
+ out += ` ${desc}\r\n`;
258
+ } else {
259
+ out += ` ${green}${cmd.padEnd(22)}${reset}${dim}${desc}${reset}\r\n`;
260
+ }
240
261
  }
241
262
 
242
263
  out += '\r\n';