@darksol/terminal 0.1.0 → 0.2.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/README.md CHANGED
@@ -184,6 +184,105 @@ JSON output mode for programmatic use:
184
184
  darksol config set output json
185
185
  ```
186
186
 
187
+ ## Helper Functions
188
+
189
+ When writing custom execution scripts, you have access to powerful helper utilities:
190
+
191
+ ```javascript
192
+ import {
193
+ // Providers & Chain
194
+ getProvider, // Get ethers provider for any chain
195
+ CHAIN_IDS, // { base: 8453, ethereum: 1, ... }
196
+ EXPLORERS, // Block explorer URLs per chain
197
+ txUrl, addressUrl, // Generate explorer links
198
+
199
+ // Tokens
200
+ getERC20, // Get ERC20 contract instance
201
+ getFullTokenInfo, // Name, symbol, decimals, totalSupply
202
+ getTokenBalance, // Formatted balance for any token
203
+ ensureApproval, // Check & approve token spending
204
+ TOKENS, // All known token addresses per chain
205
+ getUSDC, getWETH, // Quick chain-specific lookups
206
+
207
+ // Gas
208
+ estimateGasCost, // Estimate gas in ETH
209
+ getBoostedGas, // Priority gas settings for snipes
210
+
211
+ // Formatting
212
+ formatCompact, // 1234567 → "1.23M"
213
+ formatUSD, // Format as $1,234.56
214
+ formatETH, // Format wei to ETH string
215
+ formatTokenAmount, // Format with symbol
216
+ shortAddress, // 0x1234...5678
217
+ formatDuration, // Seconds → "2h 30m"
218
+
219
+ // Validation
220
+ isValidAddress, // Check Ethereum address
221
+ isValidPrivateKey, // Check private key format
222
+ isValidAmount, // Check positive number
223
+ parseTokenAmount, // String → bigint with decimals
224
+
225
+ // Async
226
+ sleep, // await sleep(1000)
227
+ retry, // Retry with exponential backoff
228
+ waitForTx, // Wait for tx with timeout
229
+
230
+ // Price
231
+ quickPrice, // DexScreener price lookup
232
+ hasLiquidity, // Check minimum liquidity
233
+ } from './utils/helpers.js';
234
+ ```
235
+
236
+ ### Example: Custom Script Using Helpers
237
+
238
+ ```javascript
239
+ module.exports = async function({ signer, provider, ethers, config, params }) {
240
+ // Import helpers (available in script context)
241
+ const helpers = await import('@darksol/terminal/src/utils/helpers.js');
242
+
243
+ // Check if token has enough liquidity
244
+ const liquid = await helpers.hasLiquidity(params.token, 5000);
245
+ if (!liquid) throw new Error('Insufficient liquidity');
246
+
247
+ // Get price
248
+ const price = await helpers.quickPrice(params.token);
249
+ console.log(`Price: ${helpers.formatUSD(price.price)}`);
250
+
251
+ // Get boosted gas for priority
252
+ const gas = await helpers.getBoostedGas(provider, 1.5);
253
+
254
+ // Execute trade with retry
255
+ const result = await helpers.retry(async () => {
256
+ const tx = await signer.sendTransaction({ ...txParams, ...gas });
257
+ return helpers.waitForTx(tx, 60000);
258
+ }, 3, 2000);
259
+
260
+ return { txHash: result.hash, price: price.price };
261
+ };
262
+ ```
263
+
264
+ ## Tips & Reference
265
+
266
+ ```bash
267
+ # Trading tips (slippage, MEV protection, etc.)
268
+ darksol tips --trading
269
+
270
+ # Script writing tips
271
+ darksol tips --scripts
272
+
273
+ # Both
274
+ darksol tips
275
+
276
+ # Network reference (chains, IDs, explorers, USDC addresses)
277
+ darksol networks
278
+
279
+ # Getting started guide
280
+ darksol quickstart
281
+
282
+ # Look up any address (auto-detects token vs wallet)
283
+ darksol lookup 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
284
+ ```
285
+
187
286
  ## Development
188
287
 
189
288
  ```bash
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {
7
- "darksol": "./bin/darksol.js"
7
+ "darksol": "bin/darksol.js"
8
8
  },
9
9
  "main": "./src/cli.js",
