@darksol/terminal 0.4.7 → 0.4.9

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.7",
3
+ "version": "0.4.9",
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": {
@@ -69,6 +69,169 @@ const USDC_ADDRESSES = {
69
69
 
70
70
  const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
71
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 'keys_provider':
139
+ if (value === 'back') {
140
+ ws.sendLine('');
141
+ return {};
142
+ }
143
+ // Ask for the key via a prompt
144
+ const svc = SERVICES[value];
145
+ if (!svc) return {};
146
+ ws.sendLine('');
147
+ ws.sendLine(` ${ANSI.gold}◆ ${svc.name}${ANSI.reset}`);
148
+ ws.sendLine(` ${ANSI.dim}Docs: ${svc.docsUrl}${ANSI.reset}`);
149
+ if (value === 'ollama') {
150
+ ws.sendLine(` ${ANSI.dim}Enter your Ollama host URL (e.g. http://localhost:11434)${ANSI.reset}`);
151
+ } else {
152
+ ws.sendLine(` ${ANSI.dim}Paste your API key below:${ANSI.reset}`);
153
+ }
154
+ ws.sendLine('');
155
+ // Send a prompt request to the client
156
+ ws.send(JSON.stringify({
157
+ type: 'prompt',
158
+ id: 'keys_input',
159
+ label: `${svc.name} key:`,
160
+ service: value,
161
+ mask: value !== 'ollama', // mask API keys, not URLs
162
+ }));
163
+ return {};
164
+
165
+ case 'config_action':
166
+ if (value === 'chain') {
167
+ const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
168
+ const current = getConfig('chain') || 'base';
169
+ ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
170
+ value: c,
171
+ label: c === current ? `★ ${c}` : c,
172
+ desc: c === current ? 'current' : '',
173
+ })));
174
+ return {};
175
+ }
176
+ if (value === 'keys') {
177
+ return await handleCommand('keys', ws);
178
+ }
179
+ ws.sendLine('');
180
+ return {};
181
+
182
+ case 'main_menu':
183
+ return await handleCommand(value, ws);
184
+ }
185
+
186
+ return {};
187
+ }
188
+
189
+ /**
190
+ * Handle text prompt responses from the client
191
+ */
192
+ export async function handlePromptResponse(id, value, meta, ws) {
193
+ if (id === 'keys_input') {
194
+ const service = meta.service;
195
+ const svc = SERVICES[service];
196
+ if (!svc || !value) {
197
+ ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`);
198
+ ws.sendLine('');
199
+ return {};
200
+ }
201
+
202
+ // Validate
203
+ if (svc.validate && !svc.validate(value)) {
204
+ ws.sendLine(` ${ANSI.red}✗ Invalid format for ${svc.name}${ANSI.reset}`);
205
+ if (service === 'openai') ws.sendLine(` ${ANSI.dim}Key should start with sk-${ANSI.reset}`);
206
+ if (service === 'anthropic') ws.sendLine(` ${ANSI.dim}Key should start with sk-ant-${ANSI.reset}`);
207
+ if (service === 'openrouter') ws.sendLine(` ${ANSI.dim}Key should start with sk-or-${ANSI.reset}`);
208
+ if (service === 'ollama') ws.sendLine(` ${ANSI.dim}Should be a URL like http://localhost:11434${ANSI.reset}`);
209
+ ws.sendLine('');
210
+ return {};
211
+ }
212
+
213
+ // Store it
214
+ try {
215
+ addKeyDirect(service, value);
216
+ ws.sendLine(` ${ANSI.green}✓ ${svc.name} key stored securely${ANSI.reset}`);
217
+ ws.sendLine(` ${ANSI.dim}Encrypted at ~/.darksol/keys/vault.json${ANSI.reset}`);
218
+ ws.sendLine('');
219
+
220
+ // Clear cached AI engine
221
+ // (chatEngines is WeakMap keyed by ws, but we can't access the real ws here —
222
+ // the engine will reinit on next ai command since keys changed)
223
+ ws.sendLine(` ${ANSI.green}● AI ready!${ANSI.reset} ${ANSI.dim}Type ${ANSI.gold}ai <question>${ANSI.dim} to start chatting.${ANSI.reset}`);
224
+ ws.sendLine('');
225
+ } catch (err) {
226
+ ws.sendLine(` ${ANSI.red}✗ Failed: ${err.message}${ANSI.reset}`);
227
+ ws.sendLine('');
228
+ }
229
+ return {};
230
+ }
231
+
232
+ return {};
233
+ }
234
+
72
235
  /**
73
236
  * AI status check — shown on connection
74
237
  */
