@darksol/terminal 0.1.1 → 0.2.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.1.1",
3
+ "version": "0.2.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": {
package/skill/SKILL.md ADDED
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: darksol-terminal
3
+ description: "DARKSOL Terminal — unified CLI for trading, wallets, execution scripts, AI-powered market analysis, secure agent signing, and all DARKSOL services. Use when: (1) swapping/sniping tokens on Base/Ethereum/L2s, (2) managing encrypted wallets, (3) running automated trading scripts, (4) DCA strategies, (5) market intel lookups, (6) on-chain oracle/casino/cards, (7) natural language trading via LLM, (8) signing transactions securely for x402/contracts without exposing private keys."
4
+ ---
5
+
6
+ # DARKSOL Terminal
7
+
8
+ **All DARKSOL services. One terminal. Zero trust required. 🌑**
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -g @darksol/terminal
14
+ ```
15
+
16
+ ## Quick Reference
17
+
18
+ ### Wallet Management
19
+ ```bash
20
+ darksol wallet create <name> # Create new wallet (AES-256-GCM encrypted)
21
+ darksol wallet import <name> # Import from private key
22
+ darksol wallet list # List all wallets
23
+ darksol wallet balance [name] # Check ETH + USDC balance
24
+ darksol wallet use <name> # Set active wallet
25
+ darksol wallet export [name] # Export details (password required for PK)
26
+ ```
27
+
28
+ ### Trading
29
+ ```bash
30
+ darksol trade swap -i ETH -o USDC -a 0.1 # Swap via Uniswap V3
31
+ darksol trade snipe <token> -a 0.05 # Fast buy with gas boost
32
+ darksol trade snipe <token> -a 0.05 -g 2.0 # Snipe with 2x gas priority
33
+ darksol trade watch # Monitor new pairs
34
+ ```
35
+
36
+ ### DCA (Dollar-Cost Averaging)
37
+ ```bash
38
+ darksol dca create # Interactive DCA order creation
39
+ darksol dca list # List active DCA orders
40
+ darksol dca run # Execute pending orders
41
+ darksol dca cancel <id> # Cancel an order
42
+ ```
43
+
44
+ ### AI Trading Assistant
45
+ ```bash
46
+ darksol ai chat # Interactive AI trading chat
47
+ darksol ai ask "buy 0.5 ETH of AERO" # Parse natural language trade intent
48
+ darksol ai strategy VIRTUAL -b 500 # DCA strategy recommendation
49
+ darksol ai analyze AERO # AI-powered token analysis
50
+ ```
51
+
52
+ ### Execution Scripts
53
+ ```bash
54
+ darksol script templates # List available templates
55
+ darksol script create # Create from template (buy, sell, limit-buy, stop-loss, etc.)
56
+ darksol script list # List saved scripts
57
+ darksol script run <name> # Execute (requires wallet password)
58
+ darksol script run <name> -p "pw" -y # Non-interactive (for automation/cron)
59
+ darksol script show <name> # View code + params
60
+ darksol script edit <name> # Edit params/wallet/chain
61
+ darksol script clone <name> <new> # Clone a script
62
+ darksol script delete <name> # Delete a script
63
+ ```
64
+
65
+ Script templates: `buy-token`, `sell-token`, `limit-buy`, `stop-loss`, `multi-buy`, `transfer`, `empty` (custom)
66
+
67
+ ### Market Intel
68
+ ```bash
69
+ darksol market top # Top movers on Base
70
+ darksol market top -c ethereum # Top movers on Ethereum
71
+ darksol market token VIRTUAL # Full token detail
72
+ darksol market compare ETH AERO VIRTUAL # Side-by-side comparison
73
+ ```
74
+
75
+ ### Secure Agent Signer (for OpenClaw / AI agents)
76
+ ```bash
77
+ darksol agent start <wallet> # Start signing proxy
78
+ darksol agent start <wallet> --max-value 0.5 --daily-limit 2.0
79
+ darksol agent docs # Full security documentation
80
+ ```
81
+
82
+ The agent signer creates a local HTTP server at `127.0.0.1:18790` that signs transactions without exposing the private key. AI agents authenticate with a one-time bearer token.
83
+
84
+ **Endpoints:**
85
+ - `GET /address` — wallet address
86
+ - `GET /balance` — ETH balance
87
+ - `POST /send` — sign + broadcast transaction
88
+ - `POST /sign-message` — sign EIP-191 message (x402)
89
+ - `POST /sign-typed-data` — sign EIP-712 typed data (x402)
90
+ - `GET /policy` — spending limits
91
+ - `GET /audit` — operation log
92
+
93
+ ### Oracle
94
+ ```bash
95
+ darksol oracle flip # Coin flip
96
+ darksol oracle dice 20 # Roll d20
97
+ darksol oracle number 1 100 # Random 1-100
98
+ darksol oracle shuffle a b c d # Shuffle list
99
+ ```
100
+
101
+ ### Casino
102
+ ```bash
103
+ darksol casino bet coin-flip heads # Place a bet
104
+ darksol casino tables # View games
105
+ darksol casino stats # House stats
106
+ darksol casino receipt <id> # Verify on-chain
107
+ ```
108
+
109
+ ### Prepaid Cards
110
+ ```bash
111
+ darksol cards catalog # Available providers
112
+ darksol cards order -p swype -a 50 # Order a card
113
+ darksol cards status <id> # Check order
114
+ ```
115
+
116
+ ### Builder Index
117
+ ```bash
118
+ darksol builders leaderboard # ERC-8021 builder rankings
119
+ darksol builders lookup <code> # Builder profile
120
+ darksol builders feed # Recent transactions
121
+ ```
122
+
123
+ ### Facilitator
124
+ ```bash
125
+ darksol facilitator health # Status
126
+ darksol facilitator verify <payment> # Verify off-chain
127
+ darksol facilitator settle <payment> # Settle on-chain (free)
128
+ ```
129
+
130
+ ### API Keys
131
+ ```bash
132
+ darksol keys list # Show all services + status
133
+ darksol keys add openai # Add OpenAI key
134
+ darksol keys add coingecko # Add CoinGecko Pro key
135
+ darksol keys add alchemy # Add Alchemy RPC key
136
+ darksol keys remove <service> # Remove a key
137
+ ```
138
+
139
+ Supported: `openai`, `anthropic`, `openrouter`, `ollama`, `coingecko`, `dexscreener`, `alchemy`, `infura`, `quicknode`, `oneinch`, `paraswap`
140
+
141
+ ### Configuration
142
+ ```bash
143
+ darksol config show # View all settings
144
+ darksol config set chain base # Set active chain
145
+ darksol config set slippage 1.0 # Set slippage %
146
+ darksol config rpc base https://... # Custom RPC endpoint
147
+ ```
148
+
149
+ ### Reference
150
+ ```bash
151
+ darksol tips # Trading + scripting tips
152
+ darksol tips --trading # Trading tips only
153
+ darksol networks # Chain reference table
154
+ darksol quickstart # Getting started guide
155
+ darksol lookup 0x... # Look up address on-chain
156
+ ```
157
+
158
+ ## Supported Chains
159
+ - **Base** (default) — chain ID 8453
160
+ - **Ethereum** — chain ID 1
161
+ - **Polygon** — chain ID 137
162
+ - **Arbitrum** — chain ID 42161
163
+ - **Optimism** — chain ID 10
164
+
165
+ ## Agent Integration Notes
166
+
167
+ - All commands work non-interactively with flags (`-p`, `-y`, `--key`, etc.)
168
+ - Set `darksol config set output json` for programmatic JSON responses
169
+ - Scripts can be executed via cron: `darksol script run my-dca -p "pass" -y`
170
+ - The agent signer is the recommended way to give AI agents wallet access
171
+ - Helper functions available at `@darksol/terminal/src/utils/helpers.js`
172
+
173
+ ## Security
174
+ - Private keys encrypted with AES-256-GCM + scrypt KDF
175
+ - Agent signer: PK never exposed, loopback-only, bearer auth, spending limits
176
+ - Dangerous contract calls (transferOwnership, selfdestruct) blocked by default
177
+ - Full audit logging on all signing operations
package/src/cli.js CHANGED
@@ -15,6 +15,10 @@ import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './servi
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
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';
21
+ import { listSkills, installSkill, skillInfo, uninstallSkill } from './services/skills.js';
18
22
 
19
23
  export function cli(argv) {
20
24
  const program = new Command();
@@ -289,6 +293,126 @@ export function cli(argv) {
289
293
  .description('Settle payment on-chain')
290
294
  .action((payment) => facilitatorSettle(payment));
291
295
 
296
+ // ═══════════════════════════════════════
297
+ // AI / LLM COMMANDS
298
+ // ═══════════════════════════════════════
299
+ const ai = program
300
+ .command('ai')
301
+ .description('AI-powered trading assistant & analysis');
302
+
303
+ ai
304
+ .command('chat')
305
+ .description('Start interactive AI trading chat')
306
+ .option('-p, --provider <name>', 'LLM provider (openai, anthropic, openrouter, ollama)')
307
+ .option('-m, --model <model>', 'Model name')
308
+ .action((opts) => startChat(opts));
309
+
310
+ ai
311
+ .command('ask <prompt...>')
312
+ .description('One-shot AI query')
313
+ .option('-p, --provider <name>', 'LLM provider')
314
+ .option('-m, --model <model>', 'Model name')
315
+ .action(async (promptParts, opts) => {
316
+ const prompt = promptParts.join(' ');
317
+ const result = await parseIntent(prompt, opts);
318
+ if (result.action !== 'error' && result.action !== 'unknown') {
319
+ showSection('PARSED INTENT');
320
+ kvDisplay(Object.entries(result)
321
+ .filter(([k]) => !['raw', 'model'].includes(k))
322
+ .map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)])
323
+ );
324
+ if (result.command) {
325
+ console.log('');
326
+ info(`Suggested command: ${theme.gold(result.command)}`);
327
+ }
328
+ }
329
+ });
330
+
331
+ ai
332
+ .command('strategy <token>')
333
+ .description('Get DCA strategy recommendation')
334
+ .requiredOption('-b, --budget <usd>', 'Total budget in USD')
335
+ .option('-t, --timeframe <period>', 'Investment timeframe', '30 days')
336
+ .option('-p, --provider <name>', 'LLM provider')
337
+ .action((token, opts) => adviseStrategy(token, opts.budget, opts.timeframe, opts));
338
+
339
+ ai
340
+ .command('analyze <token>')
341
+ .description('AI-powered token analysis')
342
+ .option('-p, --provider <name>', 'LLM provider')
343
+ .action((token, opts) => analyzeToken(token, opts));
344
+
345
+ // ═══════════════════════════════════════
346
+ // API KEYS COMMANDS
347
+ // ═══════════════════════════════════════
348
+ const keys = program
349
+ .command('keys')
350
+ .description('API key vault — store keys for LLMs, data providers, RPCs');
351
+
352
+ keys
353
+ .command('list')
354
+ .description('List all services and stored keys')
355
+ .action(() => listKeys());
356
+
357
+ keys
358
+ .command('add <service>')
359
+ .description('Add or update an API key')
360
+ .option('-k, --key <key>', 'API key (or enter interactively)')
361
+ .action((service, opts) => addKey(service, opts));
362
+
363
+ keys
364
+ .command('remove <service>')
365
+ .description('Remove a stored key')
366
+ .action((service) => removeKey(service));
367
+
368
+ // ═══════════════════════════════════════
369
+ // AGENT SIGNER COMMANDS
370
+ // ═══════════════════════════════════════
371
+ const agent = program
372
+ .command('agent')
373
+ .description('Secure agent signer — PK-isolated wallet for AI agents');
374
+
375
+ agent
376
+ .command('start [wallet]')
377
+ .description('Start the agent signing proxy')
378
+ .option('--port <port>', 'Server port', '18790')
379
+ .option('--max-value <eth>', 'Max ETH per transaction', '1.0')
380
+ .option('--daily-limit <eth>', 'Daily spending limit in ETH', '5.0')
381
+ .option('--allowlist <contracts>', 'Comma-separated contract allowlist')
382
+ .action((wallet, opts) => startAgentSigner(wallet, opts));
383
+
384
+ agent
385
+ .command('docs')
386
+ .description('Show agent signer security documentation')
387
+ .action(() => showAgentDocs());
388
+
389
+ // ═══════════════════════════════════════
390
+ // SKILLS COMMANDS
391
+ // ═══════════════════════════════════════
392
+ const skills = program
393
+ .command('skills')
394
+ .description('DARKSOL skills directory — install agent skills');
395
+
396
+ skills
397
+ .command('list')
398
+ .description('List all available DARKSOL skills')
399
+ .action(() => listSkills());
400
+
401
+ skills
402
+ .command('install <name>')
403
+ .description('Install a skill to OpenClaw')
404
+ .action((name) => installSkill(name));
405
+
406
+ skills
407
+ .command('info <name>')
408
+ .description('Show skill details')
409
+ .action((name) => skillInfo(name));
410
+
411
+ skills
412
+ .command('uninstall <name>')
413
+ .description('Uninstall a skill')
414
+ .action((name) => uninstallSkill(name));
415
+
292
416
  // ═══════════════════════════════════════
293
417
  // TIPS & REFERENCE COMMANDS
294
418
  // ═══════════════════════════════════════
@@ -466,6 +590,9 @@ export function cli(argv) {
466
590
  ['wallet', 'Create, import, manage wallets'],
467
591
  ['trade', 'Swap tokens, snipe, trading'],
468
592
  ['dca', 'Dollar-cost averaging orders'],
593
+ ['ai', 'AI trading assistant & analysis'],
594
+ ['agent', 'Secure agent signer (PK-isolated)'],
595
+ ['keys', 'API key vault (LLMs, data, RPCs)'],
469
596
  ['script', 'Execution scripts & strategies'],
470
597
  ['market', 'Market intel & token data'],
471
598
  ['oracle', 'On-chain random oracle'],
@@ -473,6 +600,7 @@ export function cli(argv) {
473
600
  ['cards', 'Prepaid Visa/MC cards'],
474
601
  ['builders', 'ERC-8021 builder index'],
475
602
  ['facilitator', 'x402 payment facilitator'],
603
+ ['skills', 'Agent skill directory & install'],
476
604
  ['config', 'Terminal configuration'],
477
605
  ['tips', 'Trading & scripting tips'],
478
606
  ['networks', 'Chain reference & explorers'],
@@ -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 };