10
10
  "scripts": {
@@ -12,7 +12,16 @@
12
12
  "dev": "node bin/darksol.js dashboard",
13
13
  "test": "node --test tests/*.test.js"
14
14
  },
15
- "keywords": ["darksol", "crypto", "trading", "cli", "x402", "base", "ethereum", "defi"],
15
+ "keywords": [
16
+ "darksol",
17
+ "crypto",
18
+ "trading",
19
+ "cli",
20
+ "x402",
21
+ "base",
22
+ "ethereum",
23
+ "defi"
24
+ ],
16
25
  "author": "DARKSOL <chris00claw@gmail.com>",
17
26
  "license": "MIT",
18
27
  "dependencies": {
package/src/cli.js CHANGED
@@ -14,6 +14,10 @@ import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
14
14
  import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
15
15
  import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
16
16
  import { createScript, listScripts, runScript, showScript, editScript, deleteScript, cloneScript, listTemplates } from './scripts/engine.js';
17
+ import { showTradingTips, showScriptTips, showNetworkReference, showQuickStart, showWalletSummary, showTokenInfo, showTxResult } from './utils/helpers.js';
18
+ import { addKey, removeKey, listKeys } from './config/keys.js';
19
+ import { parseIntent, startChat, adviseStrategy, analyzeToken } from './llm/intent.js';
20
+ import { startAgentSigner, showAgentDocs } from './wallet/agent-signer.js';
17
21
 
18
22
  export function cli(argv) {
19
23
  const program = new Command();
@@ -288,6 +292,154 @@ export function cli(argv) {
288
292
  .description('Settle payment on-chain')
289
293
  .action((payment) => facilitatorSettle(payment));
290
294
 
295
+ // ═══════════════════════════════════════
296
+ // AI / LLM COMMANDS
297
+ // ═══════════════════════════════════════
298
+ const ai = program
299
+ .command('ai')
300
+ .description('AI-powered trading assistant & analysis');
301
+
302
+ ai
303
+ .command('chat')
304
+ .description('Start interactive AI trading chat')
305
+ .option('-p, --provider <name>', 'LLM provider (openai, anthropic, openrouter, ollama)')
306
+ .option('-m, --model <model>', 'Model name')
307
+ .action((opts) => startChat(opts));
308
+
309
+ ai
310
+ .command('ask <prompt...>')
311
+ .description('One-shot AI query')
312
+ .option('-p, --provider <name>', 'LLM provider')
313
+ .option('-m, --model <model>', 'Model name')
314
+ .action(async (promptParts, opts) => {
315
+ const prompt = promptParts.join(' ');
316
+ const result = await parseIntent(prompt, opts);
317
+ if (result.action !== 'error' && result.action !== 'unknown') {
318
+ showSection('PARSED INTENT');
319
+ kvDisplay(Object.entries(result)
320
+ .filter(([k]) => !['raw', 'model'].includes(k))
321
+ .map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)])
322
+ );
323
+ if (result.command) {
324
+ console.log('');
325
+ info(`Suggested command: ${theme.gold(result.command)}`);
326
+ }
327
+ }
328
+ });
329
+
330
+ ai
331
+ .command('strategy <token>')
332
+ .description('Get DCA strategy recommendation')
333
+ .requiredOption('-b, --budget <usd>', 'Total budget in USD')
334
+ .option('-t, --timeframe <period>', 'Investment timeframe', '30 days')
335
+ .option('-p, --provider <name>', 'LLM provider')
336
+ .action((token, opts) => adviseStrategy(token, opts.budget, opts.timeframe, opts));
337
+
338
+ ai
339
+ .command('analyze <token>')
340
+ .description('AI-powered token analysis')
341
+ .option('-p, --provider <name>', 'LLM provider')
342
+ .action((token, opts) => analyzeToken(token, opts));
343
+
344
+ // ═══════════════════════════════════════
345
+ // API KEYS COMMANDS
346
+ // ═══════════════════════════════════════
347
+ const keys = program
348
+ .command('keys')
349
+ .description('API key vault — store keys for LLMs, data providers, RPCs');
350
+
351
+ keys
352
+ .command('list')
353
+ .description('List all services and stored keys')
354
+ .action(() => listKeys());
355
+
356
+ keys
357
+ .command('add <service>')
358
+ .description('Add or update an API key')
359
+ .option('-k, --key <key>', 'API key (or enter interactively)')
360
+ .action((service, opts) => addKey(service, opts));
361
+
362
+ keys
363
+ .command('remove <service>')
364
+ .description('Remove a stored key')
365
+ .action((service) => removeKey(service));
366
+
367
+ // ═══════════════════════════════════════
368
+ // AGENT SIGNER COMMANDS
369
+ // ═══════════════════════════════════════
370
+ const agent = program
371
+ .command('agent')
372
+ .description('Secure agent signer — PK-isolated wallet for AI agents');
373
+
374
+ agent
375
+ .command('start [wallet]')
376
+ .description('Start the agent signing proxy')
377
+ .option('--port <port>', 'Server port', '18790')
378
+ .option('--max-value <eth>', 'Max ETH per transaction', '1.0')
379
+ .option('--daily-limit <eth>', 'Daily spending limit in ETH', '5.0')
380
+ .option('--allowlist <contracts>', 'Comma-separated contract allowlist')
381
+ .action((wallet, opts) => startAgentSigner(wallet, opts));
382
+
383
+ agent
384
+ .command('docs')
385
+ .description('Show agent signer security documentation')
386
+ .action(() => showAgentDocs());
387
+
388
+ // ═══════════════════════════════════════
389
+ // TIPS & REFERENCE COMMANDS
390
+ // ═══════════════════════════════════════
391
+ program
392
+ .command('tips')
393
+ .description('Show trading and script writing tips')
394
+ .option('-t, --trading', 'Trading tips only')
395
+ .option('-s, --scripts', 'Script writing tips only')
396
+ .action((opts) => {
397
+ showMiniBanner();
398
+ if (opts.scripts) {
399
+ showScriptTips();
400
+ } else if (opts.trading) {
401
+ showTradingTips();
402
+ } else {
403
+ showTradingTips();
404
+ showScriptTips();
405
+ }
406
+ });
407
+
408
+ program
409
+ .command('networks')
410
+ .description('Show supported networks and chain info')
411
+ .action(() => {
412
+ showMiniBanner();
413
+ showNetworkReference();
414
+ });
415
+
416
+ program
417
+ .command('quickstart')
418
+ .description('Show getting started guide')
419
+ .action(() => {
420
+ showMiniBanner();
421
+ showQuickStart();
422
+ });
423
+
424
+ program
425
+ .command('lookup <address>')
426
+ .description('Look up a token or wallet address on-chain')
427
+ .option('-c, --chain <chain>', 'Chain to query')
428
+ .action(async (address, opts) => {
429
+ showMiniBanner();
430
+ if (address.length === 42 && address.startsWith('0x')) {
431
+ // Could be token or wallet — try token first
432
+ try {
433
+ await showTokenInfo(address, opts.chain);
434
+ } catch {
435
+ await showWalletSummary(address, opts.chain);
436
+ }
437
+ } else {
438
+ const { error } = await import('./ui/components.js');
439
+ error('Provide a valid 0x address');
440
+ }
441
+ });
442
+
291
443
  // ═══════════════════════════════════════
