@darksol/terminal 0.4.8 → 0.4.10
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/README.md +45 -14
- package/package.json +1 -1
- package/src/web/commands.js +87 -3
- package/src/web/server.js +21 -0
- package/src/web/terminal.js +64 -3
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
|
|
|
15
15
|
[](https://opensource.org/licenses/MIT)
|
|
16
16
|
[](https://nodejs.org/)
|
|
17
17
|
|
|
18
|
+
- Changelog: `CHANGELOG.md`
|
|
19
|
+
|
|
18
20
|
## Install
|
|
19
21
|
|
|
20
22
|
```bash
|
|
@@ -34,6 +36,10 @@ darksol wallet create main
|
|
|
34
36
|
darksol wallet balance
|
|
35
37
|
darksol portfolio
|
|
36
38
|
|
|
39
|
+
# Send / receive
|
|
40
|
+
darksol receive
|
|
41
|
+
darksol send --to 0xabc... --amount 10 --token USDC
|
|
42
|
+
|
|
37
43
|
# Token prices & live monitoring
|
|
38
44
|
darksol price ETH AERO VIRTUAL
|
|
39
45
|
darksol watch AERO --above 2.0
|
|
@@ -58,34 +64,56 @@ darksol serve
|
|
|
58
64
|
darksol agent start main
|
|
59
65
|
```
|
|
60
66
|
|
|
67
|
+
## `darksol serve` (Web Terminal UX)
|
|
68
|
+
|
|
69
|
+
`darksol serve` now supports an interactive keyboard-driven UI:
|
|
70
|
+
|
|
71
|
+
- Arrow-key menus (`↑/↓` + `Enter`) for wallet/config flows
|
|
72
|
+
- Interactive wallet picker + wallet action menu (receive/send/portfolio/history/switch chain)
|
|
73
|
+
- AI connection check at startup (shows ready/not configured)
|
|
74
|
+
- Interactive key setup from web terminal:
|
|
75
|
+
- `keys` → select provider → paste key/host directly
|
|
76
|
+
- masked input for API keys, plain input for Ollama URL
|
|
77
|
+
- Local chat memory logs at `~/.darksol/chat-logs/YYYY-MM-DD.jsonl`
|
|
78
|
+
- Natural language fuzzy routing to AI for non-command prompts
|
|
79
|
+
|
|
80
|
+
Useful web-shell commands:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
keys # provider status + interactive add/update
|
|
84
|
+
wallet # interactive wallet picker and actions
|
|
85
|
+
config # interactive config menu
|
|
86
|
+
logs 20 # show recent AI chat log lines
|
|
87
|
+
ai <prompt> # chat with trading assistant
|
|
88
|
+
```
|
|
89
|
+
|
|
61
90
|
## Modules
|
|
62
91
|
|
|
63
92
|
| Module | Description | Pricing |
|
|
64
93
|
|--------|-------------|---------|
|
|
65
|
-
| `wallet` | Create
|
|
94
|
+
| `wallet` | Create/import/manage encrypted EVM wallets | Free |
|
|
95
|
+
| `send` | Send ETH or ERC-20 tokens | Gas only |
|
|
96
|
+
| `receive` | Show receive address + chain safety hints | Free |
|
|
66
97
|
| `trade` | Swap (Uniswap V3), snipe (V2), token trading | Gas only |
|
|
67
98
|
| `dca` | Dollar-cost averaging engine | Gas only |
|
|
68
|
-
| `ai` | LLM-powered trading assistant &
|
|
99
|
+
| `ai` | LLM-powered trading assistant & intent execution | Provider dependent |
|
|
69
100
|
| `agent` | Secure agent signer (PK-isolated proxy) | Free |
|
|
70
|
-
| `keys` | API key vault (LLMs
|
|
101
|
+
| `keys` | Encrypted API key vault (LLMs/data/RPCs) | Free |
|
|
71
102
|
| `script` | Execution scripts & automated strategies | Free |
|
|
72
103
|
| `skills` | Agent skill directory & installer | Free |
|
|
73
|
-
| `market` | Market intel, top movers, token analysis | x402 micropayments |
|
|
74
|
-
| `oracle` | On-chain random number oracle | $0.05–$0.25 |
|
|
75
|
-
| `casino` | The Clawsino — on-chain betting | $1 flat bets |
|
|
76
104
|
| `portfolio` | Multi-chain balance view (5 EVM chains) | Free |
|
|
105
|
+
| `history` | Transaction history via block explorers | Free |
|
|
77
106
|
| `gas` | Gas prices & cost estimates | Free |
|
|
78
107
|
| `price` | Quick token price check (DexScreener) | Free |
|
|
79
108
|
| `watch` | Live price monitoring with alerts | Free |
|
|
80
|
-
| `
|
|
109
|
+
| `market` | Market intel, top movers, token analysis | x402 micropayments |
|
|
81
110
|
| `mail` | AgentMail — email for AI agents | Free tier |
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `cards` |
|
|
85
|
-
| `builders` | ERC-8021 builder
|
|
86
|
-
| `cards` | Crypto → prepaid Visa/MC (no KYC) | 3% markup |
|
|
87
|
-
| `builders` | ERC-8021 builder leaderboard | Free |
|
|
111
|
+
| `oracle` | On-chain random number oracle | $0.05–$0.25 |
|
|
112
|
+
| `casino` | The Clawsino — on-chain betting | $1 flat bets |
|
|
113
|
+
| `cards` | Crypto → prepaid Visa/MC cards | Service fees |
|
|
114
|
+
| `builders` | ERC-8021 builder directory + leaderboard | Free |
|
|
88
115
|
| `facilitator` | x402 payment verification & settlement | Free |
|
|
116
|
+
| `serve` | Local interactive web terminal (xterm.js) | Free |
|
|
89
117
|
| `config` | Terminal configuration | Free |
|
|
90
118
|
|
|
91
119
|
---
|
|
@@ -152,9 +180,12 @@ Natural language trading powered by multi-provider LLM support.
|
|
|
152
180
|
# Interactive chat with live market data
|
|
153
181
|
darksol ai chat
|
|
154
182
|
|
|
155
|
-
# One-shot intent parsing
|
|
183
|
+
# One-shot intent parsing (+ optional execution prompt)
|
|
156
184
|
darksol ai ask "buy 0.5 ETH worth of AERO on Base"
|
|
157
185
|
|
|
186
|
+
# Parse + execute directly
|
|
187
|
+
darksol ai execute "send 10 USDC to 0x..."
|
|
188
|
+
|
|
158
189
|
# DCA strategy recommendation
|
|
159
190
|
darksol ai strategy VIRTUAL --budget 500 --timeframe "30 days"
|
|
160
191
|
|
package/package.json
CHANGED
package/src/web/commands.js
CHANGED
|
@@ -135,6 +135,33 @@ export async function handleMenuSelect(id, value, item, ws) {
|
|
|
135
135
|
ws.sendLine('');
|
|
136
136
|
return {};
|
|
137
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
|
+
|
|
138
165
|
case 'config_action':
|
|
139
166
|
if (value === 'chain') {
|
|
140
167
|
const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
|
|
@@ -159,6 +186,52 @@ export async function handleMenuSelect(id, value, item, ws) {
|
|
|
159
186
|
return {};
|
|
160
187
|
}
|
|
161
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
|
+
|
|
162
235
|
/**
|
|
163
236
|
* AI status check — shown on connection
|
|
164
237
|
*/
|
|
@@ -180,14 +253,12 @@ export function getAIStatus() {
|
|
|
180
253
|
return [
|
|
181
254
|
` ${red}○ AI not configured${reset} ${dim}— no LLM provider connected${reset}`,
|
|
182
255
|
'',
|
|
183
|
-
` ${gold}
|
|
256
|
+
` ${dim}Type ${gold}keys${dim} to set up an LLM provider, or paste directly:${reset}`,
|
|
184
257
|
` ${green}keys add openai sk-...${reset} ${dim}OpenAI (GPT-4o)${reset}`,
|
|
185
258
|
` ${green}keys add anthropic sk-ant-...${reset} ${dim}Anthropic (Claude)${reset}`,
|
|
186
259
|
` ${green}keys add openrouter sk-or-...${reset} ${dim}OpenRouter (any model)${reset}`,
|
|
187
260
|
` ${green}keys add ollama http://...${reset} ${dim}Ollama (free, local)${reset}`,
|
|
188
261
|
'',
|
|
189
|
-
` ${dim}Or run the full setup wizard: ${gold}darksol setup${reset}`,
|
|
190
|
-
'',
|
|
191
262
|
].join('\r\n');
|
|
192
263
|
}
|
|
193
264
|
|
|
@@ -1014,6 +1085,19 @@ async function cmdKeys(args, ws) {
|
|
|
1014
1085
|
ws.sendLine(` ${ANSI.green}keys add ollama http://...${ANSI.reset} ${ANSI.dim}Add Ollama host${ANSI.reset}`);
|
|
1015
1086
|
ws.sendLine('');
|
|
1016
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
|
+
|
|
1017
1101
|
const dataProviders = ['coingecko', 'dexscreener', 'alchemy', 'agentmail'];
|
|
1018
1102
|
ws.sendLine(` ${ANSI.gold}Other Services:${ANSI.reset}`);
|
|
1019
1103
|
for (const p of dataProviders) {
|
package/src/web/server.js
CHANGED
|
@@ -100,6 +100,27 @@ 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
|
+
|
|
103
124
|
if (msg.type === 'menu_select') {
|
|
104
125
|
// User selected something from an interactive menu
|
|
105
126
|
try {
|
package/src/web/terminal.js
CHANGED
|
@@ -34,6 +34,13 @@ let menuIndex = 0;
|
|
|
34
34
|
let menuId = '';
|
|
35
35
|
let menuTitle = '';
|
|
36
36
|
|
|
37
|
+
// ── PROMPT STATE (text input) ─────────────────
|
|
38
|
+
let promptActive = false;
|
|
39
|
+
let promptId = '';
|
|
40
|
+
let promptMeta = {};
|
|
41
|
+
let promptInput = '';
|
|
42
|
+
let promptMask = false;
|
|
43
|
+
|
|
37
44
|
async function init() {
|
|
38
45
|
term = new Terminal({
|
|
39
46
|
theme: {
|
|
@@ -85,6 +92,48 @@ async function init() {
|
|
|
85
92
|
const code = domEvent.keyCode;
|
|
86
93
|
const ctrl = domEvent.ctrlKey;
|
|
87
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
|
+
|
|
88
137
|
// ── MENU MODE ──
|
|
89
138
|
if (menuActive) {
|
|
90
139
|
if (code === 38) { // Up
|
|
@@ -188,8 +237,13 @@ async function init() {
|
|
|
188
237
|
if (menuActive) return;
|
|
189
238
|
if (data.length > 1 && !data.startsWith('\x1b')) {
|
|
190
239
|
const clean = data.replace(/[\r\n]/g, '');
|
|
191
|
-
|
|
192
|
-
|
|
240
|
+
if (promptActive) {
|
|
241
|
+
promptInput += clean;
|
|
242
|
+
term.write(promptMask ? '●'.repeat(clean.length) : clean);
|
|
243
|
+
} else {
|
|
244
|
+
currentLine += clean;
|
|
245
|
+
term.write(clean);
|
|
246
|
+
}
|
|
193
247
|
}
|
|
194
248
|
});
|
|
195
249
|
|
|
@@ -277,8 +331,15 @@ function connectWS() {
|
|
|
277
331
|
term.clear();
|
|
278
332
|
writePrompt();
|
|
279
333
|
} else if (msg.type === 'menu') {
|
|
280
|
-
// Server is requesting user to pick from a menu
|
|
281
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} `);
|
|
282
343
|
}
|
|
283
344
|
} catch {
|
|
284
345
|
term.write(event.data);
|