@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.2.2",
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
- "conf": "^13.0.1",
39
+ "inquirer": "^12.0.0",
38
40
  "nanospinner": "^1.1.0",
39
41
  "node-fetch": "^3.3.2",
40
- "blessed": "^0.1.81",
41
- "blessed-contrib": "^4.11.0",
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 dashboard')
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
- showSection('STATUS');
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', wallet || theme.dim('Not set — run: darksol wallet create')],
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
- console.log('');
603
- showSection('COMMANDS');
604
- const commands = [
605
- ['wallet', 'Create, import, manage wallets'],
606
- ['trade', 'Swap tokens, snipe, trading'],
607
- ['dca', 'Dollar-cost averaging orders'],
608
- ['ai', 'AI trading assistant & analysis'],
609
- ['agent', 'Secure agent signer (PK-isolated)'],
610
- ['keys', 'API key vault (LLMs, data, RPCs)'],
611
- ['script', 'Execution scripts & strategies'],
612
- ['market', 'Market intel & token data'],
613
- ['oracle', 'On-chain random oracle'],
614
- ['casino', 'The Clawsino — betting'],
615
- ['cards', 'Prepaid Visa/MC cards'],
616
- ['builders', 'ERC-8021 builder index'],
617
- ['facilitator', 'x402 payment facilitator'],
618
- ['skills', 'Agent skill directory & install'],
619
- ['config', 'Terminal configuration'],
620
- ['tips', 'Trading & scripting tips'],
621
- ['networks', 'Chain reference & explorers'],
622
- ['quickstart', 'Getting started guide'],
623
- ['lookup', 'Look up any address on-chain'],
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
- commands.forEach(([cmd, desc]) => {
627
- console.log(` ${theme.gold(cmd.padEnd(16))} ${theme.dim(desc)}`);
628
- });
738
+ // Regular chat
739
+ await chatResponse(llm, input);
740
+ }
741
+ }
629
742
 
630
- console.log('');
631
- console.log(theme.dim(' Run any command with --help for details'));
632
- console.log(theme.dim(' Example: darksol trade swap --help'));
633
- console.log('');
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
- program.parse(argv);
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
  }
@@ -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.2.2') +
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.2.2'));
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
+