@@ -90,14 +253,12 @@ export function getAIStatus() {
90
253
  return [
91
254
  ` ${red}○ AI not configured${reset} ${dim}— no LLM provider connected${reset}`,
92
255
  '',
93
- ` ${gold}Quick setup pick one:${reset}`,
256
+ ` ${dim}Type ${gold}keys${dim} to set up an LLM provider, or paste directly:${reset}`,
94
257
  ` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
95
258
  ` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
96
259
  ` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
97
260
  ` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
98
261
  '',
99
- ` ${dim}Or run the full setup wizard: ${gold}darksol setup${reset}`,
100
- '',
101
262
  ].join('\r\n');
102
263
  }
103
264
 
@@ -402,47 +563,121 @@ async function cmdMarket(args, ws) {
402
563
  // WALLET
403
564
  // ══════════════════════════════════════════════════
404
565
  async function cmdWallet(args, ws) {
405
- const sub = args[0] || 'list';
566
+ const sub = args[0];
406
567
 
407
- if (sub === 'list') {
408
- const { listWallets } = await import('../wallet/keystore.js');
409
- const wallets = listWallets();
410
- const active = getConfig('activeWallet');
568
+ // If a specific subcommand, handle directly
569
+ if (sub === 'balance') {
570
+ return await showWalletDetail(args[1] || getConfig('activeWallet'), ws);
571
+ }
411
572
 
412
- ws.sendLine(`${ANSI.gold} WALLETS${ANSI.reset}`);
413
- ws.sendLine(`${ANSI.dim} ${''.repeat(50)}${ANSI.reset}`);
573
+ if (sub === 'use' && args[1]) {
574
+ setConfig('activeWallet', args[1]);
575
+ ws.sendLine(` ${ANSI.green}✓ Active wallet set to "${args[1]}"${ANSI.reset}`);
576
+ ws.sendLine('');
577
+ return {};
578
+ }
414
579
 
415
- if (wallets.length === 0) {
416
- ws.sendLine(` ${ANSI.dim}No wallets. Create one in the CLI: darksol wallet create${ANSI.reset}`);
417
- } else {
418
- for (const w of wallets) {
419
- const indicator = w === active ? `${ANSI.gold}► ${ANSI.reset}` : ' ';
420
- ws.sendLine(` ${indicator}${ANSI.white}${w}${ANSI.reset}`);
421
- }
422
- }
580
+ // Default: interactive wallet picker
581
+ const { listWallets } = await import('../wallet/keystore.js');
582
+ const wallets = listWallets();
583
+ const active = getConfig('activeWallet');
423
584
 
585
+ if (wallets.length === 0) {
586
+ ws.sendLine(`${ANSI.gold} ◆ WALLETS${ANSI.reset}`);
587
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
588
+ ws.sendLine(` ${ANSI.dim}No wallets found.${ANSI.reset}`);
589
+ ws.sendLine(` ${ANSI.dim}Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
424
590
  ws.sendLine('');
425
591
  return {};
426
592
  }
427
593
 
428
- if (sub === 'balance') {
429
- const name = args[1] || getConfig('activeWallet');
430
- if (!name) return { output: ` ${ANSI.red}No active wallet${ANSI.reset}\r\n` };
594
+ if (wallets.length === 1) {
595
+ // Only one wallet go straight to it
596
+ return await showWalletDetail(wallets[0].name, ws);
597
+ }
431
598
 
432
- const { loadWallet } = await import('../wallet/keystore.js');
433
- const w = loadWallet(name);
434
- const chain = getConfig('chain') || 'base';
435
- const provider = new ethers.JsonRpcProvider(RPCS[chain]);
436
- const bal = parseFloat(ethers.formatEther(await provider.getBalance(w.address)));
599
+ // Multiple wallets show interactive menu
600
+ const menuItems = wallets.map(w => ({
601
+ value: w.name,
602
+ label: `${w.name === active ? '★ ' : ''}${w.name}`,
603
+ desc: `${w.address.slice(0, 6)}...${w.address.slice(-4)}`,
604
+ }));
605
+
606
+ ws.sendMenu('wallet_select', '◆ Select Wallet', menuItems);
607
+ return {};
608
+ }
609
+
610
+ async function showWalletDetail(name, ws) {
611
+ if (!name) {
612
+ ws.sendLine(` ${ANSI.red}No wallet selected${ANSI.reset}`);
613
+ ws.sendLine('');
614
+ return {};
615
+ }
437
616
 
438
- ws.sendLine(`${ANSI.gold} BALANCE ${name}${ANSI.reset}`);
439
- ws.sendLine(`${ANSI.dim} ${w.address}${ANSI.reset}`);
440
- ws.sendLine(` ${ANSI.white}${bal.toFixed(6)} ETH${ANSI.reset} on ${chain}`);
617
+ const { loadWallet } = await import('../wallet/keystore.js');
618
+ let walletData;
619
+ try {
620
+ walletData = loadWallet(name);
621
+ } catch {
622
+ ws.sendLine(` ${ANSI.red}Wallet "${name}" not found${ANSI.reset}`);
441
623
  ws.sendLine('');
442
624
  return {};
443
625
  }
444
626
 
445
- return { output: ` ${ANSI.dim}Wallet commands: list, balance${ANSI.reset}\r\n` };
627
+ const chain = getConfig('chain') || 'base';
628
+ const provider = new ethers.JsonRpcProvider(RPCS[chain]);
629
+
630
+ ws.sendLine(`${ANSI.gold} ◆ WALLET — ${name}${ANSI.reset}`);
631
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
632
+ ws.sendLine(` ${ANSI.darkGold}Address${ANSI.reset} ${ANSI.white}${walletData.address}${ANSI.reset}`);
633
+ ws.sendLine(` ${ANSI.darkGold}Chain${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
634
+ ws.sendLine(` ${ANSI.darkGold}Created${ANSI.reset} ${ANSI.dim}${walletData.createdAt ? new Date(walletData.createdAt).toLocaleDateString() : 'unknown'}${ANSI.reset}`);
635
+ ws.sendLine(` ${ANSI.darkGold}Encryption${ANSI.reset} ${ANSI.dim}AES-256-GCM + scrypt${ANSI.reset}`);
636
+
637
+ // Fetch balance
638
+ ws.sendLine('');
639
+ ws.sendLine(` ${ANSI.dim}Fetching balance...${ANSI.reset}`);
640
+
641
+ try {
642
+ const balance = await provider.getBalance(walletData.address);
643
+ const ethBal = parseFloat(ethers.formatEther(balance));
644
+
645
+ // Also check USDC
646
+ const usdcAddr = USDC_ADDRESSES[chain];
647
+ let usdcBal = 0;
648
+ if (usdcAddr) {
649
+ try {
650
+ const usdc = new ethers.Contract(usdcAddr, ['function balanceOf(address) view returns (uint256)'], provider);
651
+ const raw = await usdc.balanceOf(walletData.address);
652
+ usdcBal = parseFloat(ethers.formatUnits(raw, 6));
653
+ } catch {}
654
+ }
655
+
656
+ // Get ETH price for USD value
657
+ const ethPrice = await getEthPrice();
658
+ const usdValue = (ethBal * ethPrice) + usdcBal;
659
+
660
+ // Overwrite "Fetching..." line
661
+ 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}`);
662
+ ws.sendLine(` ${ANSI.darkGold}USDC${ANSI.reset} ${ANSI.white}$${usdcBal.toFixed(2)}${ANSI.reset}`);
663
+ ws.sendLine(` ${ANSI.darkGold}Total${ANSI.reset} ${ANSI.green}$${usdValue.toFixed(2)}${ANSI.reset}`);
664
+ } catch (err) {
665
+ ws.sendLine(` ${ANSI.red}Balance fetch failed: ${err.message}${ANSI.reset}`);
666
+ }
667
+
668
+ ws.sendLine('');
669
+
670
+ // Show action menu
671
+ ws.sendMenu('wallet_action', '◆ What would you like to do?', [
672
+ { value: 'receive', label: '📥 Receive', desc: 'Show address to receive funds' },
673
+ { value: 'send', label: '📤 Send', desc: 'Send ETH or tokens (CLI required)' },
674
+ { value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balance view' },
675
+ { value: 'history', label: '📜 History', desc: 'Transaction history' },
676
+ { value: 'switch', label: '🔄 Switch chain', desc: `Currently: ${chain}` },
677
+ { value: 'back', label: '← Back', desc: '' },
678
+ ]);
679
+
680
+ return {};
446
681
  }
447
682
 
448
683
  // ══════════════════════════════════════════════════
@@ -581,8 +816,16 @@ async function cmdConfig(ws) {
581
816
  ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset}`);
582
817
  ws.sendLine(` ${ANSI.darkGold}Slippage${ANSI.reset} ${ANSI.white}${slippage}%${ANSI.reset}`);
583
818
  ws.sendLine(` ${ANSI.darkGold}Mail${ANSI.reset} ${ANSI.white}${email}${ANSI.reset}`);
584
- 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}`}`);
819
+ ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasAnyLLM() ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
585
820
  ws.sendLine('');
821
+
822
+ // Offer interactive config
823
+ ws.sendMenu('config_action', '◆ Configure', [
824
+ { value: 'chain', label: '🔗 Change chain', desc: `Currently: ${chain}` },
825
+ { value: 'keys', label: '🔑 LLM / API keys', desc: '' },
826
+ { value: 'back', label: '← Back', desc: '' },
827
+ ]);
828
+
586
829
  return {};
587
830
  }
588
831
 
@@ -842,6 +1085,19 @@ async function cmdKeys(args, ws) {
842
1085
  ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
843
1086
  ws.sendLine('');
844
1087
 
1088
+ // Interactive menu to add keys
1089
+ const llmItems = llmProviders.map(p => {
1090
+ const svc = SERVICES[p];
1091
+ const has = hasKey(p);
1092
+ return {
1093
+ value: p,
1094
+ label: `${has ? '✓' : '+'} ${svc.name}`,
1095
+ desc: has ? 'Connected — replace key' : `Add key (${svc.docsUrl})`,
1096
+ };
1097
+ });
1098
+ llmItems.push({ value: 'back', label: '← Back', desc: '' });
1099
+ ws.sendMenu('keys_provider', '◆ Add / Update API Key', llmItems);
1100
+
845
1101
  const dataProviders = ['coingecko', 'dexscreener', 'alchemy', 'agentmail'];
846
1102
  ws.sendLine(` ${ANSI.gold}Other Services:${ANSI.reset}`);
847
1103
  for (const p of dataProviders) {
package/src/web/server.js CHANGED
@@ -100,6 +100,48 @@ export async function startWebShell(opts = {}) {
100
100
  try {
101
101
  const msg = JSON.parse(raw.toString());
102
102
 
103
+ if (msg.type === 'prompt_response') {
104
+ // User typed a response to a prompt (e.g. API key input)
105
+ try {
106
+ const { handlePromptResponse } = await import('./commands.js');
107
+ const result = await handlePromptResponse(msg.id, msg.value, msg.meta || {}, {
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
+
124
+ if (msg.type === 'menu_select') {
125
+ // User selected something from an interactive menu
126
+ try {
127
+ const { handleMenuSelect } = await import('./commands.js');
128
+ const result = await handleMenuSelect(msg.id, msg.value, msg.item, {
129
+ send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
130
+ sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
131
+ sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
132
+ });
133
+ if (result?.output) {
134
+ ws.send(JSON.stringify({ type: 'output', data: result.output }));
135
+ }
136
+ } catch (err) {
137
+ ws.send(JSON.stringify({
138
+ type: 'output',
139
+ data: `\r\n \x1b[31m✗ Error: ${err.message}\x1b[0m\r\n`,
140
+ }));
141
+ }
142
+ return;
143
+ }
144
+
103
145
  if (msg.type === 'command') {
104
146
  const cmd = msg.data.trim();
105
147
 
@@ -133,6 +175,7 @@ export async function startWebShell(opts = {}) {
133
175
  const result = await handleCommand(cmd, {
134
176
  send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
135
177
  sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
178
+ sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
136
179
  });
137
180
 
138
181
  if (result?.output) {
@@ -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', 'mail', 'oracle', 'casino', 'facilitator', 'config',
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,21 @@ 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
+
37
+ // ── PROMPT STATE (text input) ─────────────────
38
+ let promptActive = false;
39
+ let promptId = '';
40
+ let promptMeta = {};
41
+ let promptInput = '';
42
+ let promptMask = false;
43
+
21
44
  async function init() {
22
- // Load xterm.js
23
45
  term = new Terminal({
24
46
  theme: {
25
47
  background: '#0a0a1a',
@@ -64,16 +86,84 @@ async function init() {
64
86
 
65
87
  window.addEventListener('resize', () => fitAddon.fit());
66
88
 
67
- // Connect WebSocket
68
89
  connectWS();
69
90
 
70
- // Input handling
71
91
  term.onKey(({ key, domEvent }) => {
72
92
  const code = domEvent.keyCode;
73
93
  const ctrl = domEvent.ctrlKey;
74
94
 
95
+ // ── PROMPT MODE (text input) ──
96
+ if (promptActive) {
97
+ if (code === 13) { // Enter — submit
98
+ promptActive = false;
99
+ term.write('\r\n');
100
+ if (promptInput) {
101
+ ws.send(JSON.stringify({
102
+ type: 'prompt_response',
103
+ id: promptId,
104
+ value: promptInput,
105
+ meta: promptMeta,
106
+ }));
107
+ } else {
108
+ term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
109
+ writePrompt();
110
+ }
111
+ promptInput = '';
112
+ return;
113
+ }
114
+ if (code === 27 || (ctrl && code === 67)) { // Esc/Ctrl+C — cancel
115
+ promptActive = false;
116
+ promptInput = '';
117
+ term.write('\r\n');
118
+ term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
119
+ writePrompt();
120
+ return;
121
+ }
122
+ if (code === 8) { // Backspace
123
+ if (promptInput.length > 0) {
124
+ promptInput = promptInput.slice(0, -1);
125
+ term.write('\b \b');
126
+ }
127
+ return;
128
+ }
129
+ // Printable chars
130
+ if (key.length === 1 && !ctrl) {
131
+ promptInput += key;
132
+ term.write(promptMask ? '●' : key);
133
+ }
134
+ return;
135
+ }
136
+
137
+ // ── MENU MODE ──
138
+ if (menuActive) {
139
+ if (code === 38) { // Up
140
+ if (menuIndex > 0) { menuIndex--; renderMenu(); }
141
+ return;
142
+ }
143
+ if (code === 40) { // Down
144
+ if (menuIndex < menuItems.length - 1) { menuIndex++; renderMenu(); }
145
+ return;
146
+ }
147
+ if (code === 13) { // Enter — select
148
+ const selected = menuItems[menuIndex];
149
+ menuActive = false;
150
+ // Clear menu display
151
+ term.write('\x1b[?25h'); // show cursor
152
+ sendMenuSelection(menuId, selected);
153
+ return;
154
+ }
155
+ if (code === 27 || (ctrl && code === 67)) { // Esc or Ctrl+C — cancel
156
+ menuActive = false;
157
+ term.write('\r\n');
158
+ term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
159
+ writePrompt();
160
+ return;
161
+ }
162
+ return; // Ignore other keys in menu mode
163
+ }
164
+
165
+ // ── NORMAL MODE ──
75
166
  if (ctrl && code === 67) {
76
- // Ctrl+C — cancel
77
167
  currentLine = '';
78
168
  term.write('^C\r\n');
79
169
  writePrompt();
@@ -81,14 +171,12 @@ async function init() {
81
171
  }
82
172
 
83
173
  if (ctrl && code === 76) {
84
- // Ctrl+L — clear
85
174
  term.clear();
86
175
  writePrompt();
87
176
  return;
88
177
  }
89
178
 
90
179
  if (code === 13) {
91
- // Enter
92
180
  term.write('\r\n');
93
181
  const cmd = currentLine.trim();
94
182
 
@@ -106,7 +194,6 @@ async function init() {
106
194
  }
107
195
 
108
196
  if (code === 8) {
109
- // Backspace
110
197
  if (currentLine.length > 0) {
111
198
  currentLine = currentLine.slice(0, -1);
112
199
  term.write('\b \b');
@@ -115,7 +202,6 @@ async function init() {
115
202
  }
116
203
 
117
204
  if (code === 38) {
118
- // Arrow up — history
119
205
  if (historyIndex < commandHistory.length - 1) {
120
206
  historyIndex++;
121
207
  replaceInput(commandHistory[historyIndex]);
@@ -124,7 +210,6 @@ async function init() {
124
210
  }
125
211
 
126
212
  if (code === 40) {
127
- // Arrow down — history
128
213
  if (historyIndex > 0) {
129
214
  historyIndex--;
130
215
  replaceInput(commandHistory[historyIndex]);
@@ -136,13 +221,11 @@ async function init() {
136
221
  }
137
222
 
138
223
  if (code === 9) {
139
- // Tab — autocomplete
140
224
  domEvent.preventDefault();
141
225
  autocomplete();
142
226
  return;
143
227
  }
144
228
 
145
- // Printable characters
146
229
  if (key.length === 1 && !ctrl) {
147
230
  currentLine += key;
148
231
  term.write(key);
@@ -151,17 +234,83 @@ async function init() {
151
234
 
152
235
  // Paste support
153
236
  term.onData((data) => {
154
- // Only handle paste (multi-char input)
237
+ if (menuActive) return;
155
238
  if (data.length > 1 && !data.startsWith('\x1b')) {
156
239
  const clean = data.replace(/[\r\n]/g, '');
157
- currentLine += clean;
158
- term.write(clean);
240
+ if (promptActive) {
241
+ promptInput += clean;
242
+ term.write(promptMask ? '●'.repeat(clean.length) : clean);
243
+ } else {
244
+ currentLine += clean;
245
+ term.write(clean);
246
+ }
159
247
  }
160
248
  });
161
249
 
162
250
  term.focus();
163
251
  }
164
252
 
253
+ // ── MENU RENDERING ────────────────────────────
254
+ function renderMenu() {
255
+ // Move cursor up to redraw (clear previous menu)
256
+ const totalLines = menuItems.length + 2; // title + items + hint
257
+ term.write(`\x1b[${totalLines}A\x1b[J`);
258
+
259
+ term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
260
+
261
+ for (let i = 0; i < menuItems.length; i++) {
262
+ const item = menuItems[i];
263
+ const label = item.label || item.value || String(item);
264
+ const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
265
+
266
+ if (i === menuIndex) {
267
+ term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
268
+ } else {
269
+ term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
270
+ }
271
+ }
272
+
273
+ term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
274
+ }
275
+
276
+ function showMenu(id, title, items) {
277
+ menuActive = true;
278
+ menuId = id;
279
+ menuTitle = title;
280
+ menuItems = items;
281
+ menuIndex = 0;
282
+
283
+ term.write('\x1b[?25l'); // hide cursor during menu
284
+ term.write('\r\n');
285
+
286
+ // Initial render
287
+ term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
288
+ for (let i = 0; i < menuItems.length; i++) {
289
+ const item = menuItems[i];
290
+ const label = item.label || item.value || String(item);
291
+ const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
292
+
293
+ if (i === menuIndex) {
294
+ term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
295
+ } else {
296
+ term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
297
+ }
298
+ }
299
+ term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
300
+ }
301
+
302
+ function sendMenuSelection(id, item) {
303
+ if (ws && ws.readyState === WebSocket.OPEN) {
304
+ ws.send(JSON.stringify({
305
+ type: 'menu_select',
306
+ id,
307
+ value: item.value || item.label || String(item),
308
+ item,
309
+ }));
310
+ }
311
+ }
312
+
313
+ // ── WEBSOCKET ─────────────────────────────────
165
314
  function connectWS() {
166
315
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
167
316
  ws = new WebSocket(`${protocol}//${window.location.host}`);
@@ -177,13 +326,22 @@ function connectWS() {
177
326
 
178
327
  if (msg.type === 'output') {
179
328
  term.write(msg.data);
180
- writePrompt();
329
+ if (!menuActive) writePrompt();
181
330
  } else if (msg.type === 'clear') {
182
331
  term.clear();
183
332
  writePrompt();
333
+ } else if (msg.type === 'menu') {
334
+ showMenu(msg.id, msg.title, msg.items);
335
+ } else if (msg.type === 'prompt') {
336
+ // Server wants text input (e.g. API key)
337
+ promptActive = true;
338
+ promptId = msg.id;
339
+ promptMeta = { service: msg.service, ...msg };
340
+ promptInput = '';
341
+ promptMask = msg.mask || false;
342
+ term.write(` ${A.gold}${msg.label || 'Input:'}${A.r} `);
184
343
  }
185
344
  } catch {
186
- // Raw text fallback
187
345
  term.write(event.data);
188
346
  }
189
347
  };
@@ -191,12 +349,8 @@ function connectWS() {
191
349
  ws.onclose = () => {
192
350
  connected = false;
193
351
  updateStatus(false);
194
- term.write('\r\n\x1b[38;2;233;69;96m ⚡ Connection lost. Reconnecting...\x1b[0m\r\n');
195
-
196
- // Reconnect after 3s
197
- setTimeout(() => {
198
- connectWS();
199
- }, 3000);
352
+ term.write(`\r\n${A.red} ⚡ Connection lost. Reconnecting...${A.r}\r\n`);
353
+ setTimeout(() => connectWS(), 3000);
200
354
  };
201
355
 
202
356
  ws.onerror = () => {
@@ -209,7 +363,7 @@ function sendCommand(cmd) {
209
363
  if (ws && ws.readyState === WebSocket.OPEN) {
210
364
  ws.send(JSON.stringify({ type: 'command', data: cmd }));
211
365
  } else {
212
- term.write('\x1b[38;2;233;69;96m ✗ Not connected\x1b[0m\r\n');
366
+ term.write(`${A.red} ✗ Not connected${A.r}\r\n`);
213
367
  writePrompt();
214
368
  }
215
369
  }
@@ -219,28 +373,20 @@ function writePrompt() {
219
373
  }
220
374
 
221
375
  function replaceInput(text) {
222
- // Clear current input
223
376
  const clearLen = currentLine.length;
224
- for (let i = 0; i < clearLen; i++) {
225
- term.write('\b \b');
226
- }
377
+ for (let i = 0; i < clearLen; i++) term.write('\b \b');
227
378
  currentLine = text;
228
379
  term.write(text);
229
380
  }
230
381
 
231
382
  function autocomplete() {
232
383
  if (!currentLine) return;
233
-
234
384
  const matches = COMMANDS.filter((c) => c.startsWith(currentLine.toLowerCase()));
235
-
236
385
  if (matches.length === 1) {
237
386
  replaceInput(matches[0]);
238
387
  } else if (matches.length > 1) {
239
- // Show options
240
388
  term.write('\r\n');
241
- term.write(
242
- matches.map((m) => ` \x1b[38;2;102;102;102m${m}\x1b[0m`).join(' ')
243
- );
389
+ term.write(matches.map((m) => ` ${A.dim}${m}${A.r}`).join(' '));
244
390
  term.write('\r\n');
245
391
  writePrompt();
246
392
  term.write(currentLine);
@@ -250,13 +396,8 @@ function autocomplete() {
250
396
  function updateStatus(isConnected) {
251
397
  const dot = document.querySelector('.status-dot');
252
398
  const text = document.querySelector('.status-text');
253
- if (dot) {
254
- dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
255
- }
256
- if (text) {
257
- text.textContent = isConnected ? 'Connected' : 'Disconnected';
258
- }
399
+ if (dot) dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
400
+ if (text) text.textContent = isConnected ? 'Connected' : 'Disconnected';
259
401
  }
260
402
 
261
- // Boot
262
403
  document.addEventListener('DOMContentLoaded', init);