@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 +1 -1
- package/src/web/commands.js +199 -1
- package/src/web/server.js +8 -1
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
|
*/
|
|
@@ -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());
|