@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 +1 -1
- package/src/cli.js +99 -0
- package/src/config/keys.js +320 -0
- package/src/llm/engine.js +286 -0
- package/src/llm/intent.js +310 -0
- package/src/ui/banner.js +3 -2
- package/src/wallet/agent-signer.js +556 -0
package/package.json
CHANGED
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 };
|