@darksol/terminal 0.1.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.1.1",
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": {
package/src/cli.js CHANGED
@@ -15,6 +15,9 @@ 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';
18
21
 
19
22
  export function cli(argv) {
20
23
  const program = new Command();
@@ -289,6 +292,99 @@ export function cli(argv) {
289
292
  .description('Settle payment on-chain')
290
293
  .action((payment) => facilitatorSettle(payment));
291
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
+
292
388
  // ═══════════════════════════════════════
293
389
  // TIPS & REFERENCE COMMANDS
294
390
  // ═══════════════════════════════════════
@@ -466,6 +562,9 @@ export function cli(argv) {
466
562
  ['wallet', 'Create, import, manage wallets'],
467
563
  ['trade', 'Swap tokens, snipe, trading'],
468
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)'],
469
568
  ['script', 'Execution scripts & strategies'],
470
569
  ['market', 'Market intel & token data'],
471
570
  ['oracle', 'On-chain random oracle'],
@@ -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 };