@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 +11 -9
- package/src/cli.js +15 -0
- package/src/config/keys.js +53 -1
- package/src/setup/wizard.js +516 -0
- package/src/ui/banner.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darksol/terminal",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
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
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -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]) => {
|
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,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.
|
|
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.
|
|
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
|
+
|