@darksol/terminal 0.2.2 ā 0.3.1
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 +11 -9
- package/src/cli.js +210 -36
- package/src/config/keys.js +53 -1
- package/src/setup/wizard.js +505 -0
- package/src/ui/banner.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darksol/terminal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/darksol.js",
|
|
12
12
|
"dev": "node bin/darksol.js dashboard",
|
|
13
|
-
"test": "node --test tests/*.test.js"
|
|
13
|
+
"test": "node --test tests/*.test.js",
|
|
14
|
+
"postinstall": "echo \"\nš DARKSOL Terminal installed. Run 'darksol setup' to configure your AI provider.\n\""
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"darksol",
|
|
@@ -25,20 +26,21 @@
|
|
|
25
26
|
"author": "DARKSOL <chris00claw@gmail.com>",
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"dependencies": {
|
|
29
|
+
"blessed": "^0.1.81",
|
|
30
|
+
"blessed-contrib": "^4.11.0",
|
|
31
|
+
"boxen": "^8.0.1",
|
|
28
32
|
"chalk": "^5.3.0",
|
|
33
|
+
"cli-table3": "^0.6.5",
|
|
29
34
|
"commander": "^12.1.0",
|
|
35
|
+
"conf": "^13.0.1",
|
|
30
36
|
"ethers": "^6.13.0",
|
|
31
|
-
"boxen": "^8.0.1",
|
|
32
|
-
"ora": "^8.1.0",
|
|
33
|
-
"cli-table3": "^0.6.5",
|
|
34
|
-
"inquirer": "^12.0.0",
|
|
35
37
|
"figlet": "^1.8.0",
|
|
36
38
|
"gradient-string": "^3.0.0",
|
|
37
|
-
"
|
|
39
|
+
"inquirer": "^12.0.0",
|
|
38
40
|
"nanospinner": "^1.1.0",
|
|
39
41
|
"node-fetch": "^3.3.2",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
+
"open": "^11.0.0",
|
|
43
|
+
"ora": "^8.1.0",
|
|
42
44
|
"terminal-link": "^3.0.0",
|
|
43
45
|
"update-notifier": "^7.3.1"
|
|
44
46
|
},
|
package/src/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { addKey, removeKey, listKeys } from './config/keys.js';
|
|
|
19
19
|
import { parseIntent, startChat, adviseStrategy, analyzeToken, executeIntent } from './llm/intent.js';
|
|
20
20
|
import { startAgentSigner, showAgentDocs } from './wallet/agent-signer.js';
|
|
21
21
|
import { listSkills, installSkill, skillInfo, uninstallSkill } from './services/skills.js';
|
|
22
|
+
import { runSetupWizard, checkFirstRun } from './setup/wizard.js';
|
|
22
23
|
|
|
23
24
|
export function cli(argv) {
|
|
24
25
|
const program = new Command();
|
|
@@ -293,6 +294,15 @@ export function cli(argv) {
|
|
|
293
294
|
.description('Settle payment on-chain')
|
|
294
295
|
.action((payment) => facilitatorSettle(payment));
|
|
295
296
|
|
|
297
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
298
|
+
// SETUP COMMAND
|
|
299
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
300
|
+
program
|
|
301
|
+
.command('setup')
|
|
302
|
+
.description('First-run setup wizard ā configure AI provider, chain, wallet')
|
|
303
|
+
.option('-f, --force', 'Re-run even if already configured')
|
|
304
|
+
.action((opts) => runSetupWizard({ force: opts.force }));
|
|
305
|
+
|
|
296
306
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
297
307
|
// AI / LLM COMMANDS
|
|
298
308
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -581,57 +591,221 @@ export function cli(argv) {
|
|
|
581
591
|
});
|
|
582
592
|
|
|
583
593
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
584
|
-
// DASHBOARD (default)
|
|
594
|
+
// DASHBOARD (default) ā CHAT-FIRST
|
|
585
595
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
586
596
|
program
|
|
587
597
|
.command('dashboard', { isDefault: true })
|
|
588
|
-
.description('Show DARKSOL Terminal
|
|
598
|
+
.description('Show DARKSOL Terminal ā chat-first interface')
|
|
589
599
|
.action(async () => {
|
|
590
600
|
showBanner();
|
|
591
601
|
|
|
602
|
+
// First-run detection ā force setup
|
|
603
|
+
const ranSetup = await checkFirstRun();
|
|
604
|
+
if (ranSetup) return;
|
|
605
|
+
|
|
592
606
|
const cfg = getAllConfig();
|
|
593
607
|
const wallet = cfg.activeWallet;
|
|
608
|
+
const { hasKey } = await import('./config/keys.js');
|
|
609
|
+
const hasLLM = ['openai', 'anthropic', 'openrouter', 'ollama'].some(s => hasKey(s));
|
|
610
|
+
|
|
611
|
+
// āā Status bar (compact) āā
|
|
612
|
+
const statusParts = [
|
|
613
|
+
wallet ? theme.success(`ā ${wallet}`) : theme.dim('ā no wallet'),
|
|
614
|
+
theme.dim(`${cfg.chain}`),
|
|
615
|
+
theme.dim(`${cfg.slippage}% slip`),
|
|
616
|
+
hasLLM ? theme.success('ā AI ready') : theme.accent('ā no AI'),
|
|
617
|
+
];
|
|
618
|
+
console.log(` ${statusParts.join(theme.dim(' ā '))}`);
|
|
619
|
+
console.log('');
|
|
594
620
|
|
|
595
|
-
|
|
621
|
+
// āā CHAT INTERFACE (primary) āā
|
|
622
|
+
if (hasLLM) {
|
|
623
|
+
// AI is connected ā drop straight into chat
|
|
624
|
+
console.log(theme.gold(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
625
|
+
console.log(theme.gold(' ā') + theme.label(' DARKSOL AI ā ready. Ask anything. ') + theme.gold('ā'));
|
|
626
|
+
console.log(theme.gold(' ā') + theme.dim(' "swap 0.1 ETH for USDC" ⢠"what\'s AERO at?" ⢠"help" ') + theme.gold('ā'));
|
|
627
|
+
console.log(theme.gold(' ā') + theme.dim(' Type "commands" to see all tools. Type "exit" to quit. ') + theme.gold('ā'));
|
|
628
|
+
console.log(theme.gold(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
629
|
+
console.log('');
|
|
630
|
+
|
|
631
|
+
// Start interactive chat loop
|
|
632
|
+
await startChatLoop(cfg);
|
|
633
|
+
} else {
|
|
634
|
+
// No AI ā show connect prompt
|
|
635
|
+
console.log(theme.gold(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
636
|
+
console.log(theme.gold(' ā') + theme.accent(' ā No AI provider connected ') + theme.gold('ā'));
|
|
637
|
+
console.log(theme.gold(' ā') + theme.dim(' The DARKSOL AI needs an LLM to work. ') + theme.gold('ā'));
|
|
638
|
+
console.log(theme.gold(' ā') + theme.dim(' ') + theme.gold('ā'));
|
|
639
|
+
console.log(theme.gold(' ā') + theme.dim(' Run: ') + theme.label('darksol setup') + theme.dim(' to connect OpenAI/Anthropic ') + theme.gold('ā'));
|
|
640
|
+
console.log(theme.gold(' ā') + theme.dim(' Run: ') + theme.label('darksol keys add openai') + theme.dim(' to add an API key ') + theme.gold('ā'));
|
|
641
|
+
console.log(theme.gold(' ā') + theme.dim(' ') + theme.gold('ā'));
|
|
642
|
+
console.log(theme.gold(' ā') + theme.dim(' Or use Ollama for free local AI ā no key needed. ') + theme.gold('ā'));
|
|
643
|
+
console.log(theme.gold(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
644
|
+
console.log('');
|
|
645
|
+
showCommandList();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
program.parse(argv);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
653
|
+
// CHAT-FIRST LOOP (default experience)
|
|
654
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
655
|
+
|
|
656
|
+
async function startChatLoop(cfg) {
|
|
657
|
+
const { createLLM } = await import('./llm/engine.js');
|
|
658
|
+
const { quickPrice } = await import('./utils/helpers.js');
|
|
659
|
+
const { executeIntent, parseIntent, INTENT_SYSTEM_PROMPT } = await import('./llm/intent.js');
|
|
660
|
+
|
|
661
|
+
let llm;
|
|
662
|
+
try {
|
|
663
|
+
llm = await createLLM({});
|
|
664
|
+
const chain = cfg.chain || 'base';
|
|
665
|
+
const wallet = cfg.activeWallet || '(not set)';
|
|
666
|
+
const slippage = cfg.slippage || 0.5;
|
|
667
|
+
|
|
668
|
+
const systemPrompt = INTENT_SYSTEM_PROMPT
|
|
669
|
+
.replace('{{chain}}', chain)
|
|
670
|
+
.replace('{{wallet}}', wallet)
|
|
671
|
+
.replace('{{slippage}}', slippage);
|
|
672
|
+
|
|
673
|
+
llm.setSystemPrompt(systemPrompt);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
error(`AI init failed: ${err.message}`);
|
|
676
|
+
info('Run: darksol setup');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const inquirerMod = await import('inquirer');
|
|
681
|
+
const inquirerDefault = inquirerMod.default;
|
|
682
|
+
|
|
683
|
+
while (true) {
|
|
684
|
+
const { input } = await inquirerDefault.prompt([{
|
|
685
|
+
type: 'input',
|
|
686
|
+
name: 'input',
|
|
687
|
+
message: theme.gold('š'),
|
|
688
|
+
validate: (v) => v.length > 0 || ' ',
|
|
689
|
+
}]);
|
|
690
|
+
|
|
691
|
+
const trimmed = input.trim().toLowerCase();
|
|
692
|
+
|
|
693
|
+
// Meta-commands within chat
|
|
694
|
+
if (['exit', 'quit', 'q'].includes(trimmed)) {
|
|
695
|
+
const usage = llm.getUsage();
|
|
696
|
+
info(`Session: ${usage.calls} calls, ${usage.totalTokens} tokens`);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (['commands', 'help', 'cmds', '?'].includes(trimmed)) {
|
|
701
|
+
showCommandList();
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (trimmed === 'status') {
|
|
596
706
|
kvDisplay([
|
|
597
|
-
['Wallet',
|
|
707
|
+
['Wallet', cfg.activeWallet || theme.dim('(none)')],
|
|
598
708
|
['Chain', cfg.chain],
|
|
709
|
+
['Provider', `${llm.provider}/${llm.model}`],
|
|
599
710
|
['Slippage', `${cfg.slippage}%`],
|
|
600
711
|
]);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
601
714
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
];
|
|
715
|
+
// Check if it looks like a trade intent
|
|
716
|
+
const tradeWords = ['buy', 'sell', 'swap', 'snipe', 'transfer', 'send', 'trade'];
|
|
717
|
+
const isTradeIntent = tradeWords.some(w => trimmed.startsWith(w));
|
|
718
|
+
|
|
719
|
+
if (isTradeIntent) {
|
|
720
|
+
// Parse as trade intent with confirmation
|
|
721
|
+
const intent = await parseIntent(input, {});
|
|
722
|
+
if (intent.action !== 'error' && intent.action !== 'unknown') {
|
|
723
|
+
showSection('PARSED INTENT');
|
|
724
|
+
kvDisplay(Object.entries(intent)
|
|
725
|
+
.filter(([k]) => !['raw', 'model', 'reasoning'].includes(k))
|
|
726
|
+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)])
|
|
727
|
+
);
|
|
728
|
+
if (intent.warnings?.length) intent.warnings.forEach(w => warn(w));
|
|
729
|
+
if (intent.command) info(`Command: ${theme.gold(intent.command)}`);
|
|
730
|
+
console.log('');
|
|
731
|
+
} else {
|
|
732
|
+
// Fall through to regular chat
|
|
733
|
+
await chatResponse(llm, input);
|
|
734
|
+
}
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
625
737
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
738
|
+
// Regular chat
|
|
739
|
+
await chatResponse(llm, input);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
629
742
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
743
|
+
async function chatResponse(llm, input) {
|
|
744
|
+
const { quickPrice } = await import('./utils/helpers.js');
|
|
745
|
+
const { spinner: spin } = await import('./ui/components.js');
|
|
746
|
+
const s = spin('Thinking...').start();
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
// Enrich with live price data
|
|
750
|
+
let enriched = input;
|
|
751
|
+
const tokenPattern = /\b([A-Z]{2,10})\b/g;
|
|
752
|
+
const tokens = [...new Set(input.toUpperCase().match(tokenPattern) || [])];
|
|
753
|
+
const skipTokens = ['ETH', 'THE', 'FOR', 'AND', 'BUY', 'SELL', 'DCA', 'SWAP', 'WHAT', 'PRICE', 'HOW', 'MUCH', 'NOT', 'CAN', 'YOU', 'HELP'];
|
|
754
|
+
|
|
755
|
+
const priceData = [];
|
|
756
|
+
for (const t of tokens.filter(t => !skipTokens.includes(t)).slice(0, 3)) {
|
|
757
|
+
const p = await quickPrice(t);
|
|
758
|
+
if (p) priceData.push(`${p.symbol}: $${p.price} (liq: $${p.liquidity})`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (priceData.length > 0) {
|
|
762
|
+
enriched += `\n\n[Live data: ${priceData.join(', ')}]`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const result = await llm.chat(enriched);
|
|
766
|
+
s.succeed('');
|
|
767
|
+
|
|
768
|
+
// Display response
|
|
769
|
+
console.log('');
|
|
770
|
+
const lines = result.content.split('\n');
|
|
771
|
+
for (const line of lines) {
|
|
772
|
+
console.log(theme.dim(' ') + line);
|
|
773
|
+
}
|
|
774
|
+
console.log('');
|
|
775
|
+
} catch (err) {
|
|
776
|
+
s.fail('Error');
|
|
777
|
+
error(err.message);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
635
780
|
|
|
636
|
-
|
|
781
|
+
function showCommandList() {
|
|
782
|
+
console.log('');
|
|
783
|
+
showSection('COMMANDS');
|
|
784
|
+
const commands = [
|
|
785
|
+
['wallet', 'Create, import, manage wallets'],
|
|
786
|
+
['trade', 'Swap tokens, snipe, trading'],
|
|
787
|
+
['dca', 'Dollar-cost averaging orders'],
|
|
788
|
+
['ai chat', 'Standalone AI chat session'],
|
|
789
|
+
['ai execute', 'Parse + execute a trade via AI'],
|
|
790
|
+
['agent start', 'Start secure agent signer'],
|
|
791
|
+
['keys', 'API key vault'],
|
|
792
|
+
['script', 'Execution scripts & strategies'],
|
|
793
|
+
['market', 'Market intel & token data'],
|
|
794
|
+
['oracle', 'On-chain random oracle'],
|
|
795
|
+
['casino', 'The Clawsino ā betting'],
|
|
796
|
+
['cards', 'Prepaid Visa/MC cards'],
|
|
797
|
+
['builders', 'ERC-8021 builder index'],
|
|
798
|
+
['facilitator', 'x402 payment facilitator'],
|
|
799
|
+
['skills', 'Agent skill directory'],
|
|
800
|
+
['setup', 'Re-run setup wizard'],
|
|
801
|
+
['config', 'Terminal configuration'],
|
|
802
|
+
];
|
|
803
|
+
|
|
804
|
+
commands.forEach(([cmd, desc]) => {
|
|
805
|
+
console.log(` ${theme.gold(cmd.padEnd(16))} ${theme.dim(desc)}`);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
console.log('');
|
|
809
|
+
console.log(theme.dim(' Run any command: darksol <command> --help'));
|
|
810
|
+
console.log('');
|
|
637
811
|
}
|
package/src/config/keys.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { homedir } from 'os';
|
|
4
|
+
import { homedir, hostname, userInfo } from 'os';
|
|
5
5
|
import { theme } from '../ui/theme.js';
|
|
6
6
|
import { kvDisplay, success, error, warn, info } from '../ui/components.js';
|
|
7
7
|
import { showSection } from '../ui/banner.js';
|
|
@@ -317,4 +317,56 @@ export function listKeys() {
|
|
|
317
317
|
info('Services: ' + Object.keys(SERVICES).join(', '));
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Add a key directly (non-interactive, for setup wizard / OAuth)
|
|
322
|
+
* Uses a machine-derived vault password for seamless storage
|
|
323
|
+
*/
|
|
324
|
+
export function addKeyDirect(service, apiKey) {
|
|
325
|
+
const vaultPass = getMachineVaultPass();
|
|
326
|
+
const vault = loadVault();
|
|
327
|
+
const svc = SERVICES[service];
|
|
328
|
+
vault.keys[service] = {
|
|
329
|
+
encrypted: encrypt(apiKey, vaultPass),
|
|
330
|
+
service: svc?.name || service,
|
|
331
|
+
category: svc?.category || 'custom',
|
|
332
|
+
addedAt: new Date().toISOString(),
|
|
333
|
+
autoStored: true, // flag: stored via wizard, not manual password
|
|
334
|
+
};
|
|
335
|
+
saveVault(vault);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get a key stored via addKeyDirect (auto-stored, machine password)
|
|
340
|
+
*/
|
|
341
|
+
export function getKeyAuto(service) {
|
|
342
|
+
const vault = loadVault();
|
|
343
|
+
const entry = vault.keys[service];
|
|
344
|
+
if (!entry) return getKeyFromEnv(service);
|
|
345
|
+
if (!entry.autoStored) return getKeyFromEnv(service); // manual entries need password
|
|
346
|
+
try {
|
|
347
|
+
return decrypt(entry.encrypted, getMachineVaultPass());
|
|
348
|
+
} catch {
|
|
349
|
+
return getKeyFromEnv(service);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check if any key exists for a service (stored or env)
|
|
355
|
+
*/
|
|
356
|
+
export function hasKey(service) {
|
|
357
|
+
const vault = loadVault();
|
|
358
|
+
if (vault.keys[service]) return true;
|
|
359
|
+
const svc = SERVICES[service];
|
|
360
|
+
if (svc?.envVar && process.env[svc.envVar]) return true;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Machine-derived vault password for auto-stored keys
|
|
366
|
+
* (derived from hostname + username ā not high security, but protects at rest)
|
|
367
|
+
*/
|
|
368
|
+
function getMachineVaultPass() {
|
|
369
|
+
return `darksol-vault-${hostname()}-${userInfo().username}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
320
372
|
export { KEYS_DIR, KEYS_FILE };
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { theme } from '../ui/theme.js';
|
|
3
|
+
import { showSection, showDivider } from '../ui/banner.js';
|
|
4
|
+
import { success, error, warn, info, kvDisplay } from '../ui/components.js';
|
|
5
|
+
import { getConfig, setConfig } from '../config/store.js';
|
|
6
|
+
import { addKeyDirect, hasKey, SERVICES } from '../config/keys.js';
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
12
|
+
// FIRST-RUN SETUP WIZARD
|
|
13
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if this is a first run (no LLM keys configured)
|
|
17
|
+
*/
|
|
18
|
+
export function isFirstRun() {
|
|
19
|
+
const hasAnyLLM = ['openai', 'anthropic', 'openrouter', 'ollama'].some(s => hasKey(s));
|
|
20
|
+
const setupDone = getConfig('setupComplete');
|
|
21
|
+
return !hasAnyLLM && !setupDone;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run the setup wizard
|
|
26
|
+
*/
|
|
27
|
+
export async function runSetupWizard(opts = {}) {
|
|
28
|
+
const force = opts.force || false;
|
|
29
|
+
|
|
30
|
+
if (!force && !isFirstRun()) {
|
|
31
|
+
info('Setup already complete. Use --force to re-run.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('');
|
|
36
|
+
showSection('š DARKSOL TERMINAL ā FIRST RUN SETUP');
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(theme.dim(' Welcome to DARKSOL Terminal. Let\'s get you set up.'));
|
|
39
|
+
console.log(theme.dim(' You need an LLM provider to use the AI trading assistant.'));
|
|
40
|
+
console.log(theme.dim(' Everything else works without one.'));
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
showDivider();
|
|
44
|
+
|
|
45
|
+
// Step 1: Choose LLM provider
|
|
46
|
+
const { provider } = await inquirer.prompt([{
|
|
47
|
+
type: 'list',
|
|
48
|
+
name: 'provider',
|
|
49
|
+
message: theme.gold('Choose your AI provider:'),
|
|
50
|
+
choices: [
|
|
51
|
+
{ name: 'š¤ OpenAI (GPT-4o, GPT-5) ā API key or OAuth', value: 'openai' },
|
|
52
|
+
{ name: 'š§ Anthropic (Claude Opus, Sonnet) ā API key or OAuth', value: 'anthropic' },
|
|
53
|
+
{ name: 'š OpenRouter (any model, one key) ā API key', value: 'openrouter' },
|
|
54
|
+
{ name: 'š Ollama (local models, free, private) ā no key needed', value: 'ollama' },
|
|
55
|
+
{ name: 'āļø Skip for now', value: 'skip' },
|
|
56
|
+
],
|
|
57
|
+
}]);
|
|
58
|
+
|
|
59
|
+
if (provider === 'skip') {
|
|
60
|
+
warn('Skipped LLM setup. You can set up later with: darksol setup');
|
|
61
|
+
setConfig('setupComplete', true);
|
|
62
|
+
showPostSetup();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (provider === 'ollama') {
|
|
67
|
+
await setupOllama();
|
|
68
|
+
} else {
|
|
69
|
+
await setupCloudProvider(provider);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2: Chain selection
|
|
73
|
+
console.log('');
|
|
74
|
+
const { chain } = await inquirer.prompt([{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'chain',
|
|
77
|
+
message: theme.gold('Default chain:'),
|
|
78
|
+
choices: [
|
|
79
|
+
{ name: 'Base (recommended ā low fees, fast)', value: 'base' },
|
|
80
|
+
{ name: 'Ethereum (mainnet)', value: 'ethereum' },
|
|
81
|
+
{ name: 'Arbitrum', value: 'arbitrum' },
|
|
82
|
+
{ name: 'Optimism', value: 'optimism' },
|
|
83
|
+
{ name: 'Polygon', value: 'polygon' },
|
|
84
|
+
],
|
|
85
|
+
default: 'base',
|
|
86
|
+
}]);
|
|
87
|
+
setConfig('chain', chain);
|
|
88
|
+
success(`Chain set to ${chain}`);
|
|
89
|
+
|
|
90
|
+
// Step 3: Wallet
|
|
91
|
+
console.log('');
|
|
92
|
+
const { createWallet } = await inquirer.prompt([{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'createWallet',
|
|
95
|
+
message: theme.gold('Create a wallet now?'),
|
|
96
|
+
default: true,
|
|
97
|
+
}]);
|
|
98
|
+
|
|
99
|
+
if (createWallet) {
|
|
100
|
+
const { createNewWallet } = await import('../wallet/manager.js');
|
|
101
|
+
await createNewWallet();
|
|
102
|
+
} else {
|
|
103
|
+
info('Create one later: darksol wallet create <name>');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setConfig('setupComplete', true);
|
|
107
|
+
showPostSetup();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Setup a cloud provider (OpenAI, Anthropic, OpenRouter)
|
|
112
|
+
*/
|
|
113
|
+
async function setupCloudProvider(provider) {
|
|
114
|
+
const supportsOAuth = ['openai', 'anthropic'].includes(provider);
|
|
115
|
+
const providerName = {
|
|
116
|
+
openai: 'OpenAI',
|
|
117
|
+
anthropic: 'Anthropic',
|
|
118
|
+
openrouter: 'OpenRouter',
|
|
119
|
+
}[provider];
|
|
120
|
+
|
|
121
|
+
if (supportsOAuth) {
|
|
122
|
+
const { method } = await inquirer.prompt([{
|
|
123
|
+
type: 'list',
|
|
124
|
+
name: 'method',
|
|
125
|
+
message: theme.gold(`How do you want to connect ${providerName}?`),
|
|
126
|
+
choices: [
|
|
127
|
+
{ name: `š API Key ā paste your ${providerName} API key`, value: 'apikey' },
|
|
128
|
+
{ name: `š OAuth ā sign in with your ${providerName} account`, value: 'oauth' },
|
|
129
|
+
{ name: `š Instructions ā show me how to get a key`, value: 'help' },
|
|
130
|
+
],
|
|
131
|
+
}]);
|
|
132
|
+
|
|
133
|
+
if (method === 'apikey') {
|
|
134
|
+
await setupAPIKey(provider);
|
|
135
|
+
} else if (method === 'oauth') {
|
|
136
|
+
await startOAuth(provider);
|
|
137
|
+
} else {
|
|
138
|
+
showKeyInstructions(provider);
|
|
139
|
+
// After showing instructions, ask for key
|
|
140
|
+
await setupAPIKey(provider);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
await setupAPIKey(provider);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Setup via API key entry
|
|
149
|
+
*/
|
|
150
|
+
async function setupAPIKey(provider) {
|
|
151
|
+
const providerName = {
|
|
152
|
+
openai: 'OpenAI',
|
|
153
|
+
anthropic: 'Anthropic',
|
|
154
|
+
openrouter: 'OpenRouter',
|
|
155
|
+
}[provider];
|
|
156
|
+
|
|
157
|
+
const { key } = await inquirer.prompt([{
|
|
158
|
+
type: 'password',
|
|
159
|
+
name: 'key',
|
|
160
|
+
message: theme.gold(`${providerName} API key:`),
|
|
161
|
+
mask: 'ā',
|
|
162
|
+
validate: (v) => {
|
|
163
|
+
if (!v || v.length < 10) return 'Key seems too short';
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
}]);
|
|
167
|
+
|
|
168
|
+
addKeyDirect(provider, key);
|
|
169
|
+
success(`${providerName} key saved (encrypted)`);
|
|
170
|
+
|
|
171
|
+
// Set as default provider
|
|
172
|
+
setConfig('llmProvider', provider);
|
|
173
|
+
info(`Default AI provider set to ${provider}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Setup Ollama (local)
|
|
178
|
+
*/
|
|
179
|
+
async function setupOllama() {
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(theme.gold(' OLLAMA SETUP'));
|
|
182
|
+
console.log(theme.dim(' Ollama runs models locally ā free, private, no API key needed.'));
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
const { host } = await inquirer.prompt([{
|
|
186
|
+
type: 'input',
|
|
187
|
+
name: 'host',
|
|
188
|
+
message: theme.gold('Ollama host:'),
|
|
189
|
+
default: 'http://localhost:11434',
|
|
190
|
+
}]);
|
|
191
|
+
|
|
192
|
+
setConfig('ollamaHost', host);
|
|
193
|
+
|
|
194
|
+
const { model } = await inquirer.prompt([{
|
|
195
|
+
type: 'input',
|
|
196
|
+
name: 'model',
|
|
197
|
+
message: theme.gold('Default model:'),
|
|
198
|
+
default: 'llama3',
|
|
199
|
+
}]);
|
|
200
|
+
|
|
201
|
+
setConfig('ollamaModel', model);
|
|
202
|
+
setConfig('llmProvider', 'ollama');
|
|
203
|
+
|
|
204
|
+
success(`Ollama configured: ${host} / ${model}`);
|
|
205
|
+
info('Make sure Ollama is running: ollama serve');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Show instructions for getting API keys
|
|
210
|
+
*/
|
|
211
|
+
function showKeyInstructions(provider) {
|
|
212
|
+
console.log('');
|
|
213
|
+
|
|
214
|
+
if (provider === 'openai') {
|
|
215
|
+
showSection('GET AN OPENAI API KEY');
|
|
216
|
+
console.log(theme.dim(' 1. Go to https://platform.openai.com/api-keys'));
|
|
217
|
+
console.log(theme.dim(' 2. Click "Create new secret key"'));
|
|
218
|
+
console.log(theme.dim(' 3. Copy the key (starts with sk-)'));
|
|
219
|
+
console.log(theme.dim(' 4. Paste it below'));
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(theme.dim(' š” If you have a ChatGPT Plus/Pro subscription,'));
|
|
222
|
+
console.log(theme.dim(' you can use OAuth instead (sign in with your account).'));
|
|
223
|
+
} else if (provider === 'anthropic') {
|
|
224
|
+
showSection('GET AN ANTHROPIC API KEY');
|
|
225
|
+
console.log(theme.dim(' 1. Go to https://console.anthropic.com/settings/keys'));
|
|
226
|
+
console.log(theme.dim(' 2. Click "Create Key"'));
|
|
227
|
+
console.log(theme.dim(' 3. Copy the key (starts with sk-ant-)'));
|
|
228
|
+
console.log(theme.dim(' 4. Paste it below'));
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(theme.dim(' š” If you have a Claude Pro/Team subscription,'));
|
|
231
|
+
console.log(theme.dim(' you can use OAuth instead.'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
238
|
+
// OAuth FLOWS
|
|
239
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
240
|
+
|
|
241
|
+
// OAuth configurations
|
|
242
|
+
const OAUTH_CONFIGS = {
|
|
243
|
+
openai: {
|
|
244
|
+
name: 'OpenAI',
|
|
245
|
+
authUrl: 'https://auth.openai.com/authorize',
|
|
246
|
+
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
247
|
+
// These are placeholder client IDs ā users need to register their own app
|
|
248
|
+
// or use the direct API key flow
|
|
249
|
+
clientId: null,
|
|
250
|
+
scopes: ['openid', 'profile'],
|
|
251
|
+
helpUrl: 'https://platform.openai.com/docs/guides/authentication',
|
|
252
|
+
},
|
|
253
|
+
anthropic: {
|
|
254
|
+
name: 'Anthropic',
|
|
255
|
+
authUrl: 'https://console.anthropic.com/oauth/authorize',
|
|
256
|
+
tokenUrl: 'https://console.anthropic.com/oauth/token',
|
|
257
|
+
clientId: null,
|
|
258
|
+
scopes: ['api'],
|
|
259
|
+
helpUrl: 'https://docs.anthropic.com/en/docs/authentication',
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Start OAuth flow for a provider
|
|
265
|
+
*/
|
|
266
|
+
async function startOAuth(provider) {
|
|
267
|
+
const config = OAUTH_CONFIGS[provider];
|
|
268
|
+
|
|
269
|
+
// Check if provider has public OAuth available
|
|
270
|
+
// As of 2026, OpenAI and Anthropic have limited OAuth ā API keys are more common
|
|
271
|
+
console.log('');
|
|
272
|
+
showSection(`${config.name} OAuth`);
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(theme.dim(' OAuth lets you sign in with your existing subscription'));
|
|
275
|
+
console.log(theme.dim(' without creating a separate API key.'));
|
|
276
|
+
console.log('');
|
|
277
|
+
|
|
278
|
+
// Check for custom client ID (user may have registered an OAuth app)
|
|
279
|
+
const storedClientId = getConfig(`oauth_${provider}_clientId`);
|
|
280
|
+
|
|
281
|
+
if (!storedClientId && !config.clientId) {
|
|
282
|
+
// No OAuth app registered ā offer alternatives
|
|
283
|
+
console.log(theme.accent(' ā ļø OAuth requires a registered application.'));
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(theme.dim(' Options:'));
|
|
286
|
+
console.log(theme.dim(` 1. Register an OAuth app at ${config.helpUrl}`));
|
|
287
|
+
console.log(theme.dim(' 2. Use an API key instead (faster, simpler)'));
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
const { oauthChoice } = await inquirer.prompt([{
|
|
291
|
+
type: 'list',
|
|
292
|
+
name: 'oauthChoice',
|
|
293
|
+
message: theme.gold('How to proceed?'),
|
|
294
|
+
choices: [
|
|
295
|
+
{ name: 'š Use API key instead (recommended)', value: 'apikey' },
|
|
296
|
+
{ name: 'š Enter my OAuth client ID', value: 'clientid' },
|
|
297
|
+
{ name: 'š Open registration page in browser', value: 'register' },
|
|
298
|
+
],
|
|
299
|
+
}]);
|
|
300
|
+
|
|
301
|
+
if (oauthChoice === 'apikey') {
|
|
302
|
+
await setupAPIKey(provider);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (oauthChoice === 'register') {
|
|
307
|
+
try {
|
|
308
|
+
await open(config.helpUrl);
|
|
309
|
+
info(`Opened ${config.helpUrl} in your browser`);
|
|
310
|
+
} catch {
|
|
311
|
+
info(`Go to: ${config.helpUrl}`);
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
const { hasClientId } = await inquirer.prompt([{
|
|
315
|
+
type: 'confirm',
|
|
316
|
+
name: 'hasClientId',
|
|
317
|
+
message: theme.gold('Do you have a client ID now?'),
|
|
318
|
+
default: false,
|
|
319
|
+
}]);
|
|
320
|
+
if (!hasClientId) {
|
|
321
|
+
info('No problem ā use an API key for now.');
|
|
322
|
+
await setupAPIKey(provider);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Get client ID from user
|
|
328
|
+
const { clientId } = await inquirer.prompt([{
|
|
329
|
+
type: 'input',
|
|
330
|
+
name: 'clientId',
|
|
331
|
+
message: theme.gold('OAuth Client ID:'),
|
|
332
|
+
validate: (v) => v.length > 5 || 'Client ID seems too short',
|
|
333
|
+
}]);
|
|
334
|
+
|
|
335
|
+
const { clientSecret } = await inquirer.prompt([{
|
|
336
|
+
type: 'password',
|
|
337
|
+
name: 'clientSecret',
|
|
338
|
+
message: theme.gold('OAuth Client Secret:'),
|
|
339
|
+
mask: 'ā',
|
|
340
|
+
}]);
|
|
341
|
+
|
|
342
|
+
setConfig(`oauth_${provider}_clientId`, clientId);
|
|
343
|
+
if (clientSecret) {
|
|
344
|
+
addKeyDirect(`${provider}_oauth_secret`, clientSecret);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await executeOAuthFlow(provider, clientId, clientSecret);
|
|
348
|
+
} else {
|
|
349
|
+
const clientId = storedClientId || config.clientId;
|
|
350
|
+
const clientSecret = getKey(`${provider}_oauth_secret`);
|
|
351
|
+
await executeOAuthFlow(provider, clientId, clientSecret);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Execute the OAuth authorization code flow
|
|
357
|
+
*/
|
|
358
|
+
async function executeOAuthFlow(provider, clientId, clientSecret) {
|
|
359
|
+
const config = OAUTH_CONFIGS[provider];
|
|
360
|
+
const port = 19876; // Local callback port
|
|
361
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
362
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
363
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
364
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
365
|
+
|
|
366
|
+
// Build auth URL
|
|
367
|
+
const params = new URLSearchParams({
|
|
368
|
+
response_type: 'code',
|
|
369
|
+
client_id: clientId,
|
|
370
|
+
redirect_uri: redirectUri,
|
|
371
|
+
scope: config.scopes.join(' '),
|
|
372
|
+
state,
|
|
373
|
+
code_challenge: codeChallenge,
|
|
374
|
+
code_challenge_method: 'S256',
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const authUrl = `${config.authUrl}?${params}`;
|
|
378
|
+
|
|
379
|
+
// Start local server to receive callback
|
|
380
|
+
return new Promise(async (resolve) => {
|
|
381
|
+
const server = createServer(async (req, res) => {
|
|
382
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
383
|
+
|
|
384
|
+
if (url.pathname === '/callback') {
|
|
385
|
+
const code = url.searchParams.get('code');
|
|
386
|
+
const returnedState = url.searchParams.get('state');
|
|
387
|
+
const err = url.searchParams.get('error');
|
|
388
|
+
|
|
389
|
+
if (err) {
|
|
390
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
391
|
+
res.end('<html><body><h2>ā Authorization failed</h2><p>You can close this window.</p></body></html>');
|
|
392
|
+
error(`OAuth error: ${err}`);
|
|
393
|
+
server.close();
|
|
394
|
+
resolve(false);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (returnedState !== state) {
|
|
399
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
400
|
+
res.end('<html><body><h2>ā State mismatch</h2><p>Possible CSRF. You can close this window.</p></body></html>');
|
|
401
|
+
error('OAuth state mismatch ā possible security issue');
|
|
402
|
+
server.close();
|
|
403
|
+
resolve(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Exchange code for token
|
|
408
|
+
try {
|
|
409
|
+
const fetch = (await import('node-fetch')).default;
|
|
410
|
+
const tokenResp = await fetch(config.tokenUrl, {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
413
|
+
body: new URLSearchParams({
|
|
414
|
+
grant_type: 'authorization_code',
|
|
415
|
+
code,
|
|
416
|
+
redirect_uri: redirectUri,
|
|
417
|
+
client_id: clientId,
|
|
418
|
+
...(clientSecret ? { client_secret: clientSecret } : {}),
|
|
419
|
+
code_verifier: codeVerifier,
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const tokenData = await tokenResp.json();
|
|
424
|
+
|
|
425
|
+
if (tokenData.access_token) {
|
|
426
|
+
// Store the token as the API key
|
|
427
|
+
addKeyDirect(provider, tokenData.access_token);
|
|
428
|
+
if (tokenData.refresh_token) {
|
|
429
|
+
addKeyDirect(`${provider}_refresh`, tokenData.refresh_token);
|
|
430
|
+
}
|
|
431
|
+
setConfig('llmProvider', provider);
|
|
432
|
+
|
|
433
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
434
|
+
res.end(`<html><body style="background:#1a1a2e;color:#d4a574;font-family:monospace;text-align:center;padding:60px"><h2>ā
DARKSOL Terminal ā Connected to ${config.name}</h2><p>You can close this window.</p></body></html>`);
|
|
435
|
+
|
|
436
|
+
success(`${config.name} connected via OAuth`);
|
|
437
|
+
info(`Token stored (encrypted). Provider set to ${provider}.`);
|
|
438
|
+
} else {
|
|
439
|
+
throw new Error(tokenData.error || 'No access token in response');
|
|
440
|
+
}
|
|
441
|
+
} catch (tokenErr) {
|
|
442
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
443
|
+
res.end('<html><body><h2>ā Token exchange failed</h2><p>You can close this window.</p></body></html>');
|
|
444
|
+
error(`Token exchange failed: ${tokenErr.message}`);
|
|
445
|
+
info('Try using an API key instead: darksol keys add ' + provider);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
server.close();
|
|
449
|
+
resolve(true);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
server.listen(port, '127.0.0.1', async () => {
|
|
454
|
+
console.log('');
|
|
455
|
+
info(`Opening ${config.name} authorization page...`);
|
|
456
|
+
console.log(theme.dim(` If browser doesn't open, go to:`));
|
|
457
|
+
console.log(theme.accent(` ${authUrl}`));
|
|
458
|
+
console.log('');
|
|
459
|
+
info('Waiting for authorization...');
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
await open(authUrl);
|
|
463
|
+
} catch {
|
|
464
|
+
warn('Could not open browser automatically');
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Timeout after 5 minutes
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
warn('OAuth timed out (5 minutes)');
|
|
471
|
+
server.close();
|
|
472
|
+
resolve(false);
|
|
473
|
+
}, 300000);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
478
|
+
// POST-SETUP & HELPERS
|
|
479
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
480
|
+
|
|
481
|
+
function showPostSetup() {
|
|
482
|
+
console.log('');
|
|
483
|
+
showSection('š YOU\'RE READY');
|
|
484
|
+
console.log('');
|
|
485
|
+
console.log(theme.gold(' Next steps:'));
|
|
486
|
+
console.log(theme.dim(' ⢠darksol ai chat Start the AI trading assistant'));
|
|
487
|
+
console.log(theme.dim(' ⢠darksol market top See what\'s moving'));
|
|
488
|
+
console.log(theme.dim(' ⢠darksol wallet create Create an encrypted wallet'));
|
|
489
|
+
console.log(theme.dim(' ⢠darksol tips Trading tips & tricks'));
|
|
490
|
+
console.log(theme.dim(' ⢠darksol quickstart Full getting started guide'));
|
|
491
|
+
console.log('');
|
|
492
|
+
console.log(theme.dim(' Re-run setup anytime: darksol setup --force'));
|
|
493
|
+
console.log('');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Quick check on startup ā if first run, FORCE setup (no prompt)
|
|
498
|
+
*/
|
|
499
|
+
export async function checkFirstRun() {
|
|
500
|
+
if (isFirstRun()) {
|
|
501
|
+
await runSetupWizard();
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
package/src/ui/banner.js
CHANGED
|
@@ -26,7 +26,7 @@ export function showBanner(opts = {}) {
|
|
|
26
26
|
);
|
|
27
27
|
console.log(
|
|
28
28
|
theme.dim(' ā ') +
|
|
29
|
-
theme.subtle(' v0.
|
|
29
|
+
theme.subtle(' v0.3.1') +
|
|
30
30
|
theme.dim(' ') +
|
|
31
31
|
theme.gold('š') +
|
|
32
32
|
theme.dim(' ā')
|
|
@@ -44,7 +44,7 @@ export function showBanner(opts = {}) {
|
|
|
44
44
|
|
|
45
45
|
export function showMiniBanner() {
|
|
46
46
|
console.log('');
|
|
47
|
-
console.log(theme.gold.bold(' š DARKSOL TERMINAL') + theme.dim(' v0.
|
|
47
|
+
console.log(theme.gold.bold(' š DARKSOL TERMINAL') + theme.dim(' v0.3.1'));
|
|
48
48
|
console.log(theme.dim(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
49
49
|
console.log('');
|
|
50
50
|
}
|
|
@@ -62,3 +62,5 @@ export function showDivider() {
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
|
|
66
|
+
|