@darksol/terminal 0.4.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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": {
@@ -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
  */
@@ -80,6 +142,12 @@ export async function handleCommand(cmd, ws) {
80
142
  case 'ask':
81
143
  case 'chat':
82
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);
83
151
  default: {
84
152
  // Fuzzy: if it looks like natural language, route to AI
85
153
  const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca)\b/i;
@@ -649,6 +717,9 @@ COMMAND REFERENCE:
649
717
  enriched += `\n\n[Live market data: ${priceData.join(', ')}]`;
650
718
  }
651
719
 
720
+ // Log user message
721
+ logChat('user', input);
722
+
652
723
  // Send to LLM
653
724
  ws.sendLine(` ${ANSI.dim}Thinking...${ANSI.reset}`);
654
725
 
@@ -656,6 +727,9 @@ COMMAND REFERENCE:
656
727
  const result = await engine.chat(enriched);
657
728
  const usage = engine.getUsage();
658
729
 
730
+ // Log AI response
731
+ logChat('assistant', result.content);
732
+
659
733
  // Display response with formatting
660
734
  ws.sendLine('');
661
735
  ws.sendLine(`${ANSI.gold} ◆ DARKSOL AI${ANSI.reset}`);
@@ -690,6 +764,130 @@ COMMAND REFERENCE:
690
764
  return {};
691
765
  }
692
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
+
693
891
  // ══════════════════════════════════════════════════
694
892
  // SEND / RECEIVE (web shell — info only, actual sends require CLI)
695
893
  // ══════════════════════════════════════════════════
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,6 +89,13 @@ 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());