@darksol/terminal 0.4.6 → 0.4.8
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 +399 -29
- package/src/web/server.js +30 -1
- package/src/web/terminal.js +121 -41
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,128 @@ const USDC_ADDRESSES = {
|
|
|
39
69
|
|
|
40
70
|
const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
41
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Handle interactive menu selections from the client
|
|
74
|
+
*/
|
|
75
|
+
export async function handleMenuSelect(id, value, item, ws) {
|
|
76
|
+
switch (id) {
|
|
77
|
+
case 'wallet_select':
|
|
78
|
+
return await showWalletDetail(value, ws);
|
|
79
|
+
|
|
80
|
+
case 'wallet_action':
|
|
81
|
+
switch (value) {
|
|
82
|
+
case 'receive': {
|
|
83
|
+
const name = getConfig('activeWallet');
|
|
84
|
+
if (!name) return {};
|
|
85
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
86
|
+
const w = loadWallet(name);
|
|
87
|
+
ws.sendLine('');
|
|
88
|
+
ws.sendLine(`${ANSI.gold} ◆ RECEIVE — ${name}${ANSI.reset}`);
|
|
89
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
90
|
+
ws.sendLine('');
|
|
91
|
+
ws.sendLine(` ${ANSI.white}Your address:${ANSI.reset}`);
|
|
92
|
+
ws.sendLine('');
|
|
93
|
+
const addr = w.address;
|
|
94
|
+
ws.sendLine(` ${ANSI.dim}┌${'─'.repeat(addr.length + 4)}┐${ANSI.reset}`);
|
|
95
|
+
ws.sendLine(` ${ANSI.dim}│ ${ANSI.gold}${addr}${ANSI.dim} │${ANSI.reset}`);
|
|
96
|
+
ws.sendLine(` ${ANSI.dim}└${'─'.repeat(addr.length + 4)}┘${ANSI.reset}`);
|
|
97
|
+
ws.sendLine('');
|
|
98
|
+
ws.sendLine(` ${ANSI.dim}Works on ALL EVM chains: Base • Ethereum • Arbitrum • Optimism • Polygon${ANSI.reset}`);
|
|
99
|
+
ws.sendLine(` ${ANSI.red}Make sure the sender is on the same chain!${ANSI.reset}`);
|
|
100
|
+
ws.sendLine('');
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
case 'send':
|
|
104
|
+
ws.sendLine('');
|
|
105
|
+
ws.sendLine(` ${ANSI.gold}◆ SEND${ANSI.reset}`);
|
|
106
|
+
ws.sendLine(` ${ANSI.dim}Sending requires wallet password — use the CLI:${ANSI.reset}`);
|
|
107
|
+
ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
|
|
108
|
+
ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode)${ANSI.reset}`);
|
|
109
|
+
ws.sendLine('');
|
|
110
|
+
return {};
|
|
111
|
+
case 'portfolio':
|
|
112
|
+
return await handleCommand('portfolio', ws);
|
|
113
|
+
case 'history':
|
|
114
|
+
return await handleCommand('history', ws);
|
|
115
|
+
case 'switch': {
|
|
116
|
+
const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
117
|
+
const current = getConfig('chain') || 'base';
|
|
118
|
+
ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
|
|
119
|
+
value: c,
|
|
120
|
+
label: c === current ? `★ ${c}` : c,
|
|
121
|
+
desc: c === current ? 'current' : '',
|
|
122
|
+
})));
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
case 'back':
|
|
126
|
+
ws.sendLine('');
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'chain_select':
|
|
132
|
+
setConfig('chain', value);
|
|
133
|
+
ws.sendLine('');
|
|
134
|
+
ws.sendLine(` ${ANSI.green}✓ Chain set to ${value}${ANSI.reset}`);
|
|
135
|
+
ws.sendLine('');
|
|
136
|
+
return {};
|
|
137
|
+
|
|
138
|
+
case 'config_action':
|
|
139
|
+
if (value === 'chain') {
|
|
140
|
+
const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
141
|
+
const current = getConfig('chain') || 'base';
|
|
142
|
+
ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
|
|
143
|
+
value: c,
|
|
144
|
+
label: c === current ? `★ ${c}` : c,
|
|
145
|
+
desc: c === current ? 'current' : '',
|
|
146
|
+
})));
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
if (value === 'keys') {
|
|
150
|
+
return await handleCommand('keys', ws);
|
|
151
|
+
}
|
|
152
|
+
ws.sendLine('');
|
|
153
|
+
return {};
|
|
154
|
+
|
|
155
|
+
case 'main_menu':
|
|
156
|
+
return await handleCommand(value, ws);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* AI status check — shown on connection
|
|
164
|
+
*/
|
|
165
|
+
export function getAIStatus() {
|
|
166
|
+
const gold = '\x1b[38;2;255;215;0m';
|
|
167
|
+
const green = '\x1b[38;2;0;255;136m';
|
|
168
|
+
const red = '\x1b[38;2;233;69;96m';
|
|
169
|
+
const dim = '\x1b[38;2;102;102;102m';
|
|
170
|
+
const reset = '\x1b[0m';
|
|
171
|
+
|
|
172
|
+
const providers = ['openai', 'anthropic', 'openrouter', 'ollama'];
|
|
173
|
+
const connected = providers.filter(p => hasKey(p));
|
|
174
|
+
|
|
175
|
+
if (connected.length > 0) {
|
|
176
|
+
const names = connected.map(p => SERVICES[p]?.name || p).join(', ');
|
|
177
|
+
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`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return [
|
|
181
|
+
` ${red}○ AI not configured${reset} ${dim}— no LLM provider connected${reset}`,
|
|
182
|
+
'',
|
|
183
|
+
` ${gold}Quick setup — pick one:${reset}`,
|
|
184
|
+
` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
|
|
185
|
+
` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
|
|
186
|
+
` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
|
|
187
|
+
` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
|
|
188
|
+
'',
|
|
189
|
+
` ${dim}Or run the full setup wizard: ${gold}darksol setup${reset}`,
|
|
190
|
+
'',
|
|
191
|
+
].join('\r\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
42
194
|
/**
|
|
43
195
|
* Handle a command string, return { output } or stream via ws helpers
|
|
44
196
|
*/
|
|
@@ -80,6 +232,12 @@ export async function handleCommand(cmd, ws) {
|
|
|
80
232
|
case 'ask':
|
|
81
233
|
case 'chat':
|
|
82
234
|
return await cmdAI(args, ws);
|
|
235
|
+
case 'keys':
|
|
236
|
+
case 'llm':
|
|
237
|
+
return await cmdKeys(args, ws);
|
|
238
|
+
case 'logs':
|
|
239
|
+
case 'chatlog':
|
|
240
|
+
return await cmdChatLogs(args, ws);
|
|
83
241
|
default: {
|
|
84
242
|
// Fuzzy: if it looks like natural language, route to AI
|
|
85
243
|
const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca)\b/i;
|
|
@@ -334,47 +492,121 @@ async function cmdMarket(args, ws) {
|
|
|
334
492
|
// WALLET
|
|
335
493
|
// ══════════════════════════════════════════════════
|
|
336
494
|
async function cmdWallet(args, ws) {
|
|
337
|
-
const sub = args[0]
|
|
495
|
+
const sub = args[0];
|
|
496
|
+
|
|
497
|
+
// If a specific subcommand, handle directly
|
|
498
|
+
if (sub === 'balance') {
|
|
499
|
+
return await showWalletDetail(args[1] || getConfig('activeWallet'), ws);
|
|
500
|
+
}
|
|
338
501
|
|
|
339
|
-
if (sub === '
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
502
|
+
if (sub === 'use' && args[1]) {
|
|
503
|
+
setConfig('activeWallet', args[1]);
|
|
504
|
+
ws.sendLine(` ${ANSI.green}✓ Active wallet set to "${args[1]}"${ANSI.reset}`);
|
|
505
|
+
ws.sendLine('');
|
|
506
|
+
return {};
|
|
507
|
+
}
|
|
343
508
|
|
|
509
|
+
// Default: interactive wallet picker
|
|
510
|
+
const { listWallets } = await import('../wallet/keystore.js');
|
|
511
|
+
const wallets = listWallets();
|
|
512
|
+
const active = getConfig('activeWallet');
|
|
513
|
+
|
|
514
|
+
if (wallets.length === 0) {
|
|
344
515
|
ws.sendLine(`${ANSI.gold} ◆ WALLETS${ANSI.reset}`);
|
|
345
516
|
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
ws.sendLine(` ${ANSI.dim}No wallets. Create one in the CLI: darksol wallet create${ANSI.reset}`);
|
|
349
|
-
} else {
|
|
350
|
-
for (const w of wallets) {
|
|
351
|
-
const indicator = w === active ? `${ANSI.gold}► ${ANSI.reset}` : ' ';
|
|
352
|
-
ws.sendLine(` ${indicator}${ANSI.white}${w}${ANSI.reset}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
517
|
+
ws.sendLine(` ${ANSI.dim}No wallets found.${ANSI.reset}`);
|
|
518
|
+
ws.sendLine(` ${ANSI.dim}Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
|
|
356
519
|
ws.sendLine('');
|
|
357
520
|
return {};
|
|
358
521
|
}
|
|
359
522
|
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
523
|
+
if (wallets.length === 1) {
|
|
524
|
+
// Only one wallet — go straight to it
|
|
525
|
+
return await showWalletDetail(wallets[0].name, ws);
|
|
526
|
+
}
|
|
363
527
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
528
|
+
// Multiple wallets — show interactive menu
|
|
529
|
+
const menuItems = wallets.map(w => ({
|
|
530
|
+
value: w.name,
|
|
531
|
+
label: `${w.name === active ? '★ ' : ''}${w.name}`,
|
|
532
|
+
desc: `${w.address.slice(0, 6)}...${w.address.slice(-4)}`,
|
|
533
|
+
}));
|
|
534
|
+
|
|
535
|
+
ws.sendMenu('wallet_select', '◆ Select Wallet', menuItems);
|
|
536
|
+
return {};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function showWalletDetail(name, ws) {
|
|
540
|
+
if (!name) {
|
|
541
|
+
ws.sendLine(` ${ANSI.red}No wallet selected${ANSI.reset}`);
|
|
542
|
+
ws.sendLine('');
|
|
543
|
+
return {};
|
|
544
|
+
}
|
|
369
545
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
546
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
547
|
+
let walletData;
|
|
548
|
+
try {
|
|
549
|
+
walletData = loadWallet(name);
|
|
550
|
+
} catch {
|
|
551
|
+
ws.sendLine(` ${ANSI.red}Wallet "${name}" not found${ANSI.reset}`);
|
|
373
552
|
ws.sendLine('');
|
|
374
553
|
return {};
|
|
375
554
|
}
|
|
376
555
|
|
|
377
|
-
|
|
556
|
+
const chain = getConfig('chain') || 'base';
|
|
557
|
+
const provider = new ethers.JsonRpcProvider(RPCS[chain]);
|
|
558
|
+
|
|
559
|
+
ws.sendLine(`${ANSI.gold} ◆ WALLET — ${name}${ANSI.reset}`);
|
|
560
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
561
|
+
ws.sendLine(` ${ANSI.darkGold}Address${ANSI.reset} ${ANSI.white}${walletData.address}${ANSI.reset}`);
|
|
562
|
+
ws.sendLine(` ${ANSI.darkGold}Chain${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
|
|
563
|
+
ws.sendLine(` ${ANSI.darkGold}Created${ANSI.reset} ${ANSI.dim}${walletData.createdAt ? new Date(walletData.createdAt).toLocaleDateString() : 'unknown'}${ANSI.reset}`);
|
|
564
|
+
ws.sendLine(` ${ANSI.darkGold}Encryption${ANSI.reset} ${ANSI.dim}AES-256-GCM + scrypt${ANSI.reset}`);
|
|
565
|
+
|
|
566
|
+
// Fetch balance
|
|
567
|
+
ws.sendLine('');
|
|
568
|
+
ws.sendLine(` ${ANSI.dim}Fetching balance...${ANSI.reset}`);
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const balance = await provider.getBalance(walletData.address);
|
|
572
|
+
const ethBal = parseFloat(ethers.formatEther(balance));
|
|
573
|
+
|
|
574
|
+
// Also check USDC
|
|
575
|
+
const usdcAddr = USDC_ADDRESSES[chain];
|
|
576
|
+
let usdcBal = 0;
|
|
577
|
+
if (usdcAddr) {
|
|
578
|
+
try {
|
|
579
|
+
const usdc = new ethers.Contract(usdcAddr, ['function balanceOf(address) view returns (uint256)'], provider);
|
|
580
|
+
const raw = await usdc.balanceOf(walletData.address);
|
|
581
|
+
usdcBal = parseFloat(ethers.formatUnits(raw, 6));
|
|
582
|
+
} catch {}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Get ETH price for USD value
|
|
586
|
+
const ethPrice = await getEthPrice();
|
|
587
|
+
const usdValue = (ethBal * ethPrice) + usdcBal;
|
|
588
|
+
|
|
589
|
+
// Overwrite "Fetching..." line
|
|
590
|
+
ws.sendLine(`\x1b[1A\x1b[2K ${ANSI.darkGold}ETH${ANSI.reset} ${ANSI.white}${ethBal.toFixed(6)}${ANSI.reset} ${ANSI.dim}($${(ethBal * ethPrice).toFixed(2)})${ANSI.reset}`);
|
|
591
|
+
ws.sendLine(` ${ANSI.darkGold}USDC${ANSI.reset} ${ANSI.white}$${usdcBal.toFixed(2)}${ANSI.reset}`);
|
|
592
|
+
ws.sendLine(` ${ANSI.darkGold}Total${ANSI.reset} ${ANSI.green}$${usdValue.toFixed(2)}${ANSI.reset}`);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
ws.sendLine(` ${ANSI.red}Balance fetch failed: ${err.message}${ANSI.reset}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
ws.sendLine('');
|
|
598
|
+
|
|
599
|
+
// Show action menu
|
|
600
|
+
ws.sendMenu('wallet_action', '◆ What would you like to do?', [
|
|
601
|
+
{ value: 'receive', label: '📥 Receive', desc: 'Show address to receive funds' },
|
|
602
|
+
{ value: 'send', label: '📤 Send', desc: 'Send ETH or tokens (CLI required)' },
|
|
603
|
+
{ value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balance view' },
|
|
604
|
+
{ value: 'history', label: '📜 History', desc: 'Transaction history' },
|
|
605
|
+
{ value: 'switch', label: '🔄 Switch chain', desc: `Currently: ${chain}` },
|
|
606
|
+
{ value: 'back', label: '← Back', desc: '' },
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
return {};
|
|
378
610
|
}
|
|
379
611
|
|
|
380
612
|
// ══════════════════════════════════════════════════
|
|
@@ -513,8 +745,16 @@ async function cmdConfig(ws) {
|
|
|
513
745
|
ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset}`);
|
|
514
746
|
ws.sendLine(` ${ANSI.darkGold}Slippage${ANSI.reset} ${ANSI.white}${slippage}%${ANSI.reset}`);
|
|
515
747
|
ws.sendLine(` ${ANSI.darkGold}Mail${ANSI.reset} ${ANSI.white}${email}${ANSI.reset}`);
|
|
516
|
-
ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${
|
|
748
|
+
ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasAnyLLM() ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
|
|
517
749
|
ws.sendLine('');
|
|
750
|
+
|
|
751
|
+
// Offer interactive config
|
|
752
|
+
ws.sendMenu('config_action', '◆ Configure', [
|
|
753
|
+
{ value: 'chain', label: '🔗 Change chain', desc: `Currently: ${chain}` },
|
|
754
|
+
{ value: 'keys', label: '🔑 LLM / API keys', desc: '' },
|
|
755
|
+
{ value: 'back', label: '← Back', desc: '' },
|
|
756
|
+
]);
|
|
757
|
+
|
|
518
758
|
return {};
|
|
519
759
|
}
|
|
520
760
|
|
|
@@ -649,6 +889,9 @@ COMMAND REFERENCE:
|
|
|
649
889
|
enriched += `\n\n[Live market data: ${priceData.join(', ')}]`;
|
|
650
890
|
}
|
|
651
891
|
|
|
892
|
+
// Log user message
|
|
893
|
+
logChat('user', input);
|
|
894
|
+
|
|
652
895
|
// Send to LLM
|
|
653
896
|
ws.sendLine(` ${ANSI.dim}Thinking...${ANSI.reset}`);
|
|
654
897
|
|
|
@@ -656,6 +899,9 @@ COMMAND REFERENCE:
|
|
|
656
899
|
const result = await engine.chat(enriched);
|
|
657
900
|
const usage = engine.getUsage();
|
|
658
901
|
|
|
902
|
+
// Log AI response
|
|
903
|
+
logChat('assistant', result.content);
|
|
904
|
+
|
|
659
905
|
// Display response with formatting
|
|
660
906
|
ws.sendLine('');
|
|
661
907
|
ws.sendLine(`${ANSI.gold} ◆ DARKSOL AI${ANSI.reset}`);
|
|
@@ -690,6 +936,130 @@ COMMAND REFERENCE:
|
|
|
690
936
|
return {};
|
|
691
937
|
}
|
|
692
938
|
|
|
939
|
+
// ══════════════════════════════════════════════════
|
|
940
|
+
// KEYS — LLM provider configuration from web shell
|
|
941
|
+
// ══════════════════════════════════════════════════
|
|
942
|
+
async function cmdKeys(args, ws) {
|
|
943
|
+
const sub = args[0]?.toLowerCase();
|
|
944
|
+
|
|
945
|
+
if (sub === 'add' && args[1]) {
|
|
946
|
+
const service = args[1].toLowerCase();
|
|
947
|
+
const key = args[2];
|
|
948
|
+
const svc = SERVICES[service];
|
|
949
|
+
|
|
950
|
+
if (!svc) {
|
|
951
|
+
ws.sendLine(` ${ANSI.red}✗ Unknown service: ${service}${ANSI.reset}`);
|
|
952
|
+
ws.sendLine(` ${ANSI.dim}Available: openai, anthropic, openrouter, ollama${ANSI.reset}`);
|
|
953
|
+
ws.sendLine('');
|
|
954
|
+
return {};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!key) {
|
|
958
|
+
ws.sendLine(` ${ANSI.red}✗ No key provided${ANSI.reset}`);
|
|
959
|
+
ws.sendLine(` ${ANSI.dim}Usage: keys add ${service} <your-api-key>${ANSI.reset}`);
|
|
960
|
+
ws.sendLine(` ${ANSI.dim}Get a key: ${svc.docsUrl}${ANSI.reset}`);
|
|
961
|
+
ws.sendLine('');
|
|
962
|
+
return {};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (svc.validate && !svc.validate(key)) {
|
|
966
|
+
ws.sendLine(` ${ANSI.red}✗ Invalid key format for ${svc.name}${ANSI.reset}`);
|
|
967
|
+
ws.sendLine('');
|
|
968
|
+
return {};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
addKeyDirect(service, key);
|
|
973
|
+
ws.sendLine(` ${ANSI.green}✓ ${svc.name} key stored securely${ANSI.reset}`);
|
|
974
|
+
|
|
975
|
+
// Clear cached engine so it picks up new key
|
|
976
|
+
chatEngines.delete(ws);
|
|
977
|
+
ws.sendLine(` ${ANSI.dim}AI session refreshed — type ${ANSI.gold}ai <question>${ANSI.dim} to chat${ANSI.reset}`);
|
|
978
|
+
ws.sendLine('');
|
|
979
|
+
} catch (err) {
|
|
980
|
+
ws.sendLine(` ${ANSI.red}✗ Failed to store key: ${err.message}${ANSI.reset}`);
|
|
981
|
+
ws.sendLine('');
|
|
982
|
+
}
|
|
983
|
+
return {};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (sub === 'remove' && args[1]) {
|
|
987
|
+
const service = args[1].toLowerCase();
|
|
988
|
+
// Can't remove via web shell without password prompt — point to CLI
|
|
989
|
+
ws.sendLine(` ${ANSI.dim}To remove keys, use the CLI:${ANSI.reset}`);
|
|
990
|
+
ws.sendLine(` ${ANSI.gold}darksol keys remove ${service}${ANSI.reset}`);
|
|
991
|
+
ws.sendLine('');
|
|
992
|
+
return {};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Default: show status
|
|
996
|
+
ws.sendLine(`${ANSI.gold} ◆ API KEYS / LLM CONFIG${ANSI.reset}`);
|
|
997
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
998
|
+
ws.sendLine('');
|
|
999
|
+
|
|
1000
|
+
const llmProviders = ['openai', 'anthropic', 'openrouter', 'ollama'];
|
|
1001
|
+
ws.sendLine(` ${ANSI.gold}LLM Providers:${ANSI.reset}`);
|
|
1002
|
+
for (const p of llmProviders) {
|
|
1003
|
+
const svc = SERVICES[p];
|
|
1004
|
+
const has = hasKey(p);
|
|
1005
|
+
const status = has ? `${ANSI.green}● Connected${ANSI.reset}` : `${ANSI.dim}○ Not set${ANSI.reset}`;
|
|
1006
|
+
ws.sendLine(` ${status} ${ANSI.white}${svc.name.padEnd(20)}${ANSI.reset}${ANSI.dim}${svc.description}${ANSI.reset}`);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
ws.sendLine('');
|
|
1010
|
+
ws.sendLine(` ${ANSI.gold}Quick Setup:${ANSI.reset}`);
|
|
1011
|
+
ws.sendLine(` ${ANSI.green}keys add openai sk-...${ANSI.reset} ${ANSI.dim}Add OpenAI key${ANSI.reset}`);
|
|
1012
|
+
ws.sendLine(` ${ANSI.green}keys add anthropic sk-ant-...${ANSI.reset} ${ANSI.dim}Add Anthropic key${ANSI.reset}`);
|
|
1013
|
+
ws.sendLine(` ${ANSI.green}keys add openrouter sk-or-...${ANSI.reset} ${ANSI.dim}Add OpenRouter key${ANSI.reset}`);
|
|
1014
|
+
ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
|
|
1015
|
+
ws.sendLine('');
|
|
1016
|
+
|
|
1017
|
+
const dataProviders = ['coingecko', 'dexscreener', 'alchemy', 'agentmail'];
|
|
1018
|
+
ws.sendLine(` ${ANSI.gold}Other Services:${ANSI.reset}`);
|
|
1019
|
+
for (const p of dataProviders) {
|
|
1020
|
+
const svc = SERVICES[p];
|
|
1021
|
+
if (!svc) continue;
|
|
1022
|
+
const has = hasKey(p);
|
|
1023
|
+
const status = has ? `${ANSI.green}●${ANSI.reset}` : `${ANSI.dim}○${ANSI.reset}`;
|
|
1024
|
+
ws.sendLine(` ${status} ${ANSI.white}${svc.name.padEnd(20)}${ANSI.reset}${ANSI.dim}${svc.description}${ANSI.reset}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
ws.sendLine('');
|
|
1028
|
+
ws.sendLine(` ${ANSI.dim}Keys are AES-256-GCM encrypted at ~/.darksol/keys/vault.json${ANSI.reset}`);
|
|
1029
|
+
ws.sendLine('');
|
|
1030
|
+
return {};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ══════════════════════════════════════════════════
|
|
1034
|
+
// CHAT LOGS — View conversation history
|
|
1035
|
+
// ══════════════════════════════════════════════════
|
|
1036
|
+
async function cmdChatLogs(args, ws) {
|
|
1037
|
+
const limit = parseInt(args[0]) || 20;
|
|
1038
|
+
const history = getChatHistory(limit);
|
|
1039
|
+
|
|
1040
|
+
ws.sendLine(`${ANSI.gold} ◆ CHAT LOG${ANSI.reset}`);
|
|
1041
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
1042
|
+
ws.sendLine('');
|
|
1043
|
+
|
|
1044
|
+
if (history.length === 0) {
|
|
1045
|
+
ws.sendLine(` ${ANSI.dim}No chat history today. Start with: ai <question>${ANSI.reset}`);
|
|
1046
|
+
ws.sendLine('');
|
|
1047
|
+
return {};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
for (const entry of history) {
|
|
1051
|
+
const time = entry.time || '';
|
|
1052
|
+
const role = entry.role === 'user' ? `${ANSI.gold}You${ANSI.reset}` : `${ANSI.green}AI${ANSI.reset}`;
|
|
1053
|
+
const preview = entry.content.split('\n')[0].slice(0, 80);
|
|
1054
|
+
ws.sendLine(` ${ANSI.dim}${time}${ANSI.reset} ${role}: ${preview}${entry.content.length > 80 ? ANSI.dim + '...' + ANSI.reset : ''}`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
ws.sendLine('');
|
|
1058
|
+
ws.sendLine(` ${ANSI.dim}Logs: ~/.darksol/chat-logs/ (${history.length} messages shown)${ANSI.reset}`);
|
|
1059
|
+
ws.sendLine('');
|
|
1060
|
+
return {};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
693
1063
|
// ══════════════════════════════════════════════════
|
|
694
1064
|
// SEND / RECEIVE (web shell — info only, actual sends require CLI)
|
|
695
1065
|
// ══════════════════════════════════════════════════
|
package/src/web/server.js
CHANGED
|
@@ -20,7 +20,7 @@ const __dirname = dirname(__filename);
|
|
|
20
20
|
* Command handler registry — maps command strings to async functions
|
|
21
21
|
* that return { output: string } with ANSI stripped for web
|
|
22
22
|
*/
|
|
23
|
-
import { handleCommand } from './commands.js';
|
|
23
|
+
import { handleCommand, getAIStatus } from './commands.js';
|
|
24
24
|
|
|
25
25
|
export async function startWebShell(opts = {}) {
|
|
26
26
|
process.on('uncaughtException', (err) => {
|
|
@@ -89,10 +89,38 @@ export async function startWebShell(opts = {}) {
|
|
|
89
89
|
data: getBanner(),
|
|
90
90
|
}));
|
|
91
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
|
+
|
|
92
99
|
ws.on('message', async (raw) => {
|
|
93
100
|
try {
|
|
94
101
|
const msg = JSON.parse(raw.toString());
|
|
95
102
|
|
|
103
|
+
if (msg.type === 'menu_select') {
|
|
104
|
+
// User selected something from an interactive menu
|
|
105
|
+
try {
|
|
106
|
+
const { handleMenuSelect } = await import('./commands.js');
|
|
107
|
+
const result = await handleMenuSelect(msg.id, msg.value, msg.item, {
|
|
108
|
+
send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
|
|
109
|
+
sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
|
|
110
|
+
sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
|
|
111
|
+
});
|
|
112
|
+
if (result?.output) {
|
|
113
|
+
ws.send(JSON.stringify({ type: 'output', data: result.output }));
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
ws.send(JSON.stringify({
|
|
117
|
+
type: 'output',
|
|
118
|
+
data: `\r\n \x1b[31m✗ Error: ${err.message}\x1b[0m\r\n`,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
if (msg.type === 'command') {
|
|
97
125
|
const cmd = msg.data.trim();
|
|
98
126
|
|
|
@@ -126,6 +154,7 @@ export async function startWebShell(opts = {}) {
|
|
|
126
154
|
const result = await handleCommand(cmd, {
|
|
127
155
|
send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
|
|
128
156
|
sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
|
|
157
|
+
sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
|
|
129
158
|
});
|
|
130
159
|
|
|
131
160
|
if (result?.output) {
|
package/src/web/terminal.js
CHANGED
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
const PROMPT = '\x1b[38;2;255;215;0m❯\x1b[0m ';
|
|
6
6
|
const PROMPT_LENGTH = 2; // visible chars: ❯ + space
|
|
7
|
+
const A = {
|
|
8
|
+
gold: '\x1b[38;2;255;215;0m',
|
|
9
|
+
dim: '\x1b[38;2;102;102;102m',
|
|
10
|
+
green: '\x1b[38;2;0;255;136m',
|
|
11
|
+
red: '\x1b[38;2;233;69;96m',
|
|
12
|
+
white: '\x1b[1;37m',
|
|
13
|
+
blue: '\x1b[38;2;68;136;255m',
|
|
14
|
+
r: '\x1b[0m',
|
|
15
|
+
};
|
|
7
16
|
|
|
8
17
|
const COMMANDS = [
|
|
9
|
-
'price', 'watch', 'gas', 'portfolio', 'history', 'market',
|
|
10
|
-
'wallet', '
|
|
11
|
-
'help', 'clear', 'banner', 'exit',
|
|
18
|
+
'ai', 'price', 'watch', 'gas', 'portfolio', 'history', 'market',
|
|
19
|
+
'wallet', 'send', 'receive', 'mail', 'keys', 'oracle', 'casino',
|
|
20
|
+
'facilitator', 'config', 'logs', 'help', 'clear', 'banner', 'exit',
|
|
12
21
|
];
|
|
13
22
|
|
|
14
23
|
let term;
|
|
@@ -18,8 +27,14 @@ let commandHistory = [];
|
|
|
18
27
|
let historyIndex = -1;
|
|
19
28
|
let connected = false;
|
|
20
29
|
|
|
30
|
+
// ── MENU STATE ────────────────────────────────
|
|
31
|
+
let menuActive = false;
|
|
32
|
+
let menuItems = [];
|
|
33
|
+
let menuIndex = 0;
|
|
34
|
+
let menuId = '';
|
|
35
|
+
let menuTitle = '';
|
|
36
|
+
|
|
21
37
|
async function init() {
|
|
22
|
-
// Load xterm.js
|
|
23
38
|
term = new Terminal({
|
|
24
39
|
theme: {
|
|
25
40
|
background: '#0a0a1a',
|
|
@@ -64,16 +79,42 @@ async function init() {
|
|
|
64
79
|
|
|
65
80
|
window.addEventListener('resize', () => fitAddon.fit());
|
|
66
81
|
|
|
67
|
-
// Connect WebSocket
|
|
68
82
|
connectWS();
|
|
69
83
|
|
|
70
|
-
// Input handling
|
|
71
84
|
term.onKey(({ key, domEvent }) => {
|
|
72
85
|
const code = domEvent.keyCode;
|
|
73
86
|
const ctrl = domEvent.ctrlKey;
|
|
74
87
|
|
|
88
|
+
// ── MENU MODE ──
|
|
89
|
+
if (menuActive) {
|
|
90
|
+
if (code === 38) { // Up
|
|
91
|
+
if (menuIndex > 0) { menuIndex--; renderMenu(); }
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (code === 40) { // Down
|
|
95
|
+
if (menuIndex < menuItems.length - 1) { menuIndex++; renderMenu(); }
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (code === 13) { // Enter — select
|
|
99
|
+
const selected = menuItems[menuIndex];
|
|
100
|
+
menuActive = false;
|
|
101
|
+
// Clear menu display
|
|
102
|
+
term.write('\x1b[?25h'); // show cursor
|
|
103
|
+
sendMenuSelection(menuId, selected);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (code === 27 || (ctrl && code === 67)) { // Esc or Ctrl+C — cancel
|
|
107
|
+
menuActive = false;
|
|
108
|
+
term.write('\r\n');
|
|
109
|
+
term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
|
|
110
|
+
writePrompt();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return; // Ignore other keys in menu mode
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── NORMAL MODE ──
|
|
75
117
|
if (ctrl && code === 67) {
|
|
76
|
-
// Ctrl+C — cancel
|
|
77
118
|
currentLine = '';
|
|
78
119
|
term.write('^C\r\n');
|
|
79
120
|
writePrompt();
|
|
@@ -81,14 +122,12 @@ async function init() {
|
|
|
81
122
|
}
|
|
82
123
|
|
|
83
124
|
if (ctrl && code === 76) {
|
|
84
|
-
// Ctrl+L — clear
|
|
85
125
|
term.clear();
|
|
86
126
|
writePrompt();
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
89
129
|
|
|
90
130
|
if (code === 13) {
|
|
91
|
-
// Enter
|
|
92
131
|
term.write('\r\n');
|
|
93
132
|
const cmd = currentLine.trim();
|
|
94
133
|
|
|
@@ -106,7 +145,6 @@ async function init() {
|
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
if (code === 8) {
|
|
109
|
-
// Backspace
|
|
110
148
|
if (currentLine.length > 0) {
|
|
111
149
|
currentLine = currentLine.slice(0, -1);
|
|
112
150
|
term.write('\b \b');
|
|
@@ -115,7 +153,6 @@ async function init() {
|
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
if (code === 38) {
|
|
118
|
-
// Arrow up — history
|
|
119
156
|
if (historyIndex < commandHistory.length - 1) {
|
|
120
157
|
historyIndex++;
|
|
121
158
|
replaceInput(commandHistory[historyIndex]);
|
|
@@ -124,7 +161,6 @@ async function init() {
|
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
if (code === 40) {
|
|
127
|
-
// Arrow down — history
|
|
128
164
|
if (historyIndex > 0) {
|
|
129
165
|
historyIndex--;
|
|
130
166
|
replaceInput(commandHistory[historyIndex]);
|
|
@@ -136,13 +172,11 @@ async function init() {
|
|
|
136
172
|
}
|
|
137
173
|
|
|
138
174
|
if (code === 9) {
|
|
139
|
-
// Tab — autocomplete
|
|
140
175
|
domEvent.preventDefault();
|
|
141
176
|
autocomplete();
|
|
142
177
|
return;
|
|
143
178
|
}
|
|
144
179
|
|
|
145
|
-
// Printable characters
|
|
146
180
|
if (key.length === 1 && !ctrl) {
|
|
147
181
|
currentLine += key;
|
|
148
182
|
term.write(key);
|
|
@@ -151,7 +185,7 @@ async function init() {
|
|
|
151
185
|
|
|
152
186
|
// Paste support
|
|
153
187
|
term.onData((data) => {
|
|
154
|
-
|
|
188
|
+
if (menuActive) return;
|
|
155
189
|
if (data.length > 1 && !data.startsWith('\x1b')) {
|
|
156
190
|
const clean = data.replace(/[\r\n]/g, '');
|
|
157
191
|
currentLine += clean;
|
|
@@ -162,6 +196,67 @@ async function init() {
|
|
|
162
196
|
term.focus();
|
|
163
197
|
}
|
|
164
198
|
|
|
199
|
+
// ── MENU RENDERING ────────────────────────────
|
|
200
|
+
function renderMenu() {
|
|
201
|
+
// Move cursor up to redraw (clear previous menu)
|
|
202
|
+
const totalLines = menuItems.length + 2; // title + items + hint
|
|
203
|
+
term.write(`\x1b[${totalLines}A\x1b[J`);
|
|
204
|
+
|
|
205
|
+
term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < menuItems.length; i++) {
|
|
208
|
+
const item = menuItems[i];
|
|
209
|
+
const label = item.label || item.value || String(item);
|
|
210
|
+
const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
|
|
211
|
+
|
|
212
|
+
if (i === menuIndex) {
|
|
213
|
+
term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
|
|
214
|
+
} else {
|
|
215
|
+
term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function showMenu(id, title, items) {
|
|
223
|
+
menuActive = true;
|
|
224
|
+
menuId = id;
|
|
225
|
+
menuTitle = title;
|
|
226
|
+
menuItems = items;
|
|
227
|
+
menuIndex = 0;
|
|
228
|
+
|
|
229
|
+
term.write('\x1b[?25l'); // hide cursor during menu
|
|
230
|
+
term.write('\r\n');
|
|
231
|
+
|
|
232
|
+
// Initial render
|
|
233
|
+
term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
|
|
234
|
+
for (let i = 0; i < menuItems.length; i++) {
|
|
235
|
+
const item = menuItems[i];
|
|
236
|
+
const label = item.label || item.value || String(item);
|
|
237
|
+
const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
|
|
238
|
+
|
|
239
|
+
if (i === menuIndex) {
|
|
240
|
+
term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
|
|
241
|
+
} else {
|
|
242
|
+
term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sendMenuSelection(id, item) {
|
|
249
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
250
|
+
ws.send(JSON.stringify({
|
|
251
|
+
type: 'menu_select',
|
|
252
|
+
id,
|
|
253
|
+
value: item.value || item.label || String(item),
|
|
254
|
+
item,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── WEBSOCKET ─────────────────────────────────
|
|
165
260
|
function connectWS() {
|
|
166
261
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
167
262
|
ws = new WebSocket(`${protocol}//${window.location.host}`);
|
|
@@ -177,13 +272,15 @@ function connectWS() {
|
|
|
177
272
|
|
|
178
273
|
if (msg.type === 'output') {
|
|
179
274
|
term.write(msg.data);
|
|
180
|
-
writePrompt();
|
|
275
|
+
if (!menuActive) writePrompt();
|
|
181
276
|
} else if (msg.type === 'clear') {
|
|
182
277
|
term.clear();
|
|
183
278
|
writePrompt();
|
|
279
|
+
} else if (msg.type === 'menu') {
|
|
280
|
+
// Server is requesting user to pick from a menu
|
|
281
|
+
showMenu(msg.id, msg.title, msg.items);
|
|
184
282
|
}
|
|
185
283
|
} catch {
|
|
186
|
-
// Raw text fallback
|
|
187
284
|
term.write(event.data);
|
|
188
285
|
}
|
|
189
286
|
};
|
|
@@ -191,12 +288,8 @@ function connectWS() {
|
|
|
191
288
|
ws.onclose = () => {
|
|
192
289
|
connected = false;
|
|
193
290
|
updateStatus(false);
|
|
194
|
-
term.write(
|
|
195
|
-
|
|
196
|
-
// Reconnect after 3s
|
|
197
|
-
setTimeout(() => {
|
|
198
|
-
connectWS();
|
|
199
|
-
}, 3000);
|
|
291
|
+
term.write(`\r\n${A.red} ⚡ Connection lost. Reconnecting...${A.r}\r\n`);
|
|
292
|
+
setTimeout(() => connectWS(), 3000);
|
|
200
293
|
};
|
|
201
294
|
|
|
202
295
|
ws.onerror = () => {
|
|
@@ -209,7 +302,7 @@ function sendCommand(cmd) {
|
|
|
209
302
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
210
303
|
ws.send(JSON.stringify({ type: 'command', data: cmd }));
|
|
211
304
|
} else {
|
|
212
|
-
term.write(
|
|
305
|
+
term.write(`${A.red} ✗ Not connected${A.r}\r\n`);
|
|
213
306
|
writePrompt();
|
|
214
307
|
}
|
|
215
308
|
}
|
|
@@ -219,28 +312,20 @@ function writePrompt() {
|
|
|
219
312
|
}
|
|
220
313
|
|
|
221
314
|
function replaceInput(text) {
|
|
222
|
-
// Clear current input
|
|
223
315
|
const clearLen = currentLine.length;
|
|
224
|
-
for (let i = 0; i < clearLen; i++)
|
|
225
|
-
term.write('\b \b');
|
|
226
|
-
}
|
|
316
|
+
for (let i = 0; i < clearLen; i++) term.write('\b \b');
|
|
227
317
|
currentLine = text;
|
|
228
318
|
term.write(text);
|
|
229
319
|
}
|
|
230
320
|
|
|
231
321
|
function autocomplete() {
|
|
232
322
|
if (!currentLine) return;
|
|
233
|
-
|
|
234
323
|
const matches = COMMANDS.filter((c) => c.startsWith(currentLine.toLowerCase()));
|
|
235
|
-
|
|
236
324
|
if (matches.length === 1) {
|
|
237
325
|
replaceInput(matches[0]);
|
|
238
326
|
} else if (matches.length > 1) {
|
|
239
|
-
// Show options
|
|
240
327
|
term.write('\r\n');
|
|
241
|
-
term.write(
|
|
242
|
-
matches.map((m) => ` \x1b[38;2;102;102;102m${m}\x1b[0m`).join(' ')
|
|
243
|
-
);
|
|
328
|
+
term.write(matches.map((m) => ` ${A.dim}${m}${A.r}`).join(' '));
|
|
244
329
|
term.write('\r\n');
|
|
245
330
|
writePrompt();
|
|
246
331
|
term.write(currentLine);
|
|
@@ -250,13 +335,8 @@ function autocomplete() {
|
|
|
250
335
|
function updateStatus(isConnected) {
|
|
251
336
|
const dot = document.querySelector('.status-dot');
|
|
252
337
|
const text = document.querySelector('.status-text');
|
|
253
|
-
if (dot)
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
if (text) {
|
|
257
|
-
text.textContent = isConnected ? 'Connected' : 'Disconnected';
|
|
258
|
-
}
|
|
338
|
+
if (dot) dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
|
|
339
|
+
if (text) text.textContent = isConnected ? 'Connected' : 'Disconnected';
|
|
259
340
|
}
|
|
260
341
|
|
|
261
|
-
// Boot
|
|
262
342
|
document.addEventListener('DOMContentLoaded', init);
|