@darksol/terminal 0.4.4 → 0.4.6
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/cli.js +40 -5
- package/src/llm/intent.js +141 -27
- package/src/web/commands.js +266 -2
- package/src/web/server.js +28 -7
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -516,18 +516,53 @@ export function cli(argv) {
|
|
|
516
516
|
.description('One-shot AI query')
|
|
517
517
|
.option('-p, --provider <name>', 'LLM provider')
|
|
518
518
|
.option('-m, --model <model>', 'Model name')
|
|
519
|
+
.option('-x, --execute', 'Auto-execute if confidence > 60%')
|
|
519
520
|
.action(async (promptParts, opts) => {
|
|
520
521
|
const prompt = promptParts.join(' ');
|
|
521
522
|
const result = await parseIntent(prompt, opts);
|
|
522
523
|
if (result.action !== 'error' && result.action !== 'unknown') {
|
|
523
524
|
showSection('PARSED INTENT');
|
|
524
|
-
|
|
525
|
-
.filter(([k]) => !['raw', 'model'].includes(k))
|
|
526
|
-
.map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)])
|
|
527
|
-
);
|
|
525
|
+
const displayEntries = Object.entries(result)
|
|
526
|
+
.filter(([k]) => !['raw', 'model', 'reasoning'].includes(k))
|
|
527
|
+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)]);
|
|
528
|
+
kvDisplay(displayEntries);
|
|
529
|
+
|
|
530
|
+
if (result.reasoning) {
|
|
531
|
+
console.log('');
|
|
532
|
+
info(result.reasoning);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (result.warnings?.length > 0) {
|
|
536
|
+
result.warnings.forEach(w => warn(w));
|
|
537
|
+
}
|
|
538
|
+
|
|
528
539
|
if (result.command) {
|
|
529
540
|
console.log('');
|
|
530
|
-
info(`
|
|
541
|
+
info(`Command: ${theme.gold(result.command)}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Offer to execute actionable intents
|
|
545
|
+
const actionable = ['swap', 'send', 'transfer', 'snipe', 'dca', 'price', 'balance', 'gas', 'analyze'];
|
|
546
|
+
if (actionable.includes(result.action)) {
|
|
547
|
+
if (opts.execute && result.confidence >= 0.6) {
|
|
548
|
+
console.log('');
|
|
549
|
+
await executeIntent(result, {});
|
|
550
|
+
} else if (!opts.execute) {
|
|
551
|
+
console.log('');
|
|
552
|
+
const inquirer = (await import('inquirer')).default;
|
|
553
|
+
const { run } = await inquirer.prompt([{
|
|
554
|
+
type: 'confirm',
|
|
555
|
+
name: 'run',
|
|
556
|
+
message: theme.gold('Execute this?'),
|
|
557
|
+
default: result.confidence >= 0.7,
|
|
558
|
+
}]);
|
|
559
|
+
if (run) await executeIntent(result, {});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
if (result.raw) {
|
|
564
|
+
console.log('');
|
|
565
|
+
console.log(theme.dim(' ') + result.raw);
|
|
531
566
|
}
|
|
532
567
|
}
|
|
533
568
|
});
|
package/src/llm/intent.js
CHANGED
|
@@ -9,42 +9,70 @@ import { showSection } from '../ui/banner.js';
|
|
|
9
9
|
// INTENT SYSTEM PROMPT
|
|
10
10
|
// ──────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
const INTENT_SYSTEM_PROMPT = `You are DARKSOL Terminal's trading AI assistant. You help users execute trades, analyze markets, manage DCA strategies, and navigate the DARKSOL ecosystem.
|
|
12
|
+
const INTENT_SYSTEM_PROMPT = `You are DARKSOL Terminal's trading AI assistant. You help users execute trades, send/receive tokens, analyze markets, manage DCA strategies, and navigate the DARKSOL ecosystem.
|
|
13
13
|
|
|
14
14
|
CAPABILITIES:
|
|
15
|
-
- Parse natural language
|
|
16
|
-
- Analyze token prices, liquidity, and market conditions
|
|
15
|
+
- Parse natural language into structured trade/transfer commands
|
|
16
|
+
- Analyze token prices, liquidity, and market conditions
|
|
17
17
|
- Suggest DCA strategies based on user goals
|
|
18
18
|
- Explain transaction results and gas costs
|
|
19
19
|
- Warn about risks (low liquidity, high slippage, unverified contracts)
|
|
20
20
|
|
|
21
21
|
SUPPORTED CHAINS: Base (default), Ethereum, Polygon, Arbitrum, Optimism
|
|
22
|
-
|
|
23
|
-
RESPONSE RULES:
|
|
24
|
-
- Be concise and direct
|
|
25
|
-
- Always include risk warnings for trades
|
|
26
|
-
- When parsing trade intent, output structured JSON
|
|
27
|
-
- Never reveal private keys or sensitive wallet info
|
|
28
|
-
- If uncertain about a token, say so
|
|
29
|
-
- Use plain numbers, avoid scientific notation
|
|
22
|
+
KNOWN TOKENS: ETH, USDC, USDT, DAI, WETH, AERO, VIRTUAL, ARB, OP, WMATIC
|
|
30
23
|
|
|
31
24
|
USER CONTEXT:
|
|
32
25
|
- Active chain: {{chain}}
|
|
33
26
|
- Active wallet: {{wallet}}
|
|
34
27
|
- Slippage setting: {{slippage}}%
|
|
35
28
|
|
|
36
|
-
|
|
29
|
+
RESPONSE RULES:
|
|
30
|
+
- Be concise and direct
|
|
31
|
+
- Always include risk warnings for trades
|
|
32
|
+
- When parsing trade/transfer intent, output structured JSON
|
|
33
|
+
- Never reveal private keys or sensitive wallet info
|
|
34
|
+
- If uncertain about a token, say so — don't guess contract addresses
|
|
35
|
+
- Use plain numbers, avoid scientific notation
|
|
36
|
+
- For ambiguous amounts, ask for clarification (confidence < 0.5)
|
|
37
|
+
|
|
38
|
+
ACTIONS (use the most specific one):
|
|
39
|
+
- "swap" — exchange one token for another (e.g. "swap ETH to USDC", "buy VIRTUAL with 0.1 ETH")
|
|
40
|
+
- "send" — transfer tokens to an address (e.g. "send 10 USDC to 0x...", "transfer 0.5 ETH to vitalik.eth")
|
|
41
|
+
- "snipe" — fast-buy a new/low-liquidity token with ETH
|
|
42
|
+
- "dca" — set up recurring buys (e.g. "DCA $100 into ETH over 30 days")
|
|
43
|
+
- "price" — check current price (e.g. "price of AERO", "how much is VIRTUAL")
|
|
44
|
+
- "balance" — check wallet balance
|
|
45
|
+
- "info" — general question about a token or protocol
|
|
46
|
+
- "analyze" — deep analysis of a token
|
|
47
|
+
- "gas" — gas price check
|
|
48
|
+
- "unknown" — can't determine what the user wants
|
|
49
|
+
|
|
50
|
+
When parsing, respond with ONLY valid JSON:
|
|
37
51
|
{
|
|
38
|
-
"action": "swap|snipe|dca|
|
|
39
|
-
"tokenIn": "symbol or address",
|
|
40
|
-
"tokenOut": "symbol or address",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
52
|
+
"action": "swap|send|snipe|dca|price|balance|info|analyze|gas|unknown",
|
|
53
|
+
"tokenIn": "symbol or address (for swaps)",
|
|
54
|
+
"tokenOut": "symbol or address (for swaps)",
|
|
55
|
+
"token": "symbol (for send/price/analyze)",
|
|
56
|
+
"amount": "number as string",
|
|
57
|
+
"to": "recipient address (for send)",
|
|
58
|
+
"chain": "chain name if specified, null if not",
|
|
59
|
+
"interval": "for DCA: 1h, 4h, 1d, etc.",
|
|
60
|
+
"orders": "for DCA: number of orders",
|
|
61
|
+
"confidence": 0.0-1.0,
|
|
62
|
+
"reasoning": "brief explanation of interpretation",
|
|
45
63
|
"warnings": ["array of risk warnings"],
|
|
46
|
-
"command": "the CLI command to
|
|
47
|
-
}
|
|
64
|
+
"command": "the exact darksol CLI command to run"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
COMMAND MAPPING:
|
|
68
|
+
- swap → darksol trade swap -i <tokenIn> -o <tokenOut> -a <amount>
|
|
69
|
+
- send → darksol send --to <address> --amount <amount> --token <token>
|
|
70
|
+
- snipe → darksol trade snipe <address> <ethAmount>
|
|
71
|
+
- dca → darksol dca create -t <token> -a <amount> -i <interval> -n <orders>
|
|
72
|
+
- price → darksol price <token>
|
|
73
|
+
- balance → darksol wallet balance
|
|
74
|
+
- gas → darksol gas <chain>
|
|
75
|
+
- analyze → darksol ai analyze <token>`;
|
|
48
76
|
|
|
49
77
|
// ──────────────────────────────────────────────────
|
|
50
78
|
// INTENT PARSER
|
|
@@ -126,7 +154,8 @@ export async function parseIntent(input, opts = {}) {
|
|
|
126
154
|
export async function startChat(opts = {}) {
|
|
127
155
|
showSection('DARKSOL AI — TRADING ASSISTANT');
|
|
128
156
|
console.log(theme.dim(' Natural language trading. Type "exit" to quit.'));
|
|
129
|
-
console.log(theme.dim('
|
|
157
|
+
console.log(theme.dim(' Try: "swap 0.1 ETH to USDC", "send 5 USDC to 0x...", "price of AERO"'));
|
|
158
|
+
console.log(theme.dim(' Actions auto-detected — you\'ll be asked to confirm before execution.'));
|
|
130
159
|
console.log('');
|
|
131
160
|
|
|
132
161
|
const spin = spinner('Initializing AI...').start();
|
|
@@ -187,7 +216,31 @@ export async function startChat(opts = {}) {
|
|
|
187
216
|
enriched += `\n\n[Live data: ${priceData.join(', ')}]`;
|
|
188
217
|
}
|
|
189
218
|
|
|
190
|
-
|
|
219
|
+
// Try to detect actionable intent
|
|
220
|
+
const actionKeywords = /\b(swap|send|transfer|buy|sell|snipe|dca|price|balance|gas)\b/i;
|
|
221
|
+
const isActionable = actionKeywords.test(input);
|
|
222
|
+
|
|
223
|
+
let result;
|
|
224
|
+
let parsedIntent = null;
|
|
225
|
+
|
|
226
|
+
if (isActionable) {
|
|
227
|
+
// Use JSON mode to get structured intent
|
|
228
|
+
const intentResult = await llm.json(
|
|
229
|
+
`Parse this as a trading/transfer instruction:\n\n"${enriched}"`,
|
|
230
|
+
{ ephemeral: true }
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (intentResult.parsed && intentResult.parsed.action && intentResult.parsed.action !== 'unknown') {
|
|
234
|
+
parsedIntent = intentResult.parsed;
|
|
235
|
+
// Also get a human-readable response
|
|
236
|
+
result = await llm.chat(enriched);
|
|
237
|
+
} else {
|
|
238
|
+
result = await llm.chat(enriched);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
result = await llm.chat(enriched);
|
|
242
|
+
}
|
|
243
|
+
|
|
191
244
|
spin2.succeed('');
|
|
192
245
|
|
|
193
246
|
// Display response
|
|
@@ -199,6 +252,41 @@ export async function startChat(opts = {}) {
|
|
|
199
252
|
}
|
|
200
253
|
console.log('');
|
|
201
254
|
|
|
255
|
+
// If actionable intent was detected, offer to execute
|
|
256
|
+
if (parsedIntent) {
|
|
257
|
+
const execActions = ['swap', 'send', 'transfer', 'snipe', 'dca', 'price', 'balance', 'gas'];
|
|
258
|
+
if (execActions.includes(parsedIntent.action)) {
|
|
259
|
+
const displayPairs = [];
|
|
260
|
+
if (parsedIntent.action) displayPairs.push(['Action', parsedIntent.action]);
|
|
261
|
+
if (parsedIntent.tokenIn) displayPairs.push(['From', parsedIntent.tokenIn]);
|
|
262
|
+
if (parsedIntent.tokenOut) displayPairs.push(['To token', parsedIntent.tokenOut]);
|
|
263
|
+
if (parsedIntent.token) displayPairs.push(['Token', parsedIntent.token]);
|
|
264
|
+
if (parsedIntent.amount) displayPairs.push(['Amount', parsedIntent.amount]);
|
|
265
|
+
if (parsedIntent.to) displayPairs.push(['Recipient', parsedIntent.to]);
|
|
266
|
+
if (parsedIntent.confidence) displayPairs.push(['Confidence', `${(parsedIntent.confidence * 100).toFixed(0)}%`]);
|
|
267
|
+
|
|
268
|
+
if (displayPairs.length > 1) {
|
|
269
|
+
showSection('DETECTED INTENT');
|
|
270
|
+
kvDisplay(displayPairs);
|
|
271
|
+
if (parsedIntent.warnings?.length > 0) {
|
|
272
|
+
parsedIntent.warnings.forEach(w => warn(w));
|
|
273
|
+
}
|
|
274
|
+
console.log('');
|
|
275
|
+
|
|
276
|
+
const { execute } = await inquirer.prompt([{
|
|
277
|
+
type: 'confirm',
|
|
278
|
+
name: 'execute',
|
|
279
|
+
message: theme.gold(`Execute ${parsedIntent.action}?`),
|
|
280
|
+
default: false,
|
|
281
|
+
}]);
|
|
282
|
+
|
|
283
|
+
if (execute) {
|
|
284
|
+
await executeIntent(parsedIntent, {});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
202
290
|
} catch (err) {
|
|
203
291
|
spin2.fail('Error');
|
|
204
292
|
error(err.message);
|
|
@@ -402,11 +490,37 @@ export async function executeIntent(intent, opts = {}) {
|
|
|
402
490
|
});
|
|
403
491
|
}
|
|
404
492
|
|
|
493
|
+
case 'send':
|
|
405
494
|
case 'transfer': {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
495
|
+
const { sendFunds } = await import('../wallet/manager.js');
|
|
496
|
+
return await sendFunds({
|
|
497
|
+
to: intent.to,
|
|
498
|
+
amount: intent.amount,
|
|
499
|
+
token: intent.token || intent.tokenIn || 'ETH',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case 'price': {
|
|
504
|
+
const token = intent.token || intent.tokenOut || intent.tokenIn;
|
|
505
|
+
if (token) {
|
|
506
|
+
const { checkPrices } = await import('../services/watch.js');
|
|
507
|
+
await checkPrices([token]);
|
|
508
|
+
return { success: true, action: 'price' };
|
|
509
|
+
}
|
|
510
|
+
info('No token specified');
|
|
511
|
+
return { success: false, reason: 'no token specified' };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
case 'balance': {
|
|
515
|
+
const { getBalance } = await import('../wallet/manager.js');
|
|
516
|
+
await getBalance();
|
|
517
|
+
return { success: true, action: 'balance' };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case 'gas': {
|
|
521
|
+
const { showGas } = await import('../services/gas.js');
|
|
522
|
+
await showGas(intent.chain || getConfig('chain') || 'base');
|
|
523
|
+
return { success: true, action: 'gas' };
|
|
410
524
|
}
|
|
411
525
|
|
|
412
526
|
case 'info':
|
package/src/web/commands.js
CHANGED
|
@@ -72,10 +72,24 @@ export async function handleCommand(cmd, ws) {
|
|
|
72
72
|
return await cmdCasino(args, ws);
|
|
73
73
|
case 'facilitator':
|
|
74
74
|
return await cmdFacilitator(args, ws);
|
|
75
|
-
|
|
75
|
+
case 'send':
|
|
76
|
+
return await cmdSend(args, ws);
|
|
77
|
+
case 'receive':
|
|
78
|
+
return await cmdReceive(ws);
|
|
79
|
+
case 'ai':
|
|
80
|
+
case 'ask':
|
|
81
|
+
case 'chat':
|
|
82
|
+
return await cmdAI(args, ws);
|
|
83
|
+
default: {
|
|
84
|
+
// Fuzzy: if it looks like natural language, route to AI
|
|
85
|
+
const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca)\b/i;
|
|
86
|
+
if (nlKeywords.test(cmd)) {
|
|
87
|
+
return await cmdAI(cmd.split(/\s+/), ws);
|
|
88
|
+
}
|
|
76
89
|
return {
|
|
77
|
-
output: `\r\n ${ANSI.red}✗ Unknown command: ${cmd}${ANSI.reset}\r\n ${ANSI.dim}Type ${ANSI.gold}help${ANSI.dim} for
|
|
90
|
+
output: `\r\n ${ANSI.red}✗ Unknown command: ${cmd}${ANSI.reset}\r\n ${ANSI.dim}Type ${ANSI.gold}help${ANSI.dim} for commands, or ${ANSI.gold}ai <question>${ANSI.dim} to chat.${ANSI.reset}\r\n\r\n`,
|
|
78
91
|
};
|
|
92
|
+
}
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
|
|
@@ -504,6 +518,256 @@ async function cmdConfig(ws) {
|
|
|
504
518
|
return {};
|
|
505
519
|
}
|
|
506
520
|
|
|
521
|
+
// ══════════════════════════════════════════════════
|
|
522
|
+
// AI CHAT — LLM-powered assistant in the web shell
|
|
523
|
+
// ══════════════════════════════════════════════════
|
|
524
|
+
|
|
525
|
+
// Persistent chat engine per WebSocket connection
|
|
526
|
+
const chatEngines = new WeakMap();
|
|
527
|
+
|
|
528
|
+
async function cmdAI(args, ws) {
|
|
529
|
+
const input = args.join(' ').trim();
|
|
530
|
+
|
|
531
|
+
if (!input || input === 'help') {
|
|
532
|
+
ws.sendLine(`${ANSI.gold} ◆ AI TRADING ASSISTANT${ANSI.reset}`);
|
|
533
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
534
|
+
ws.sendLine('');
|
|
535
|
+
ws.sendLine(` ${ANSI.white}Natural language trading — just describe what you want.${ANSI.reset}`);
|
|
536
|
+
ws.sendLine('');
|
|
537
|
+
ws.sendLine(` ${ANSI.darkGold}Usage:${ANSI.reset}`);
|
|
538
|
+
ws.sendLine(` ${ANSI.gold}ai swap 0.1 ETH to USDC${ANSI.reset}`);
|
|
539
|
+
ws.sendLine(` ${ANSI.gold}ai what's the price of AERO?${ANSI.reset}`);
|
|
540
|
+
ws.sendLine(` ${ANSI.gold}ai analyze VIRTUAL on base${ANSI.reset}`);
|
|
541
|
+
ws.sendLine(` ${ANSI.gold}ai should I DCA into ETH?${ANSI.reset}`);
|
|
542
|
+
ws.sendLine(` ${ANSI.gold}ai send 10 USDC to 0x1234...${ANSI.reset}`);
|
|
543
|
+
ws.sendLine(` ${ANSI.gold}ai gas on base${ANSI.reset}`);
|
|
544
|
+
ws.sendLine('');
|
|
545
|
+
ws.sendLine(` ${ANSI.dim}Conversation history is kept for the session.${ANSI.reset}`);
|
|
546
|
+
ws.sendLine(` ${ANSI.dim}Type ${ANSI.gold}ai clear${ANSI.dim} to reset history.${ANSI.reset}`);
|
|
547
|
+
ws.sendLine('');
|
|
548
|
+
return {};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (input === 'clear' || input === 'reset') {
|
|
552
|
+
if (chatEngines.has(ws)) {
|
|
553
|
+
chatEngines.get(ws).clearHistory();
|
|
554
|
+
}
|
|
555
|
+
ws.sendLine(` ${ANSI.green}✓ Chat history cleared${ANSI.reset}`);
|
|
556
|
+
ws.sendLine('');
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (input === 'status') {
|
|
561
|
+
const engine = chatEngines.get(ws);
|
|
562
|
+
if (engine) {
|
|
563
|
+
const usage = engine.getUsage();
|
|
564
|
+
ws.sendLine(`${ANSI.gold} ◆ AI STATUS${ANSI.reset}`);
|
|
565
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
566
|
+
ws.sendLine(` ${ANSI.darkGold}Provider${ANSI.reset} ${ANSI.white}${usage.provider}${ANSI.reset}`);
|
|
567
|
+
ws.sendLine(` ${ANSI.darkGold}Model${ANSI.reset} ${ANSI.white}${usage.model}${ANSI.reset}`);
|
|
568
|
+
ws.sendLine(` ${ANSI.darkGold}Messages${ANSI.reset} ${ANSI.white}${usage.calls}${ANSI.reset}`);
|
|
569
|
+
ws.sendLine(` ${ANSI.darkGold}Tokens${ANSI.reset} ${ANSI.white}${usage.totalTokens}${ANSI.reset}`);
|
|
570
|
+
ws.sendLine('');
|
|
571
|
+
} else {
|
|
572
|
+
ws.sendLine(` ${ANSI.dim}No active AI session${ANSI.reset}`);
|
|
573
|
+
ws.sendLine('');
|
|
574
|
+
}
|
|
575
|
+
return {};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Initialize or retrieve the LLM engine
|
|
579
|
+
let engine = chatEngines.get(ws);
|
|
580
|
+
if (!engine) {
|
|
581
|
+
try {
|
|
582
|
+
const { createLLM } = await import('../llm/engine.js');
|
|
583
|
+
engine = await createLLM({});
|
|
584
|
+
|
|
585
|
+
const chain = getConfig('chain') || 'base';
|
|
586
|
+
const wallet = getConfig('activeWallet') || '(not set)';
|
|
587
|
+
const slippage = getConfig('slippage') || 0.5;
|
|
588
|
+
|
|
589
|
+
engine.setSystemPrompt(`You are DARKSOL Terminal's AI trading assistant running in a web terminal.
|
|
590
|
+
|
|
591
|
+
You help users with:
|
|
592
|
+
- Token swaps, sends, and transfers
|
|
593
|
+
- Price checks and market analysis
|
|
594
|
+
- DCA strategy recommendations
|
|
595
|
+
- Gas estimates and chain info
|
|
596
|
+
- Portfolio analysis
|
|
597
|
+
- General crypto/DeFi questions
|
|
598
|
+
|
|
599
|
+
USER CONTEXT:
|
|
600
|
+
- Active chain: ${chain}
|
|
601
|
+
- Active wallet: ${wallet}
|
|
602
|
+
- Slippage: ${slippage}%
|
|
603
|
+
- Supported chains: Base (default), Ethereum, Polygon, Arbitrum, Optimism
|
|
604
|
+
|
|
605
|
+
RULES:
|
|
606
|
+
- Be concise — this is a terminal, not a blog
|
|
607
|
+
- Use short paragraphs, bullet points where helpful
|
|
608
|
+
- Include risk warnings for any trade suggestions
|
|
609
|
+
- Never reveal private keys or sensitive info
|
|
610
|
+
- When suggesting trades, give the exact darksol CLI command
|
|
611
|
+
- If you detect an actionable intent (swap, send, price, etc), include the command at the end
|
|
612
|
+
|
|
613
|
+
COMMAND REFERENCE:
|
|
614
|
+
- darksol trade swap -i ETH -o USDC -a 0.1
|
|
615
|
+
- darksol send --to 0x... --amount 0.1 --token ETH
|
|
616
|
+
- darksol price ETH AERO VIRTUAL
|
|
617
|
+
- darksol gas base
|
|
618
|
+
- darksol wallet balance
|
|
619
|
+
- darksol portfolio
|
|
620
|
+
- darksol dca create -t ETH -a 0.01 -i 1h -n 24
|
|
621
|
+
- darksol ai analyze <token>`);
|
|
622
|
+
|
|
623
|
+
chatEngines.set(ws, engine);
|
|
624
|
+
ws.sendLine(` ${ANSI.green}● AI connected${ANSI.reset} ${ANSI.dim}(${engine.provider}/${engine.model})${ANSI.reset}`);
|
|
625
|
+
ws.sendLine('');
|
|
626
|
+
} catch (err) {
|
|
627
|
+
ws.sendLine(` ${ANSI.red}✗ AI initialization failed: ${err.message}${ANSI.reset}`);
|
|
628
|
+
ws.sendLine(` ${ANSI.dim}Configure an API key: darksol keys add openai${ANSI.reset}`);
|
|
629
|
+
ws.sendLine('');
|
|
630
|
+
return {};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Enrich with live price data
|
|
635
|
+
let enriched = input;
|
|
636
|
+
const tokenPattern = /\b([A-Z]{2,10})\b/g;
|
|
637
|
+
const tokens = [...new Set(input.toUpperCase().match(tokenPattern) || [])];
|
|
638
|
+
const skipWords = ['ETH', 'THE', 'FOR', 'AND', 'BUY', 'SELL', 'DCA', 'SWAP', 'WHAT', 'PRICE', 'HOW', 'MUCH', 'SEND', 'SHOULD', 'CAN', 'ANALYZE', 'CHECK'];
|
|
639
|
+
|
|
640
|
+
const priceData = [];
|
|
641
|
+
for (const t of tokens.filter(t => !skipWords.includes(t)).slice(0, 3)) {
|
|
642
|
+
try {
|
|
643
|
+
const { quickPrice } = await import('../utils/helpers.js');
|
|
644
|
+
const p = await quickPrice(t);
|
|
645
|
+
if (p) priceData.push(`${p.symbol}: $${p.price} (24h: ${p.change24h}%)`);
|
|
646
|
+
} catch {}
|
|
647
|
+
}
|
|
648
|
+
if (priceData.length > 0) {
|
|
649
|
+
enriched += `\n\n[Live market data: ${priceData.join(', ')}]`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Send to LLM
|
|
653
|
+
ws.sendLine(` ${ANSI.dim}Thinking...${ANSI.reset}`);
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const result = await engine.chat(enriched);
|
|
657
|
+
const usage = engine.getUsage();
|
|
658
|
+
|
|
659
|
+
// Display response with formatting
|
|
660
|
+
ws.sendLine('');
|
|
661
|
+
ws.sendLine(`${ANSI.gold} ◆ DARKSOL AI${ANSI.reset}`);
|
|
662
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
663
|
+
|
|
664
|
+
const lines = result.content.split('\n');
|
|
665
|
+
for (const line of lines) {
|
|
666
|
+
// Highlight code blocks
|
|
667
|
+
if (line.trim().startsWith('```')) {
|
|
668
|
+
ws.sendLine(` ${ANSI.dim}${line}${ANSI.reset}`);
|
|
669
|
+
} else if (line.trim().startsWith('darksol ') || line.trim().startsWith('$ darksol')) {
|
|
670
|
+
// Highlight CLI commands
|
|
671
|
+
ws.sendLine(` ${ANSI.gold}${line}${ANSI.reset}`);
|
|
672
|
+
} else if (line.trim().startsWith('⚠') || line.trim().startsWith('Warning') || line.trim().toLowerCase().startsWith('risk')) {
|
|
673
|
+
ws.sendLine(` ${ANSI.red}${line}${ANSI.reset}`);
|
|
674
|
+
} else if (line.trim().startsWith('•') || line.trim().startsWith('-') || line.trim().startsWith('*')) {
|
|
675
|
+
ws.sendLine(` ${ANSI.white}${line}${ANSI.reset}`);
|
|
676
|
+
} else {
|
|
677
|
+
ws.sendLine(` ${line}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
ws.sendLine('');
|
|
682
|
+
ws.sendLine(` ${ANSI.dim}[${usage.calls} msgs | ${usage.totalTokens} tokens | ${engine.provider}/${engine.model}]${ANSI.reset}`);
|
|
683
|
+
ws.sendLine('');
|
|
684
|
+
|
|
685
|
+
} catch (err) {
|
|
686
|
+
ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
|
|
687
|
+
ws.sendLine('');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return {};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ══════════════════════════════════════════════════
|
|
694
|
+
// SEND / RECEIVE (web shell — info only, actual sends require CLI)
|
|
695
|
+
// ══════════════════════════════════════════════════
|
|
696
|
+
async function cmdSend(args, ws) {
|
|
697
|
+
const chain = getConfig('chain') || 'base';
|
|
698
|
+
const wallet = getConfig('activeWallet');
|
|
699
|
+
|
|
700
|
+
ws.sendLine(`${ANSI.gold} ◆ SEND TOKENS${ANSI.reset}`);
|
|
701
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
702
|
+
ws.sendLine('');
|
|
703
|
+
|
|
704
|
+
if (!wallet) {
|
|
705
|
+
ws.sendLine(` ${ANSI.red}No wallet configured.${ANSI.reset}`);
|
|
706
|
+
ws.sendLine(` Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
|
|
707
|
+
ws.sendLine('');
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
ws.sendLine(` ${ANSI.white}Send ETH or any ERC-20 token from your wallet.${ANSI.reset}`);
|
|
712
|
+
ws.sendLine('');
|
|
713
|
+
ws.sendLine(` ${ANSI.darkGold}Usage:${ANSI.reset}`);
|
|
714
|
+
ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
|
|
715
|
+
ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 50 --token USDC${ANSI.reset}`);
|
|
716
|
+
ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode — prompts for everything)${ANSI.reset}`);
|
|
717
|
+
ws.sendLine('');
|
|
718
|
+
ws.sendLine(` ${ANSI.darkGold}Features:${ANSI.reset}`);
|
|
719
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} ETH and any ERC-20 token`);
|
|
720
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Balance check before sending`);
|
|
721
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Gas estimation in preview`);
|
|
722
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} Confirmation prompt before execution`);
|
|
723
|
+
ws.sendLine(` ${ANSI.dim}•${ANSI.reset} On-chain receipt after confirmation`);
|
|
724
|
+
ws.sendLine('');
|
|
725
|
+
ws.sendLine(` ${ANSI.darkGold}Active:${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset} on ${ANSI.white}${chain}${ANSI.reset}`);
|
|
726
|
+
ws.sendLine('');
|
|
727
|
+
ws.sendLine(` ${ANSI.dim}⚠ Sending requires the CLI. Install: npm i -g @darksol/terminal${ANSI.reset}`);
|
|
728
|
+
ws.sendLine('');
|
|
729
|
+
return {};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function cmdReceive(ws) {
|
|
733
|
+
const chain = getConfig('chain') || 'base';
|
|
734
|
+
const wallet = getConfig('activeWallet');
|
|
735
|
+
|
|
736
|
+
ws.sendLine(`${ANSI.gold} ◆ RECEIVE${ANSI.reset}`);
|
|
737
|
+
ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
|
|
738
|
+
ws.sendLine('');
|
|
739
|
+
|
|
740
|
+
if (!wallet) {
|
|
741
|
+
ws.sendLine(` ${ANSI.red}No wallet configured.${ANSI.reset}`);
|
|
742
|
+
ws.sendLine(` Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
|
|
743
|
+
ws.sendLine('');
|
|
744
|
+
return {};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
const { loadWallet } = await import('../wallet/keystore.js');
|
|
749
|
+
const walletData = loadWallet(wallet);
|
|
750
|
+
const addr = walletData.address;
|
|
751
|
+
|
|
752
|
+
ws.sendLine(` ${ANSI.white}Your address:${ANSI.reset}`);
|
|
753
|
+
ws.sendLine('');
|
|
754
|
+
ws.sendLine(` ${ANSI.dim}┌${'─'.repeat(addr.length + 4)}┐${ANSI.reset}`);
|
|
755
|
+
ws.sendLine(` ${ANSI.dim}│ ${ANSI.gold}${addr}${ANSI.dim} │${ANSI.reset}`);
|
|
756
|
+
ws.sendLine(` ${ANSI.dim}└${'─'.repeat(addr.length + 4)}┘${ANSI.reset}`);
|
|
757
|
+
ws.sendLine('');
|
|
758
|
+
ws.sendLine(` ${ANSI.dim}Works on ALL EVM chains:${ANSI.reset}`);
|
|
759
|
+
ws.sendLine(` ${ANSI.dim}Base • Ethereum • Arbitrum • Optimism • Polygon${ANSI.reset}`);
|
|
760
|
+
ws.sendLine('');
|
|
761
|
+
ws.sendLine(` ${ANSI.darkGold}Active chain:${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
|
|
762
|
+
ws.sendLine(` ${ANSI.red}Make sure the sender is on the same chain!${ANSI.reset}`);
|
|
763
|
+
ws.sendLine('');
|
|
764
|
+
} catch {
|
|
765
|
+
ws.sendLine(` ${ANSI.dim}Run: darksol wallet receive${ANSI.reset}`);
|
|
766
|
+
ws.sendLine('');
|
|
767
|
+
}
|
|
768
|
+
return {};
|
|
769
|
+
}
|
|
770
|
+
|
|
507
771
|
// ══════════════════════════════════════════════════
|
|
508
772
|
// HELPERS
|
|
509
773
|
// ══════════════════════════════════════════════════
|
package/src/web/server.js
CHANGED
|
@@ -5,6 +5,9 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
6
|
import open from 'open';
|
|
7
7
|
import { theme } from '../ui/theme.js';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { version: PKG_VERSION } = require('../../package.json');
|
|
8
11
|
|
|
9
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
13
|
const __dirname = dirname(__filename);
|
|
@@ -46,7 +49,7 @@ export async function startWebShell(opts = {}) {
|
|
|
46
49
|
res.end(js);
|
|
47
50
|
} else if (pathname === '/health') {
|
|
48
51
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
49
|
-
res.end(JSON.stringify({ status: 'ok', version:
|
|
52
|
+
res.end(JSON.stringify({ status: 'ok', version: PKG_VERSION }));
|
|
50
53
|
} else {
|
|
51
54
|
res.writeHead(404);
|
|
52
55
|
res.end('Not found');
|
|
@@ -194,12 +197,13 @@ function getBanner() {
|
|
|
194
197
|
'',
|
|
195
198
|
`${dim} ╔══════════════════════════════════════════════════════════╗${reset}`,
|
|
196
199
|
`${dim} ║${reset} ${gold}${white} DARKSOL TERMINAL${reset}${dim} — ${reset}${dim}Ghost in the machine with teeth${reset}${dim} ║${reset}`,
|
|
197
|
-
`${dim} ║${reset}${dim}
|
|
200
|
+
`${dim} ║${reset}${dim} v${PKG_VERSION}${' '.repeat(Math.max(0, 52 - PKG_VERSION.length))}${reset}${gold}🌑${reset}${dim} ║${reset}`,
|
|
198
201
|
`${dim} ╚══════════════════════════════════════════════════════════╝${reset}`,
|
|
199
202
|
'',
|
|
200
203
|
`${dim} All services. One terminal. Zero trust required.${reset}`,
|
|
201
204
|
'',
|
|
202
|
-
`${dim} Type ${gold}
|
|
205
|
+
`${dim} Type ${gold}ai <question>${dim} to chat with the trading AI.${reset}`,
|
|
206
|
+
`${dim} Type ${gold}help${dim} for all commands.${reset}`,
|
|
203
207
|
'',
|
|
204
208
|
].join('\r\n');
|
|
205
209
|
}
|
|
@@ -211,20 +215,31 @@ function getHelp() {
|
|
|
211
215
|
const reset = '\x1b[0m';
|
|
212
216
|
|
|
213
217
|
const cmds = [
|
|
218
|
+
['', `${gold}AI ASSISTANT${reset}`],
|
|
219
|
+
['ai <question>', 'Chat with trading AI'],
|
|
220
|
+
['ai clear', 'Reset chat history'],
|
|
221
|
+
['ai status', 'Show AI session info'],
|
|
222
|
+
['', ''],
|
|
223
|
+
['', `${gold}TRADING & WALLET${reset}`],
|
|
214
224
|
['price <token...>', 'Quick price check'],
|
|
215
225
|
['watch <token>', 'Live price monitor'],
|
|
216
226
|
['gas [chain]', 'Gas prices & estimates'],
|
|
217
227
|
['portfolio', 'Multi-chain balances'],
|
|
228
|
+
['send', 'Send ETH or tokens'],
|
|
229
|
+
['receive', 'Show address to receive'],
|
|
230
|
+
['wallet list', 'List wallets'],
|
|
231
|
+
['wallet balance', 'Wallet balance'],
|
|
218
232
|
['history', 'Transaction history'],
|
|
233
|
+
['', ''],
|
|
234
|
+
['', `${gold}SERVICES${reset}`],
|
|
219
235
|
['market <token>', 'Market intel & data'],
|
|
220
236
|
['mail status', 'AgentMail status'],
|
|
221
237
|
['mail inbox', 'Check email inbox'],
|
|
222
|
-
['mail send', 'Send an email'],
|
|
223
238
|
['oracle roll', 'On-chain random oracle'],
|
|
224
239
|
['casino status', 'Casino status'],
|
|
225
|
-
['wallet list', 'List wallets'],
|
|
226
|
-
['wallet balance', 'Wallet balance'],
|
|
227
240
|
['config', 'Show configuration'],
|
|
241
|
+
['', ''],
|
|
242
|
+
['', `${gold}GENERAL${reset}`],
|
|
228
243
|
['banner', 'Show banner'],
|
|
229
244
|
['clear', 'Clear screen'],
|
|
230
245
|
['help', 'This help message'],
|
|
@@ -236,7 +251,13 @@ function getHelp() {
|
|
|
236
251
|
out += `${dim} ${'─'.repeat(50)}${reset}\r\n`;
|
|
237
252
|
|
|
238
253
|
for (const [cmd, desc] of cmds) {
|
|
239
|
-
|
|
254
|
+
if (!cmd && !desc) {
|
|
255
|
+
out += '\r\n';
|
|
256
|
+
} else if (!cmd) {
|
|
257
|
+
out += ` ${desc}\r\n`;
|
|
258
|
+
} else {
|
|
259
|
+
out += ` ${green}${cmd.padEnd(22)}${reset}${dim}${desc}${reset}\r\n`;
|
|
260
|
+
}
|
|
240
261
|
}
|
|
241
262
|
|
|
242
263
|
out += '\r\n';
|