@darksol/terminal 0.2.2 → 0.3.0

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.0",
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
  // ═══════════════════════════════════════
@@ -589,6 +599,10 @@ export function cli(argv) {
589
599
  .action(async () => {
590
600
  showBanner();
591
601
 
602
+ // First-run detection — offer setup wizard
603
+ const ranSetup = await checkFirstRun();
604
+ if (ranSetup) return;
605
+
592
606
  const cfg = getAllConfig();
593
607
  const wallet = cfg.activeWallet;
594
608
 
@@ -621,6 +635,7 @@ export function cli(argv) {
621
635
  ['networks', 'Chain reference & explorers'],
622
636
  ['quickstart', 'Getting started guide'],
623
637
  ['lookup', 'Look up any address on-chain'],
638
+ ['setup', 'First-run setup wizard'],
624
639
  ];
625
640
 
626
641
  commands.forEach(([cmd, desc]) => {
@@ -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,516 @@
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, prompt setup
498
+ */
499
+ export async function checkFirstRun() {
500
+ if (isFirstRun()) {
501
+ console.log('');
502
+ warn('No AI provider configured yet.');
503
+ const { runSetup } = await inquirer.prompt([{
504
+ type: 'confirm',
505
+ name: 'runSetup',
506
+ message: theme.gold('Run setup wizard?'),
507
+ default: true,
508
+ }]);
509
+ if (runSetup) {
510
+ await runSetupWizard();
511
+ return true;
512
+ }
513
+ info('Skip for now. Run later: darksol setup');
514
+ }
515
+ return false;
516
+ }
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.0') +
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.0'));
48
48
  console.log(theme.dim(' ─────────────────────────────'));
49
49
  console.log('');
50
50
  }
@@ -62,3 +62,4 @@ export function showDivider() {
62
62
 
63
63
 
64
64
 
65
+