292
444
  // SCRIPT COMMANDS
293
445
  // ═══════════════════════════════════════
@@ -410,6 +562,9 @@ export function cli(argv) {
410
562
  ['wallet', 'Create, import, manage wallets'],
411
563
  ['trade', 'Swap tokens, snipe, trading'],
412
564
  ['dca', 'Dollar-cost averaging orders'],
565
+ ['ai', 'AI trading assistant & analysis'],
566
+ ['agent', 'Secure agent signer (PK-isolated)'],
567
+ ['keys', 'API key vault (LLMs, data, RPCs)'],
413
568
  ['script', 'Execution scripts & strategies'],
414
569
  ['market', 'Market intel & token data'],
415
570
  ['oracle', 'On-chain random oracle'],
@@ -418,6 +573,10 @@ export function cli(argv) {
418
573
  ['builders', 'ERC-8021 builder index'],
419
574
  ['facilitator', 'x402 payment facilitator'],
420
575
  ['config', 'Terminal configuration'],
576
+ ['tips', 'Trading & scripting tips'],
577
+ ['networks', 'Chain reference & explorers'],
578
+ ['quickstart', 'Getting started guide'],
579
+ ['lookup', 'Look up any address on-chain'],
421
580
  ];
422
581
 
423
582
  commands.forEach(([cmd, desc]) => {
@@ -0,0 +1,320 @@
1
+ import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import { theme } from '../ui/theme.js';
6
+ import { kvDisplay, success, error, warn, info } from '../ui/components.js';
7
+ import { showSection } from '../ui/banner.js';
8
+ import inquirer from 'inquirer';
9
+
10
+ const KEYS_DIR = join(homedir(), '.darksol', 'keys');
11
+ const KEYS_FILE = join(KEYS_DIR, 'vault.json');
12
+ const ALGORITHM = 'aes-256-gcm';
13
+ const SCRYPT_N = 2 ** 16; // lighter for keys (faster unlock)
14
+ const SCRYPT_r = 8;
15
+ const SCRYPT_p = 1;
16
+ const SCRYPT_MAXMEM = 512 * 1024 * 1024;
17
+
18
+ function ensureDir() {
19
+ if (!existsSync(KEYS_DIR)) mkdirSync(KEYS_DIR, { recursive: true });
20
+ }
21
+
22
+ function encrypt(value, password) {
23
+ const salt = randomBytes(32);
24
+ const iv = randomBytes(16);
25
+ const key = scryptSync(password, salt, 32, { N: SCRYPT_N, r: SCRYPT_r, p: SCRYPT_p, maxmem: SCRYPT_MAXMEM });
26
+ const cipher = createCipheriv(ALGORITHM, key, iv);
27
+ let encrypted = cipher.update(value, 'utf8', 'hex');
28
+ encrypted += cipher.final('hex');
29
+ return {
30
+ salt: salt.toString('hex'),
31
+ iv: iv.toString('hex'),
32
+ tag: cipher.getAuthTag().toString('hex'),
33
+ data: encrypted,
34
+ };
35
+ }
36
+
37
+ function decrypt(entry, password) {
38
+ const key = scryptSync(password, Buffer.from(entry.salt, 'hex'), 32, { N: SCRYPT_N, r: SCRYPT_r, p: SCRYPT_p, maxmem: SCRYPT_MAXMEM });
39
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(entry.iv, 'hex'));
40
+ decipher.setAuthTag(Buffer.from(entry.tag, 'hex'));
41
+ let decrypted = decipher.update(entry.data, 'hex', 'utf8');
42
+ decrypted += decipher.final('utf8');
43
+ return decrypted;
44
+ }
45
+
46
+ function loadVault() {
47
+ ensureDir();
48
+ if (!existsSync(KEYS_FILE)) return { version: 1, keys: {} };
49
+ return JSON.parse(readFileSync(KEYS_FILE, 'utf8'));
50
+ }
51
+
52
+ function saveVault(vault) {
53
+ ensureDir();
54
+ writeFileSync(KEYS_FILE, JSON.stringify(vault, null, 2));
55
+ }
56
+
57
+ // ──────────────────────────────────────────────────
58
+ // SUPPORTED API SERVICES
59
+ // ──────────────────────────────────────────────────
60
+
61
+ export const SERVICES = {
62
+ // LLM Providers
63
+ openai: {
64
+ name: 'OpenAI',
65
+ category: 'llm',
66
+ description: 'GPT-4o, GPT-5 — natural language trading, strategy advisor',
67
+ envVar: 'OPENAI_API_KEY',
68
+ docsUrl: 'https://platform.openai.com/api-keys',
69
+ validate: (key) => key.startsWith('sk-'),
70
+ },
71
+ anthropic: {
72
+ name: 'Anthropic',
73
+ category: 'llm',
74
+ description: 'Claude Opus, Sonnet — intent parsing, analysis',
75
+ envVar: 'ANTHROPIC_API_KEY',
76
+ docsUrl: 'https://console.anthropic.com/settings/keys',
77
+ validate: (key) => key.startsWith('sk-ant-'),
78
+ },
79
+ openrouter: {
80
+ name: 'OpenRouter',
81
+ category: 'llm',
82
+ description: 'Multi-model gateway — any LLM via one key',
83
+ envVar: 'OPENROUTER_API_KEY',
84
+ docsUrl: 'https://openrouter.ai/keys',
85
+ validate: (key) => key.startsWith('sk-or-'),
86
+ },
87
+ ollama: {
88
+ name: 'Ollama (Local)',
89
+ category: 'llm',
90
+ description: 'Local models — free, private, no API key needed',
91
+ envVar: 'OLLAMA_HOST',
92
+ docsUrl: 'https://ollama.ai',
93
+ validate: (key) => key.startsWith('http'),
94
+ },
95
+
96
+ // Data Providers
97
+ coingecko: {
98
+ name: 'CoinGecko Pro',
99
+ category: 'data',
100
+ description: 'Pro/Demo API — higher rate limits, more endpoints',
101
+ envVar: 'COINGECKO_API_KEY',
102
+ docsUrl: 'https://www.coingecko.com/en/api/pricing',
103
+ validate: (key) => key.length > 10,
104
+ },
105
+ dexscreener: {
106
+ name: 'DexScreener',
107
+ category: 'data',
108
+ description: 'Enhanced DEX data — paid tier for higher limits',
109
+ envVar: 'DEXSCREENER_API_KEY',
110
+ docsUrl: 'https://docs.dexscreener.com',
111
+ validate: (key) => key.length > 10,
112
+ },
113
+ defillama: {
114
+ name: 'DefiLlama',
115
+ category: 'data',
116
+ description: 'TVL, yield, protocol data — free, no key needed',
117
+ envVar: null,
118
+ docsUrl: 'https://defillama.com/docs/api',
119
+ validate: () => true,
120
+ },
121
+
122
+ // RPC Providers (OAuth/API key)
123
+ alchemy: {
124
+ name: 'Alchemy',
125
+ category: 'rpc',
126
+ description: 'Premium RPC — faster, more reliable, trace APIs',
127
+ envVar: 'ALCHEMY_API_KEY',
128
+ docsUrl: 'https://dashboard.alchemy.com',
129
+ validate: (key) => key.length > 10,
130
+ },
131
+ infura: {
132
+ name: 'Infura',
133
+ category: 'rpc',
134
+ description: 'RPC provider — Ethereum, Polygon, Arbitrum, Optimism',
135
+ envVar: 'INFURA_API_KEY',
136
+ docsUrl: 'https://app.infura.io',
137
+ validate: (key) => key.length > 10,
138
+ },
139
+ quicknode: {
140
+ name: 'QuickNode',
141
+ category: 'rpc',
142
+ description: 'High-performance RPC — WebSocket support, trace',
143
+ envVar: 'QUICKNODE_API_KEY',
144
+ docsUrl: 'https://dashboard.quicknode.com',
145
+ validate: (key) => key.length > 10,
146
+ },
147
+
148
+ // Trading & Auth
149
+ oneinch: {
150
+ name: '1inch',
151
+ category: 'trading',
152
+ description: 'DEX aggregator API — best swap routing',
153
+ envVar: 'ONEINCH_API_KEY',
154
+ docsUrl: 'https://portal.1inch.dev',
155
+ validate: (key) => key.length > 10,
156
+ },
157
+ paraswap: {
158
+ name: 'ParaSwap',
159
+ category: 'trading',
160
+ description: 'DEX aggregator — competitive routing',
161
+ envVar: 'PARASWAP_API_KEY',
162
+ docsUrl: 'https://developers.paraswap.network',
163
+ validate: (key) => key.length > 5,
164
+ },
165
+ };
166
+
167
+ // ──────────────────────────────────────────────────
168
+ // KEY MANAGEMENT
169
+ // ──────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Add or update an API key
173
+ */
174
+ export async function addKey(service, opts = {}) {
175
+ const svc = SERVICES[service];
176
+ if (!svc) {
177
+ error(`Unknown service: ${service}. Run: darksol keys list`);
178
+ return;
179
+ }
180
+
181
+ let apiKey = opts.key;
182
+ if (!apiKey) {
183
+ const { key } = await inquirer.prompt([{
184
+ type: 'password',
185
+ name: 'key',
186
+ message: theme.gold(`${svc.name} API key:`),
187
+ mask: '●',
188
+ validate: (v) => {
189
+ if (!v) return 'Key required';
190
+ if (svc.validate && !svc.validate(v)) return `Invalid format for ${svc.name}`;
191
+ return true;
192
+ },
193
+ }]);
194
+ apiKey = key;
195
+ }
196
+
197
+ // Get vault password
198
+ let vaultPass = opts.password;
199
+ if (!vaultPass) {
200
+ const { password } = await inquirer.prompt([{
201
+ type: 'password',
202
+ name: 'password',
203
+ message: theme.gold('Vault password:'),
204
+ mask: '●',
205
+ validate: (v) => v.length >= 6 || 'Minimum 6 characters',
206
+ }]);
207
+ vaultPass = password;
208
+ }
209
+
210
+ const vault = loadVault();
211
+ vault.keys[service] = {
212
+ encrypted: encrypt(apiKey, vaultPass),
213
+ service: svc.name,
214
+ category: svc.category,
215
+ addedAt: new Date().toISOString(),
216
+ };
217
+ saveVault(vault);
218
+
219
+ success(`${svc.name} key stored securely`);
220
+ if (svc.envVar) {
221
+ info(`Also available via env: ${svc.envVar}`);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Get a decrypted API key
227
+ */
228
+ export async function getKey(service, password) {
229
+ const vault = loadVault();
230
+ const entry = vault.keys[service];
231
+
232
+ if (!entry) {
233
+ // Fall back to environment variable
234
+ const svc = SERVICES[service];
235
+ if (svc?.envVar && process.env[svc.envVar]) {
236
+ return process.env[svc.envVar];
237
+ }
238
+ return null;
239
+ }
240
+
241
+ try {
242
+ return decrypt(entry.encrypted, password);
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Get a key without password (tries env var first, then cached session)
250
+ */
251
+ export function getKeyFromEnv(service) {
252
+ const svc = SERVICES[service];
253
+ if (svc?.envVar && process.env[svc.envVar]) {
254
+ return process.env[svc.envVar];
255
+ }
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * Remove a key
261
+ */
262
+ export async function removeKey(service) {
263
+ const vault = loadVault();
264
+ if (!vault.keys[service]) {
265
+ error(`No key stored for: ${service}`);
266
+ return;
267
+ }
268
+ const svc = SERVICES[service];
269
+ const { confirm } = await inquirer.prompt([{
270
+ type: 'confirm',
271
+ name: 'confirm',
272
+ message: theme.accent(`Remove ${svc?.name || service} key?`),
273
+ default: false,
274
+ }]);
275
+ if (!confirm) return;
276
+
277
+ delete vault.keys[service];
278
+ saveVault(vault);
279
+ success(`${svc?.name || service} key removed`);
280
+ }
281
+
282
+ /**
283
+ * List all services and stored keys
284
+ */
285
+ export function listKeys() {
286
+ const vault = loadVault();
287
+
288
+ showSection('API KEY VAULT');
289
+
290
+ const categories = ['llm', 'data', 'rpc', 'trading'];
291
+ const catNames = { llm: '🧠 LLM PROVIDERS', data: '📊 DATA PROVIDERS', rpc: '🌐 RPC PROVIDERS', trading: '📈 TRADING' };
292
+
293
+ for (const cat of categories) {
294
+ console.log('');
295
+ console.log(theme.gold(` ${catNames[cat]}`));
296
+
297
+ const services = Object.entries(SERVICES).filter(([, s]) => s.category === cat);
298
+ for (const [key, svc] of services) {
299
+ const stored = vault.keys[key];
300
+ const envKey = svc.envVar ? getKeyFromEnv(key) : null;
301
+ let status;
302
+
303
+ if (stored) {
304
+ status = theme.success('● Stored');
305
+ } else if (envKey) {
306
+ status = theme.info('● Env');
307
+ } else {
308
+ status = theme.dim('○ Not set');
309
+ }
310
+
311
+ console.log(` ${status} ${theme.label(svc.name.padEnd(18))} ${theme.dim(svc.description)}`);
312
+ }
313
+ }
314
+
315
+ console.log('');
316
+ info('Add a key: darksol keys add <service>');
317
+ info('Services: ' + Object.keys(SERVICES).join(', '));
318
+ }
319
+
320
+ export { KEYS_DIR, KEYS_FILE };