@darksol/terminal 0.4.5 → 0.4.7
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 +1 -1
- package/src/web/commands.js +383 -3
- package/src/web/server.js +36 -8
package/package.json
CHANGED
package/src/web/commands.js
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { getConfig, setConfig } from '../config/store.js';
|
|
3
|
-
import { hasKey, getKeyAuto } from '../config/keys.js';
|
|
3
|
+
import { hasKey, hasAnyLLM, getKeyAuto, addKeyDirect, SERVICES } from '../config/keys.js';
|
|
4
4
|
import { ethers } from 'ethers';
|
|
5
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
// ══════════════════════════════════════════════════
|
|
10
|
+
// CHAT LOG PERSISTENCE
|
|
11
|
+
// ══════════════════════════════════════════════════
|
|
12
|
+
const CHAT_LOG_DIR = join(homedir(), '.darksol', 'chat-logs');
|
|
13
|
+
|
|
14
|
+
function ensureChatLogDir() {
|
|
15
|
+
if (!existsSync(CHAT_LOG_DIR)) mkdirSync(CHAT_LOG_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function logChat(role, content) {
|
|
19
|
+
ensureChatLogDir();
|
|
20
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
21
|
+
const time = new Date().toISOString().slice(11, 19);
|
|
22
|
+
const file = join(CHAT_LOG_DIR, `${date}.jsonl`);
|
|
23
|
+
const entry = JSON.stringify({ ts: new Date().toISOString(), time, role, content });
|
|
24
|
+
appendFileSync(file, entry + '\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getChatHistory(limit = 20) {
|
|
28
|
+
ensureChatLogDir();
|
|
29
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
30
|
+
const file = join(CHAT_LOG_DIR, `${date}.jsonl`);
|
|
31
|
+
if (!existsSync(file)) return [];
|
|
32
|
+
const lines = readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean);
|
|
33
|
+
return lines.slice(-limit).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
34
|
+
}
|
|
5
35
|
|
|
6
36
|
// ══════════════════════════════════════════════════
|
|
7
37
|
// WEB SHELL COMMAND HANDLER
|
|
@@ -39,6 +69,38 @@ const USDC_ADDRESSES = {
|
|
|
39
69
|
|
|
40
70
|
const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
41
71
|
|
|
72
|
+
/**
|
|
73
|
+
* AI status check — shown on connection
|
|
74
|
+
*/
|
|
75
|
+
export function getAIStatus() {
|
|
76
|
+
const gold = '\x1b[38;2;255;215;0m';
|
|
77
|
+
const green = '\x1b[38;2;0;255;136m';
|
|
78
|
+
const red = '\x1b[38;2;233;69;96m';
|
|
79
|
+
const dim = '\x1b[38;2;102;102;102m';
|
|
80
|
+
const reset = '\x1b[0m';
|
|
81
|
+
|
|
82
|
+
const providers = ['openai', 'anthropic', 'openrouter', 'ollama'];
|
|
83
|
+
const connected = providers.filter(p => hasKey(p));
|
|
84
|
+
|
|
85
|
+
if (connected.length > 0) {
|
|
86
|
+
const names = connected.map(p => SERVICES[p]?.name || p).join(', ');
|
|
87
|
+
return ` ${green}● AI ready${reset} ${dim}(${names})${reset}\r\n ${dim}Type ${gold}ai <question>${dim} to start chatting. Chat logs saved to ~/.darksol/chat-logs/${reset}\r\n\r\n`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
` ${red}○ AI not configured${reset} ${dim}— no LLM provider connected${reset}`,
|
|
92
|
+
'',
|
|
93
|
+
` ${gold}Quick setup — pick one:${reset}`,
|
|
94
|
+
` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
|
|
95
|
+
` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
|
|
96
|
+
` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
|
|
97
|
+
` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
|
|
98
|
+
'',
|
|
99
|
+
` ${dim}Or run the full setup wizard: ${gold}darksol setup${reset}`,
|
|
100
|
+
'',
|
|
101
|
+
].join('\r\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
42
104
|
/**
|
|
43
105
|
* Handle a command string, return { output } or stream via ws helpers
|
|
44
106
|
*/
|
|
@@ -76,10 +138,26 @@ export async function handleCommand(cmd, ws) {
|
|
|
76
138
|
return await cmdSend(args, ws);
|
|
77
139
|
case 'receive':
|
|
78
140
|
return await cmdReceive(ws);
|
|
79
|
-
|
|
141
|
+
case 'ai':
|
|
142
|
+
case 'ask':
|
|
143
|
+
case 'chat':
|
|
144
|
+
return await cmdAI(args, ws);
|
|
145
|
+
case 'keys':
|
|
146
|
+
case 'llm':
|
|
147
|
+
return await cmdKeys(args, ws);
|
|
148
|
+
case 'logs':
|
|
149
|
+
case 'chatlog':
|
|
150
|
+
return await cmdChatLogs(args, ws);
|
|
151
|
+
default: {
|
|
152
|
+
// Fuzzy: if it looks like natural language, route to AI
|
|
153
|
+
const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca)\b/i;
|
|
154
|
+
if (nlKeywords.test(cmd)) {
|
|
155
|
+
return await cmdAI(cmd.split(/\s+/), ws);
|
|
156
|
+
}
|
|
80
157
|
return {
|
|
81
|
-
output: `\r\n ${ANSI.red}✗ Unknown command: ${cmd}${ANSI.reset}\r\n ${ANSI.dim}Type ${ANSI.gold}help${ANSI.dim} for
|
|
158
|
+
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
159
|
};
|
|
160
|
+
}
|
|
83
161
|
}
|
|
84
162
|
}
|
|
85
163
|
|
|
@@ -508,6 +586,308 @@ async function cmdConfig(ws) {
|
|
|
508
586
|
return {};
|
|
509
587
|
}
|
|
510
588
|
|
|
589
|
+
// ══════════════════════════════════════════════════
|
|
590
|
+
// AI CHAT — LLM-powered assistant in the web shell
|
|
591
|
+
// ══════════════════════════════════════════════════
|
|
592
|
+
|
|
593
|
+
// Persistent chat engine per WebSocket connection
|
|
594
|
+
const chatEngines = new WeakMap();
|
|
595
|
+
|
|
596
|
+
async function cmdAI(args, ws) {
|
|
597
|
+
const input = args.join(' ').trim();
|
|
598
|
+
|
|
599
|
+
if (!input || input === 'help') {
|
|
600
|
+
ws.sendLine(`${ANSI.gold} ◆ AI TRADING ASSISTANT${ANSI.reset}`);
|
|
601
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
602
|
+
ws.sendLine('');
|
|
603
|
+
ws.sendLine(` ${ANSI.white}Natural language trading — just describe what you want.${ANSI.reset}`);
|
|
604
|
+
ws.sendLine('');
|
|
605
|
+
ws.sendLine(` ${ANSI.darkGold}Usage:${ANSI.reset}`);
|
|
606
|
+
ws.sendLine(` ${ANSI.gold}ai swap 0.1 ETH to USDC${ANSI.reset}`);
|
|
607
|
+
ws.sendLine(` ${ANSI.gold}ai what's the price of AERO?${ANSI.reset}`);
|
|
608
|
+
ws.sendLine(` ${ANSI.gold}ai analyze VIRTUAL on base${ANSI.reset}`);
|
|
609
|
+
ws.sendLine(` ${ANSI.gold}ai should I DCA into ETH?${ANSI.reset}`);
|
|
610
|
+
ws.sendLine(` ${ANSI.gold}ai send 10 USDC to 0x1234...${ANSI.reset}`);
|
|
611
|
+
ws.sendLine(` ${ANSI.gold}ai gas on base${ANSI.reset}`);
|
|
612
|
+
ws.sendLine('');
|
|
613
|
+
ws.sendLine(` ${ANSI.dim}Conversation history is kept for the session.${ANSI.reset}`);
|
|
614
|
+
ws.sendLine(` ${ANSI.dim}Type ${ANSI.gold}ai clear${ANSI.dim} to reset history.${ANSI.reset}`);
|
|
615
|
+
ws.sendLine('');
|
|
616
|
+
return {};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (input === 'clear' || input === 'reset') {
|
|
620
|
+
if (chatEngines.has(ws)) {
|
|
621
|
+
chatEngines.get(ws).clearHistory();
|
|
622
|
+
}
|
|
623
|
+
ws.sendLine(` ${ANSI.green}✓ Chat history cleared${ANSI.reset}`);
|
|
624
|
+
ws.sendLine('');
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (input === 'status') {
|
|
629
|
+
const engine = chatEngines.get(ws);
|
|
630
|
+
if (engine) {
|
|
631
|
+
const usage = engine.getUsage();
|
|
632
|
+
ws.sendLine(`${ANSI.gold} ◆ AI STATUS${ANSI.reset}`);
|
|
633
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
634
|
+
ws.sendLine(` ${ANSI.darkGold}Provider${ANSI.reset} ${ANSI.white}${usage.provider}${ANSI.reset}`);
|
|
635
|
+
ws.sendLine(` ${ANSI.darkGold}Model${ANSI.reset} ${ANSI.white}${usage.model}${ANSI.reset}`);
|
|
636
|
+
ws.sendLine(` ${ANSI.darkGold}Messages${ANSI.reset} ${ANSI.white}${usage.calls}${ANSI.reset}`);
|
|
637
|
+
ws.sendLine(` ${ANSI.darkGold}Tokens${ANSI.reset} ${ANSI.white}${usage.totalTokens}${ANSI.reset}`);
|
|
638
|
+
ws.sendLine('');
|
|
639
|
+
} else {
|
|
640
|
+
ws.sendLine(` ${ANSI.dim}No active AI session${ANSI.reset}`);
|
|
641
|
+
ws.sendLine('');
|
|
642
|
+
}
|
|
643
|
+
return {};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Initialize or retrieve the LLM engine
|
|
647
|
+
let engine = chatEngines.get(ws);
|
|
648
|
+
if (!engine) {
|
|
649
|
+
try {
|
|
650
|
+
const { createLLM } = await import('../llm/engine.js');
|
|
651
|
+
engine = await createLLM({});
|
|
652
|
+
|
|
653
|
+
const chain = getConfig('chain') || 'base';
|
|
654
|
+
const wallet = getConfig('activeWallet') || '(not set)';
|
|
655
|
+
const slippage = getConfig('slippage') || 0.5;
|
|
656
|
+
|
|
657
|
+
engine.setSystemPrompt(`You are DARKSOL Terminal's AI trading assistant running in a web terminal.
|
|
658
|
+
|
|
659
|
+
You help users with:
|
|
660
|
+
- Token swaps, sends, and transfers
|
|
661
|
+
- Price checks and market analysis
|
|
662
|
+
- DCA strategy recommendations
|
|
663
|
+
- Gas estimates and chain info
|
|
664
|
+
- Portfolio analysis
|
|
665
|
+
- General crypto/DeFi questions
|
|
666
|
+
|
|
667
|
+
USER CONTEXT:
|
|
668
|
+
- Active chain: ${chain}
|
|
669
|
+
- Active wallet: ${wallet}
|
|
670
|
+
- Slippage: ${slippage}%
|
|
671
|
+
- Supported chains: Base (default), Ethereum, Polygon, Arbitrum, Optimism
|
|
672
|
+
|
|
673
|
+
RULES:
|
|
674
|
+
- Be concise — this is a terminal, not a blog
|
|
675
|
+
- Use short paragraphs, bullet points where helpful
|
|
676
|
+
- Include risk warnings for any trade suggestions
|
|
677
|
+
- Never reveal private keys or sensitive info
|
|
678
|
+
- When suggesting trades, give the exact darksol CLI command
|
|
679
|
+
- If you detect an actionable intent (swap, send, price, etc), include the command at the end
|
|
680
|
+
|
|
681
|
+
COMMAND REFERENCE:
|
|
682
|
+
- darksol trade swap -i ETH -o USDC -a 0.1
|
|
683
|
+
- darksol send --to 0x... --amount 0.1 --token ETH
|
|
684
|
+
- darksol price ETH AERO VIRTUAL
|
|
685
|
+
- darksol gas base
|
|
686
|
+
- darksol wallet balance
|
|
687
|
+
- darksol portfolio
|
|
688
|
+
- darksol dca create -t ETH -a 0.01 -i 1h -n 24
|
|
689
|
+
- darksol ai analyze <token>`);
|
|
690
|
+
|
|
691
|
+
chatEngines.set(ws, engine);
|
|
692
|
+
ws.sendLine(` ${ANSI.green}● AI connected${ANSI.reset} ${ANSI.dim}(${engine.provider}/${engine.model})${ANSI.reset}`);
|
|
693
|
+
ws.sendLine('');
|
|
694
|
+
} catch (err) {
|
|
695
|
+
ws.sendLine(` ${ANSI.red}✗ AI initialization failed: ${err.message}${ANSI.reset}`);
|
|
696
|
+
ws.sendLine(` ${ANSI.dim}Configure an API key: darksol keys add openai${ANSI.reset}`);
|
|
697
|
+
ws.sendLine('');
|
|
698
|
+
return {};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Enrich with live price data
|
|
703
|
+
let enriched = input;
|
|
704
|
+
const tokenPattern = /\b([A-Z]{2,10})\b/g;
|
|
705
|
+
const tokens = [...new Set(input.toUpperCase().match(tokenPattern) || [])];
|
|
706
|
+
const skipWords = ['ETH', 'THE', 'FOR', 'AND', 'BUY', 'SELL', 'DCA', 'SWAP', 'WHAT', 'PRICE', 'HOW', 'MUCH', 'SEND', 'SHOULD', 'CAN', 'ANALYZE', 'CHECK'];
|
|
707
|
+
|
|
708
|
+
const priceData = [];
|
|
709
|
+
for (const t of tokens.filter(t => !skipWords.includes(t)).slice(0, 3)) {
|
|
710
|
+
try {
|
|
711
|
+
const { quickPrice } = await import('../utils/helpers.js');
|
|
712
|
+
const p = await quickPrice(t);
|
|
713
|
+
if (p) priceData.push(`${p.symbol}: $${p.price} (24h: ${p.change24h}%)`);
|
|
714
|
+
} catch {}
|
|
715
|
+
}
|
|
716
|
+
if (priceData.length > 0) {
|
|
717
|
+
enriched += `\n\n[Live market data: ${priceData.join(', ')}]`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Log user message
|
|
721
|
+
logChat('user', input);
|
|
722
|
+
|
|
723
|
+
// Send to LLM
|
|
724
|
+
ws.sendLine(` ${ANSI.dim}Thinking...${ANSI.reset}`);
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
const result = await engine.chat(enriched);
|
|
728
|
+
const usage = engine.getUsage();
|
|
729
|
+
|
|
730
|
+
// Log AI response
|
|
731
|
+
logChat('assistant', result.content);
|
|
732
|
+
|
|
733
|
+
// Display response with formatting
|
|
734
|
+
ws.sendLine('');
|
|
735
|
+
ws.sendLine(`${ANSI.gold} ◆ DARKSOL AI${ANSI.reset}`);
|
|
736
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
737
|
+
|
|
738
|
+
const lines = result.content.split('\n');
|
|
739
|
+
for (const line of lines) {
|
|
740
|
+
// Highlight code blocks
|
|
741
|
+
if (line.trim().startsWith('```')) {
|
|
742
|
+
ws.sendLine(` ${ANSI.dim}${line}${ANSI.reset}`);
|
|
743
|
+
} else if (line.trim().startsWith('darksol ') || line.trim().startsWith('$ darksol')) {
|
|
744
|
+
// Highlight CLI commands
|
|
745
|
+
ws.sendLine(` ${ANSI.gold}${line}${ANSI.reset}`);
|
|
746
|
+
} else if (line.trim().startsWith('⚠') || line.trim().startsWith('Warning') || line.trim().toLowerCase().startsWith('risk')) {
|
|
747
|
+
ws.sendLine(` ${ANSI.red}${line}${ANSI.reset}`);
|
|
748
|
+
} else if (line.trim().startsWith('•') || line.trim().startsWith('-') || line.trim().startsWith('*')) {
|
|
749
|
+
ws.sendLine(` ${ANSI.white}${line}${ANSI.reset}`);
|
|
750
|
+
} else {
|
|
751
|
+
ws.sendLine(` ${line}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
ws.sendLine('');
|
|
756
|
+
ws.sendLine(` ${ANSI.dim}[${usage.calls} msgs | ${usage.totalTokens} tokens | ${engine.provider}/${engine.model}]${ANSI.reset}`);
|
|
757
|
+
ws.sendLine('');
|
|
758
|
+
|
|
759
|
+
} catch (err) {
|
|
760
|
+
ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
|
|
761
|
+
ws.sendLine('');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return {};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ══════════════════════════════════════════════════
|
|
768
|
+
// KEYS — LLM provider configuration from web shell
|
|
769
|
+
// ══════════════════════════════════════════════════
|
|
770
|
+
async function cmdKeys(args, ws) {
|
|
771
|
+
const sub = args[0]?.toLowerCase();
|
|
772
|
+
|
|
773
|
+
if (sub === 'add' && args[1]) {
|
|
774
|
+
const service = args[1].toLowerCase();
|
|
775
|
+
const key = args[2];
|
|
776
|
+
const svc = SERVICES[service];
|
|
777
|
+
|
|
778
|
+
if (!svc) {
|
|
779
|
+
ws.sendLine(` ${ANSI.red}✗ Unknown service: ${service}${ANSI.reset}`);
|
|
780
|
+
ws.sendLine(` ${ANSI.dim}Available: openai, anthropic, openrouter, ollama${ANSI.reset}`);
|
|
781
|
+
ws.sendLine('');
|
|
782
|
+
return {};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!key) {
|
|
786
|
+
ws.sendLine(` ${ANSI.red}✗ No key provided${ANSI.reset}`);
|
|
787
|
+
ws.sendLine(` ${ANSI.dim}Usage: keys add ${service} <your-api-key>${ANSI.reset}`);
|
|
788
|
+
ws.sendLine(` ${ANSI.dim}Get a key: ${svc.docsUrl}${ANSI.reset}`);
|
|
789
|
+
ws.sendLine('');
|
|
790
|
+
return {};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (svc.validate && !svc.validate(key)) {
|
|
794
|
+
ws.sendLine(` ${ANSI.red}✗ Invalid key format for ${svc.name}${ANSI.reset}`);
|
|
795
|
+
ws.sendLine('');
|
|
796
|
+
return {};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
addKeyDirect(service, key);
|
|
801
|
+
ws.sendLine(` ${ANSI.green}✓ ${svc.name} key stored securely${ANSI.reset}`);
|
|
802
|
+
|
|
803
|
+
// Clear cached engine so it picks up new key
|
|
804
|
+
chatEngines.delete(ws);
|
|
805
|
+
ws.sendLine(` ${ANSI.dim}AI session refreshed — type ${ANSI.gold}ai <question>${ANSI.dim} to chat${ANSI.reset}`);
|
|
806
|
+
ws.sendLine('');
|
|
807
|
+
} catch (err) {
|
|
808
|
+
ws.sendLine(` ${ANSI.red}✗ Failed to store key: ${err.message}${ANSI.reset}`);
|
|
809
|
+
ws.sendLine('');
|
|
810
|
+
}
|
|
811
|
+
return {};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (sub === 'remove' && args[1]) {
|
|
815
|
+
const service = args[1].toLowerCase();
|
|
816
|
+
// Can't remove via web shell without password prompt — point to CLI
|
|
817
|
+
ws.sendLine(` ${ANSI.dim}To remove keys, use the CLI:${ANSI.reset}`);
|
|
818
|
+
ws.sendLine(` ${ANSI.gold}darksol keys remove ${service}${ANSI.reset}`);
|
|
819
|
+
ws.sendLine('');
|
|
820
|
+
return {};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Default: show status
|
|
824
|
+
ws.sendLine(`${ANSI.gold} ◆ API KEYS / LLM CONFIG${ANSI.reset}`);
|
|
825
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
826
|
+
ws.sendLine('');
|
|
827
|
+
|
|
828
|
+
const llmProviders = ['openai', 'anthropic', 'openrouter', 'ollama'];
|
|
829
|
+
ws.sendLine(` ${ANSI.gold}LLM Providers:${ANSI.reset}`);
|
|
830
|
+
for (const p of llmProviders) {
|
|
831
|
+
const svc = SERVICES[p];
|
|
832
|
+
const has = hasKey(p);
|
|
833
|
+
const status = has ? `${ANSI.green}● Connected${ANSI.reset}` : `${ANSI.dim}○ Not set${ANSI.reset}`;
|
|
834
|
+
ws.sendLine(` ${status} ${ANSI.white}${svc.name.padEnd(20)}${ANSI.reset}${ANSI.dim}${svc.description}${ANSI.reset}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
ws.sendLine('');
|
|
838
|
+
ws.sendLine(` ${ANSI.gold}Quick Setup:${ANSI.reset}`);
|
|
839
|
+
ws.sendLine(` ${ANSI.green}keys add openai sk-...${ANSI.reset} ${ANSI.dim}Add OpenAI key${ANSI.reset}`);
|
|
840
|
+
ws.sendLine(` ${ANSI.green}keys add anthropic sk-ant-...${ANSI.reset} ${ANSI.dim}Add Anthropic key${ANSI.reset}`);
|
|
841
|
+
ws.sendLine(` ${ANSI.green}keys add openrouter sk-or-...${ANSI.reset} ${ANSI.dim}Add OpenRouter key${ANSI.reset}`);
|
|
842
|
+
ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
|
|
843
|
+
ws.sendLine('');
|
|
844
|
+
|
|
845
|
+
const dataProviders = ['coingecko', 'dexscreener', 'alchemy', 'agentmail'];
|
|
846
|
+
ws.sendLine(` ${ANSI.gold}Other Services:${ANSI.reset}`);
|
|
847
|
+
for (const p of dataProviders) {
|
|
848
|
+
const svc = SERVICES[p];
|
|
849
|
+
if (!svc) continue;
|
|
850
|
+
const has = hasKey(p);
|
|
851
|
+
const status = has ? `${ANSI.green}●${ANSI.reset}` : `${ANSI.dim}○${ANSI.reset}`;
|
|
852
|
+
ws.sendLine(` ${status} ${ANSI.white}${svc.name.padEnd(20)}${ANSI.reset}${ANSI.dim}${svc.description}${ANSI.reset}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
ws.sendLine('');
|
|
856
|
+
ws.sendLine(` ${ANSI.dim}Keys are AES-256-GCM encrypted at ~/.darksol/keys/vault.json${ANSI.reset}`);
|
|
857
|
+
ws.sendLine('');
|
|
858
|
+
return {};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ══════════════════════════════════════════════════
|
|
862
|
+
// CHAT LOGS — View conversation history
|
|
863
|
+
// ══════════════════════════════════════════════════
|
|
864
|
+
async function cmdChatLogs(args, ws) {
|
|
865
|
+
const limit = parseInt(args[0]) || 20;
|
|
866
|
+
const history = getChatHistory(limit);
|
|
867
|
+
|
|
868
|
+
ws.sendLine(`${ANSI.gold} ◆ CHAT LOG${ANSI.reset}`);
|
|
869
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
870
|
+
ws.sendLine('');
|
|
871
|
+
|
|
872
|
+
if (history.length === 0) {
|
|
873
|
+
ws.sendLine(` ${ANSI.dim}No chat history today. Start with: ai <question>${ANSI.reset}`);
|
|
874
|
+
ws.sendLine('');
|
|
875
|
+
return {};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
for (const entry of history) {
|
|
879
|
+
const time = entry.time || '';
|
|
880
|
+
const role = entry.role === 'user' ? `${ANSI.gold}You${ANSI.reset}` : `${ANSI.green}AI${ANSI.reset}`;
|
|
881
|
+
const preview = entry.content.split('\n')[0].slice(0, 80);
|
|
882
|
+
ws.sendLine(` ${ANSI.dim}${time}${ANSI.reset} ${role}: ${preview}${entry.content.length > 80 ? ANSI.dim + '...' + ANSI.reset : ''}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
ws.sendLine('');
|
|
886
|
+
ws.sendLine(` ${ANSI.dim}Logs: ~/.darksol/chat-logs/ (${history.length} messages shown)${ANSI.reset}`);
|
|
887
|
+
ws.sendLine('');
|
|
888
|
+
return {};
|
|
889
|
+
}
|
|
890
|
+
|
|
511
891
|
// ══════════════════════════════════════════════════
|
|
512
892
|
// SEND / RECEIVE (web shell — info only, actual sends require CLI)
|
|
513
893
|
// ══════════════════════════════════════════════════
|
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);
|
|
@@ -17,7 +20,7 @@ const __dirname = dirname(__filename);
|
|
|
17
20
|
* Command handler registry — maps command strings to async functions
|
|
18
21
|
* that return { output: string } with ANSI stripped for web
|
|
19
22
|
*/
|
|
20
|
-
import { handleCommand } from './commands.js';
|
|
23
|
+
import { handleCommand, getAIStatus } from './commands.js';
|
|
21
24
|
|
|
22
25
|
export async function startWebShell(opts = {}) {
|
|
23
26
|
process.on('uncaughtException', (err) => {
|
|
@@ -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:
|
|
52
|
+
res.end(JSON.stringify({ status: 'ok', version: PKG_VERSION }));
|
|
50
53
|
} else {
|
|
51
54
|
res.writeHead(404);
|
|
52
55
|
res.end('Not found');
|
|
@@ -86,6 +89,13 @@ export async function startWebShell(opts = {}) {
|
|
|
86
89
|
data: getBanner(),
|
|
87
90
|
}));
|
|
88
91
|
|
|
92
|
+
// AI connection check right after banner
|
|
93
|
+
const aiStatus = getAIStatus();
|
|
94
|
+
ws.send(JSON.stringify({
|
|
95
|
+
type: 'output',
|
|
96
|
+
data: aiStatus,
|
|
97
|
+
}));
|
|
98
|
+
|
|
89
99
|
ws.on('message', async (raw) => {
|
|
90
100
|
try {
|
|
91
101
|
const msg = JSON.parse(raw.toString());
|
|
@@ -194,12 +204,13 @@ function getBanner() {
|
|
|
194
204
|
'',
|
|
195
205
|
`${dim} ╔══════════════════════════════════════════════════════════╗${reset}`,
|
|
196
206
|
`${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} — ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
|
|
197
|
-
`${dim} ║${reset}${dim}
|
|
207
|
+
`${dim} ║${reset}${dim} v${PKG_VERSION}${' '.repeat(Math.max(0, 52 - PKG_VERSION.length))}${reset}${gold}🌑${reset}${dim} ║${reset}`,
|
|
198
208
|
`${dim} ╚══════════════════════════════════════════════════════════╝${reset}`,
|
|
199
209
|
'',
|
|
200
210
|
`${dim} All services. One terminal. Zero trust required.${reset}`,
|
|
201
211
|
'',
|
|
202
|
-
`${dim} Type ${gold}
|
|
212
|
+
`${dim} Type ${gold}ai <question>${dim} to chat with the trading AI.${reset}`,
|
|
213
|
+
`${dim} Type ${gold}help${dim} for all commands.${reset}`,
|
|
203
214
|
'',
|
|
204
215
|
].join('\r\n');
|
|
205
216
|
}
|
|
@@ -211,20 +222,31 @@ function getHelp() {
|
|
|
211
222
|
const reset = '\x1b[0m';
|
|
212
223
|
|
|
213
224
|
const cmds = [
|
|
225
|
+
['', `${gold}AI ASSISTANT${reset}`],
|
|
226
|
+
['ai <question>', 'Chat with trading AI'],
|
|
227
|
+
['ai clear', 'Reset chat history'],
|
|
228
|
+
['ai status', 'Show AI session info'],
|
|
229
|
+
['', ''],
|
|
230
|
+
['', `${gold}TRADING & WALLET${reset}`],
|
|
214
231
|
['price <token...>', 'Quick price check'],
|
|
215
232
|
['watch <token>', 'Live price monitor'],
|
|
216
233
|
['gas [chain]', 'Gas prices & estimates'],
|
|
217
234
|
['portfolio', 'Multi-chain balances'],
|
|
235
|
+
['send', 'Send ETH or tokens'],
|
|
236
|
+
['receive', 'Show address to receive'],
|
|
237
|
+
['wallet list', 'List wallets'],
|
|
238
|
+
['wallet balance', 'Wallet balance'],
|
|
218
239
|
['history', 'Transaction history'],
|
|
240
|
+
['', ''],
|
|
241
|
+
['', `${gold}SERVICES${reset}`],
|
|
219
242
|
['market <token>', 'Market intel & data'],
|
|
220
243
|
['mail status', 'AgentMail status'],
|
|
221
244
|
['mail inbox', 'Check email inbox'],
|
|
222
|
-
['mail send', 'Send an email'],
|
|
223
245
|
['oracle roll', 'On-chain random oracle'],
|
|
224
246
|
['casino status', 'Casino status'],
|
|
225
|
-
['wallet list', 'List wallets'],
|
|
226
|
-
['wallet balance', 'Wallet balance'],
|
|
227
247
|
['config', 'Show configuration'],
|
|
248
|
+
['', ''],
|
|
249
|
+
['', `${gold}GENERAL${reset}`],
|
|
228
250
|
['banner', 'Show banner'],
|
|
229
251
|
['clear', 'Clear screen'],
|
|
230
252
|
['help', 'This help message'],
|
|
@@ -236,7 +258,13 @@ function getHelp() {
|
|
|
236
258
|
out += `${dim} ${'─'.repeat(50)}${reset}\r\n`;
|
|
237
259
|
|
|
238
260
|
for (const [cmd, desc] of cmds) {
|
|
239
|
-
|
|
261
|
+
if (!cmd && !desc) {
|
|
262
|
+
out += '\r\n';
|
|
263
|
+
} else if (!cmd) {
|
|
264
|
+
out += ` ${desc}\r\n`;
|
|
265
|
+
} else {
|
|
266
|
+
out += ` ${green}${cmd.padEnd(22)}${reset}${dim}${desc}${reset}\r\n`;
|
|
267
|
+
}
|
|
240
268
|
}
|
|
241
269
|
|
|
242
270
|
out += '\r\n';
|