@trading-boy/cli 1.12.0 → 2.0.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 +50 -22
- package/dist/api-client.d.ts +4 -7
- package/dist/api-client.js +8 -13
- package/dist/cli.bundle.js +1896 -33657
- package/dist/credentials.js +1 -1
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -24
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +12 -0
- package/dist/utils.js +3 -3
- package/package.json +20 -5
- package/dist/cli.d.ts +0 -5
- package/dist/cli.js +0 -157
- package/dist/commands/agent-cmd.d.ts +0 -9
- package/dist/commands/agent-cmd.js +0 -567
- package/dist/commands/audit.d.ts +0 -18
- package/dist/commands/audit.js +0 -73
- package/dist/commands/behavioral.d.ts +0 -73
- package/dist/commands/behavioral.js +0 -349
- package/dist/commands/benchmark-cmd.d.ts +0 -3
- package/dist/commands/benchmark-cmd.js +0 -191
- package/dist/commands/billing.d.ts +0 -12
- package/dist/commands/billing.js +0 -142
- package/dist/commands/catalysts.d.ts +0 -17
- package/dist/commands/catalysts.js +0 -151
- package/dist/commands/coaching-cmd.d.ts +0 -16
- package/dist/commands/coaching-cmd.js +0 -222
- package/dist/commands/config-cmd.d.ts +0 -30
- package/dist/commands/config-cmd.js +0 -515
- package/dist/commands/connect-chatgpt.d.ts +0 -5
- package/dist/commands/connect-chatgpt.js +0 -293
- package/dist/commands/connect-claude.d.ts +0 -5
- package/dist/commands/connect-claude.js +0 -280
- package/dist/commands/context.d.ts +0 -41
- package/dist/commands/context.js +0 -405
- package/dist/commands/cron-cmd.d.ts +0 -3
- package/dist/commands/cron-cmd.js +0 -305
- package/dist/commands/decisions.d.ts +0 -57
- package/dist/commands/decisions.js +0 -364
- package/dist/commands/edge-cmd.d.ts +0 -78
- package/dist/commands/edge-cmd.js +0 -183
- package/dist/commands/edge-guard-cmd.d.ts +0 -36
- package/dist/commands/edge-guard-cmd.js +0 -169
- package/dist/commands/events.d.ts +0 -3
- package/dist/commands/events.js +0 -117
- package/dist/commands/infra.d.ts +0 -24
- package/dist/commands/infra.js +0 -137
- package/dist/commands/journal.d.ts +0 -3
- package/dist/commands/journal.js +0 -302
- package/dist/commands/login.d.ts +0 -18
- package/dist/commands/login.js +0 -127
- package/dist/commands/logout.d.ts +0 -8
- package/dist/commands/logout.js +0 -108
- package/dist/commands/narratives.d.ts +0 -3
- package/dist/commands/narratives.js +0 -259
- package/dist/commands/onboarding.d.ts +0 -7
- package/dist/commands/onboarding.js +0 -281
- package/dist/commands/query.d.ts +0 -32
- package/dist/commands/query.js +0 -135
- package/dist/commands/replay-cmd.d.ts +0 -43
- package/dist/commands/replay-cmd.js +0 -184
- package/dist/commands/review.d.ts +0 -3
- package/dist/commands/review.js +0 -443
- package/dist/commands/risk.d.ts +0 -47
- package/dist/commands/risk.js +0 -158
- package/dist/commands/social.d.ts +0 -43
- package/dist/commands/social.js +0 -318
- package/dist/commands/soul-wizard.d.ts +0 -29
- package/dist/commands/soul-wizard.js +0 -155
- package/dist/commands/strategy-cmd.d.ts +0 -44
- package/dist/commands/strategy-cmd.js +0 -335
- package/dist/commands/subscribe.d.ts +0 -78
- package/dist/commands/subscribe.js +0 -552
- package/dist/commands/suggestions-cmd.d.ts +0 -24
- package/dist/commands/suggestions-cmd.js +0 -148
- package/dist/commands/thesis-cmd.d.ts +0 -3
- package/dist/commands/thesis-cmd.js +0 -129
- package/dist/commands/trader.d.ts +0 -30
- package/dist/commands/trader.js +0 -971
- package/dist/commands/watch.d.ts +0 -16
- package/dist/commands/watch.js +0 -104
- package/dist/commands/whoami.d.ts +0 -14
- package/dist/commands/whoami.js +0 -105
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
import { Option } from 'commander';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { createLogger } from '@trading-boy/core';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
import { apiRequest, ApiError } from '../api-client.js';
|
|
7
|
-
import { padRight } from '../utils.js';
|
|
8
|
-
// ─── Logger ───
|
|
9
|
-
const logger = createLogger('cli-config');
|
|
10
|
-
// ─── Sensitive Keys ───
|
|
11
|
-
/** Keys whose values should be redacted in `config show` output. */
|
|
12
|
-
const SENSITIVE_KEYS = new Set([
|
|
13
|
-
'NEO4J_PASSWORD',
|
|
14
|
-
'TIMESCALE_PASSWORD',
|
|
15
|
-
'REDIS_PASSWORD',
|
|
16
|
-
'HELIUS_API_KEY',
|
|
17
|
-
'COINGECKO_API_KEY',
|
|
18
|
-
'ALCHEMY_API_KEY',
|
|
19
|
-
'GLASSNODE_API_KEY',
|
|
20
|
-
'TWITTER_BEARER_TOKEN',
|
|
21
|
-
'ANTHROPIC_API_KEY',
|
|
22
|
-
'LLM_API_KEY',
|
|
23
|
-
'LLM_FALLBACK_API_KEY',
|
|
24
|
-
'BELIEF_HMAC_KEY',
|
|
25
|
-
'STRIPE_SECRET_KEY',
|
|
26
|
-
'STRIPE_WEBHOOK_SECRET',
|
|
27
|
-
'TELEGRAM_BOT_TOKEN',
|
|
28
|
-
'RESEND_API_KEY',
|
|
29
|
-
'SMTP_PASS',
|
|
30
|
-
'API_KEY_HASHES',
|
|
31
|
-
'LLM_ENCRYPTION_KEY',
|
|
32
|
-
]);
|
|
33
|
-
/** Valid config keys (from the Zod schema). */
|
|
34
|
-
const VALID_KEYS = [
|
|
35
|
-
'NODE_ENV',
|
|
36
|
-
'LOG_LEVEL',
|
|
37
|
-
'NEO4J_URI',
|
|
38
|
-
'NEO4J_USER',
|
|
39
|
-
'NEO4J_PASSWORD',
|
|
40
|
-
'TIMESCALE_HOST',
|
|
41
|
-
'TIMESCALE_PORT',
|
|
42
|
-
'TIMESCALE_USER',
|
|
43
|
-
'TIMESCALE_PASSWORD',
|
|
44
|
-
'TIMESCALE_DB',
|
|
45
|
-
'REDIS_URL',
|
|
46
|
-
'REDIS_PASSWORD',
|
|
47
|
-
'HELIUS_API_KEY',
|
|
48
|
-
'COINGECKO_API_KEY',
|
|
49
|
-
'ALCHEMY_API_KEY',
|
|
50
|
-
'GLASSNODE_API_KEY',
|
|
51
|
-
'TWITTER_BEARER_TOKEN',
|
|
52
|
-
'ANTHROPIC_API_KEY',
|
|
53
|
-
'LLM_PROVIDER',
|
|
54
|
-
'LLM_API_KEY',
|
|
55
|
-
'LLM_BASE_URL',
|
|
56
|
-
'LLM_MODEL',
|
|
57
|
-
'LLM_FALLBACK_PROVIDER',
|
|
58
|
-
'LLM_FALLBACK_MODEL',
|
|
59
|
-
'LLM_FALLBACK_API_KEY',
|
|
60
|
-
'LLM_MAX_RETRIES',
|
|
61
|
-
'LLM_TIMEOUT_MS',
|
|
62
|
-
'LLM_ENCRYPTION_KEY',
|
|
63
|
-
'BELIEF_HMAC_KEY',
|
|
64
|
-
'ENABLE_LLM',
|
|
65
|
-
'ENABLE_LEARNING',
|
|
66
|
-
'ENABLE_SOCIAL',
|
|
67
|
-
'API_PORT',
|
|
68
|
-
'TELEGRAM_BOT_TOKEN',
|
|
69
|
-
'TELEGRAM_ALLOWED_CHAT_IDS',
|
|
70
|
-
'RESEND_API_KEY',
|
|
71
|
-
'EMAIL_FROM_ADDRESS',
|
|
72
|
-
'EMAIL_FROM_NAME',
|
|
73
|
-
'SMTP_HOST',
|
|
74
|
-
'SMTP_PORT',
|
|
75
|
-
'SMTP_USER',
|
|
76
|
-
'SMTP_PASS',
|
|
77
|
-
'NOTIFICATION_FROM_EMAIL',
|
|
78
|
-
'STRIPE_SECRET_KEY',
|
|
79
|
-
'STRIPE_WEBHOOK_SECRET',
|
|
80
|
-
'STRIPE_PRICE_ID_STARTER',
|
|
81
|
-
'STRIPE_PRICE_ID_PRO',
|
|
82
|
-
'STRIPE_PRICE_ID_EDGE',
|
|
83
|
-
'APP_URL',
|
|
84
|
-
'API_KEY_HASHES',
|
|
85
|
-
'API_KEY_AUTH_ENABLED',
|
|
86
|
-
];
|
|
87
|
-
// ─── Formatters ───
|
|
88
|
-
/**
|
|
89
|
-
* Redact a config value if it's a sensitive key.
|
|
90
|
-
* Shows first 3 chars + '****' for non-empty sensitive values.
|
|
91
|
-
*/
|
|
92
|
-
export function redactValue(key, value) {
|
|
93
|
-
const strValue = String(value);
|
|
94
|
-
if (!SENSITIVE_KEYS.has(key)) {
|
|
95
|
-
return strValue;
|
|
96
|
-
}
|
|
97
|
-
// Empty values shown as placeholder
|
|
98
|
-
if (strValue === '') {
|
|
99
|
-
return chalk.dim('(empty)');
|
|
100
|
-
}
|
|
101
|
-
// Redact: show first 3 chars + ****
|
|
102
|
-
if (strValue.length <= 3) {
|
|
103
|
-
return '****';
|
|
104
|
-
}
|
|
105
|
-
return strValue.slice(0, 3) + '****';
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Format the full config for display.
|
|
109
|
-
*/
|
|
110
|
-
export function formatConfigOutput(config) {
|
|
111
|
-
const lines = [];
|
|
112
|
-
lines.push('');
|
|
113
|
-
lines.push(chalk.bold.cyan(' Configuration'));
|
|
114
|
-
lines.push(chalk.gray(' ' + '\u2500'.repeat(60)));
|
|
115
|
-
lines.push('');
|
|
116
|
-
// Group config keys by category
|
|
117
|
-
const groups = [
|
|
118
|
-
{ name: 'Runtime', keys: ['NODE_ENV', 'LOG_LEVEL'] },
|
|
119
|
-
{ name: 'Neo4j', keys: ['NEO4J_URI', 'NEO4J_USER', 'NEO4J_PASSWORD'] },
|
|
120
|
-
{ name: 'TimescaleDB', keys: ['TIMESCALE_HOST', 'TIMESCALE_PORT', 'TIMESCALE_USER', 'TIMESCALE_PASSWORD', 'TIMESCALE_DB'] },
|
|
121
|
-
{ name: 'Redis', keys: ['REDIS_URL', 'REDIS_PASSWORD'] },
|
|
122
|
-
{ name: 'Data Sources', keys: ['HELIUS_API_KEY', 'COINGECKO_API_KEY', 'ALCHEMY_API_KEY', 'GLASSNODE_API_KEY', 'TWITTER_BEARER_TOKEN'] },
|
|
123
|
-
{ name: 'AI / LLM', keys: ['ANTHROPIC_API_KEY', 'LLM_PROVIDER', 'LLM_API_KEY', 'LLM_BASE_URL', 'LLM_MODEL', 'LLM_FALLBACK_PROVIDER', 'LLM_FALLBACK_MODEL', 'LLM_FALLBACK_API_KEY', 'LLM_MAX_RETRIES', 'LLM_TIMEOUT_MS', 'LLM_ENCRYPTION_KEY', 'ENABLE_LLM'] },
|
|
124
|
-
{ name: 'Learning', keys: ['BELIEF_HMAC_KEY', 'ENABLE_LEARNING', 'ENABLE_SOCIAL'] },
|
|
125
|
-
{ name: 'API', keys: ['API_PORT', 'API_KEY_HASHES', 'API_KEY_AUTH_ENABLED'] },
|
|
126
|
-
{ name: 'Billing', keys: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', 'STRIPE_PRICE_ID_STARTER', 'STRIPE_PRICE_ID_PRO', 'STRIPE_PRICE_ID_EDGE', 'APP_URL'] },
|
|
127
|
-
{ name: 'Telegram', keys: ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_ALLOWED_CHAT_IDS'] },
|
|
128
|
-
{ name: 'Email', keys: ['RESEND_API_KEY', 'EMAIL_FROM_ADDRESS', 'EMAIL_FROM_NAME', 'SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'NOTIFICATION_FROM_EMAIL'] },
|
|
129
|
-
];
|
|
130
|
-
const configRecord = config;
|
|
131
|
-
for (const group of groups) {
|
|
132
|
-
lines.push(chalk.bold(` ${group.name}`));
|
|
133
|
-
for (const key of group.keys) {
|
|
134
|
-
const value = configRecord[key];
|
|
135
|
-
const displayValue = value !== undefined ? redactValue(key, value) : chalk.dim('(not set)');
|
|
136
|
-
lines.push(` ${chalk.gray(padRight(key, 26))} ${displayValue}`);
|
|
137
|
-
}
|
|
138
|
-
lines.push('');
|
|
139
|
-
}
|
|
140
|
-
return lines.join('\n');
|
|
141
|
-
}
|
|
142
|
-
// ─── .env File Operations ───
|
|
143
|
-
/**
|
|
144
|
-
* Resolve the .env file path from the project root.
|
|
145
|
-
*/
|
|
146
|
-
export function resolveEnvPath() {
|
|
147
|
-
return resolve(process.cwd(), '.env');
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Parse a .env file into a Map of key-value pairs.
|
|
151
|
-
* Preserves comments and blank lines by returning the full lines array too.
|
|
152
|
-
*/
|
|
153
|
-
export function parseEnvFile(filePath) {
|
|
154
|
-
const entries = new Map();
|
|
155
|
-
const lines = [];
|
|
156
|
-
if (!existsSync(filePath)) {
|
|
157
|
-
return { entries, lines };
|
|
158
|
-
}
|
|
159
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
160
|
-
const rawLines = content.split('\n');
|
|
161
|
-
for (const line of rawLines) {
|
|
162
|
-
lines.push(line);
|
|
163
|
-
const trimmed = line.trim();
|
|
164
|
-
// Skip comments and empty lines
|
|
165
|
-
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
const eqIndex = trimmed.indexOf('=');
|
|
169
|
-
if (eqIndex > 0) {
|
|
170
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
171
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
172
|
-
// Remove surrounding quotes if present
|
|
173
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
174
|
-
value = value.slice(1, -1);
|
|
175
|
-
}
|
|
176
|
-
entries.set(key, value);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return { entries, lines };
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Write a key-value pair to the .env file.
|
|
183
|
-
* If the key already exists, update the line; otherwise append.
|
|
184
|
-
*/
|
|
185
|
-
export function writeEnvValue(filePath, key, value) {
|
|
186
|
-
if (/[\n\r\0]/.test(value)) {
|
|
187
|
-
throw new Error('Config value must not contain newlines or null bytes');
|
|
188
|
-
}
|
|
189
|
-
const { entries, lines } = parseEnvFile(filePath);
|
|
190
|
-
if (entries.has(key)) {
|
|
191
|
-
// Update existing line
|
|
192
|
-
const updatedLines = lines.map((line) => {
|
|
193
|
-
const trimmed = line.trim();
|
|
194
|
-
if (trimmed.startsWith('#') || trimmed === '')
|
|
195
|
-
return line;
|
|
196
|
-
const eqIndex = trimmed.indexOf('=');
|
|
197
|
-
if (eqIndex > 0) {
|
|
198
|
-
const lineKey = trimmed.slice(0, eqIndex).trim();
|
|
199
|
-
if (lineKey === key) {
|
|
200
|
-
return `${key}=${value}`;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return line;
|
|
204
|
-
});
|
|
205
|
-
writeFileSync(filePath, updatedLines.join('\n'));
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
// Append to end of file
|
|
209
|
-
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
210
|
-
const separator = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
211
|
-
writeFileSync(filePath, existing + separator + `${key}=${value}\n`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// ─── Command Registration ───
|
|
215
|
-
export function registerConfigCommand(program) {
|
|
216
|
-
const configCmd = program
|
|
217
|
-
.command('config')
|
|
218
|
-
.description('Configuration management commands');
|
|
219
|
-
// ─── config show ───
|
|
220
|
-
configCmd
|
|
221
|
-
.command('show')
|
|
222
|
-
.description('Display current configuration')
|
|
223
|
-
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
224
|
-
.addOption(new Option('--all', 'Show all config variables (including server/infrastructure)'))
|
|
225
|
-
.action((options) => {
|
|
226
|
-
// User-facing config: only what a CLI user cares about
|
|
227
|
-
const USER_CONFIG = [
|
|
228
|
-
{ section: 'Connection', keys: [
|
|
229
|
-
{ key: 'TRADING_BOY_API_URL', label: 'API URL' },
|
|
230
|
-
] },
|
|
231
|
-
{ section: 'LLM (Coaching)', keys: [
|
|
232
|
-
{ key: 'LLM_PROVIDER', label: 'Provider' },
|
|
233
|
-
{ key: 'LLM_MODEL', label: 'Model' },
|
|
234
|
-
{ key: 'LLM_API_KEY', label: 'API Key' },
|
|
235
|
-
{ key: 'LLM_BASE_URL', label: 'Base URL' },
|
|
236
|
-
] },
|
|
237
|
-
{ section: 'Notifications', keys: [
|
|
238
|
-
{ key: 'TELEGRAM_BOT_TOKEN', label: 'Telegram Bot' },
|
|
239
|
-
{ key: 'TELEGRAM_ALLOWED_CHAT_IDS', label: 'Telegram Chat IDs' },
|
|
240
|
-
] },
|
|
241
|
-
];
|
|
242
|
-
const getEnv = (key) => {
|
|
243
|
-
const value = process.env[key];
|
|
244
|
-
if (!value)
|
|
245
|
-
return chalk.dim('not set');
|
|
246
|
-
return SENSITIVE_KEYS.has(key) ? redactValue(key, value) : value;
|
|
247
|
-
};
|
|
248
|
-
if (options.all) {
|
|
249
|
-
// Full dump for developers
|
|
250
|
-
const redacted = {};
|
|
251
|
-
for (const key of VALID_KEYS) {
|
|
252
|
-
redacted[key] = getEnv(key);
|
|
253
|
-
}
|
|
254
|
-
if (options.format === 'json') {
|
|
255
|
-
console.log(JSON.stringify(redacted, null, 2));
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
for (const [key, value] of Object.entries(redacted)) {
|
|
259
|
-
console.log(` ${chalk.cyan(key.padEnd(30))} ${value}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (options.format === 'json') {
|
|
265
|
-
const result = {};
|
|
266
|
-
for (const section of USER_CONFIG) {
|
|
267
|
-
for (const { key } of section.keys) {
|
|
268
|
-
result[key] = process.env[key] ?? '';
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
console.log(JSON.stringify(result, null, 2));
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
console.log('');
|
|
275
|
-
for (const section of USER_CONFIG) {
|
|
276
|
-
console.log(chalk.bold(` ${section.section}`));
|
|
277
|
-
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
278
|
-
for (const { key, label } of section.keys) {
|
|
279
|
-
console.log(` ${chalk.cyan(label.padEnd(22))} ${getEnv(key)}`);
|
|
280
|
-
}
|
|
281
|
-
console.log('');
|
|
282
|
-
}
|
|
283
|
-
console.log(chalk.dim(' Use --all to show all config variables.'));
|
|
284
|
-
console.log('');
|
|
285
|
-
});
|
|
286
|
-
// ─── config set ───
|
|
287
|
-
configCmd
|
|
288
|
-
.command('set <key> <value>')
|
|
289
|
-
.description('Set a config value in the .env file')
|
|
290
|
-
.action((key, value) => {
|
|
291
|
-
try {
|
|
292
|
-
// Validate key
|
|
293
|
-
if (!VALID_KEYS.includes(key)) {
|
|
294
|
-
console.error(chalk.red(`Error: Unknown config key "${key}".`));
|
|
295
|
-
console.error(chalk.dim(`Valid keys: ${VALID_KEYS.join(', ')}`));
|
|
296
|
-
process.exitCode = 1;
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
const envPath = resolveEnvPath();
|
|
300
|
-
writeEnvValue(envPath, key, value);
|
|
301
|
-
console.log('');
|
|
302
|
-
console.log(chalk.green(` Config updated: ${key}`));
|
|
303
|
-
if (SENSITIVE_KEYS.has(key)) {
|
|
304
|
-
console.log(` ${chalk.gray('Value:')} ${redactValue(key, value)}`);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
console.log(` ${chalk.gray('Value:')} ${value}`);
|
|
308
|
-
}
|
|
309
|
-
console.log(` ${chalk.gray('File:')} ${envPath}`);
|
|
310
|
-
console.log('');
|
|
311
|
-
console.log(chalk.dim(' Note: Restart services for changes to take effect.'));
|
|
312
|
-
console.log('');
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
316
|
-
logger.error({ error: message }, 'Failed to set config');
|
|
317
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
318
|
-
process.exitCode = 1;
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
// ─── config set-llm-key ───
|
|
322
|
-
configCmd
|
|
323
|
-
.command('set-llm-key <apiKey>')
|
|
324
|
-
.description('Store your LLM API key for thesis extraction + coaching (BYOK)')
|
|
325
|
-
.addOption(new Option('-p, --provider <provider>', 'LLM provider (auto-detected from key prefix if omitted)').choices(['anthropic', 'openai', 'openrouter', 'ollama', 'gemini', 'custom']))
|
|
326
|
-
.option('-m, --model <model>', 'Model name (default for all phases)')
|
|
327
|
-
.option('--base-url <url>', 'Custom base URL (for openrouter/ollama/custom providers)')
|
|
328
|
-
.option('--scan-model <model>', 'Model for market scanning (e.g. claude-haiku-4-5)')
|
|
329
|
-
.option('--analyze-model <model>', 'Model for deep analysis (e.g. claude-sonnet-4-6)')
|
|
330
|
-
.option('--decide-model <model>', 'Model for trade decisions (e.g. claude-opus-4-6)')
|
|
331
|
-
.addOption(new Option('--scan-provider <provider>', 'Provider for scan phase').choices(['anthropic', 'openai', 'openrouter', 'ollama', 'gemini', 'custom']))
|
|
332
|
-
.option('--scan-key <key>', 'API key for scan phase provider')
|
|
333
|
-
.addOption(new Option('--analyze-provider <provider>', 'Provider for analyze phase').choices(['anthropic', 'openai', 'openrouter', 'ollama', 'gemini', 'custom']))
|
|
334
|
-
.option('--analyze-key <key>', 'API key for analyze phase provider')
|
|
335
|
-
.addOption(new Option('--decide-provider <provider>', 'Provider for decide phase').choices(['anthropic', 'openai', 'openrouter', 'ollama', 'gemini', 'custom']))
|
|
336
|
-
.option('--decide-key <key>', 'API key for decide phase provider')
|
|
337
|
-
.action(async (apiKey, opts) => {
|
|
338
|
-
try {
|
|
339
|
-
const result = await apiRequest('/api/v1/llm-config', {
|
|
340
|
-
method: 'PUT',
|
|
341
|
-
body: {
|
|
342
|
-
apiKey,
|
|
343
|
-
...(opts.provider ? { provider: opts.provider } : {}),
|
|
344
|
-
...(opts.model ? { model: opts.model } : {}),
|
|
345
|
-
...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
|
|
346
|
-
...(opts.scanModel ? { scanModel: opts.scanModel } : {}),
|
|
347
|
-
...(opts.analyzeModel ? { analyzeModel: opts.analyzeModel } : {}),
|
|
348
|
-
...(opts.decideModel ? { decideModel: opts.decideModel } : {}),
|
|
349
|
-
...(opts.scanProvider ? { scanProvider: opts.scanProvider } : {}),
|
|
350
|
-
...(opts.scanKey ? { scanApiKey: opts.scanKey } : {}),
|
|
351
|
-
...(opts.analyzeProvider ? { analyzeProvider: opts.analyzeProvider } : {}),
|
|
352
|
-
...(opts.analyzeKey ? { analyzeApiKey: opts.analyzeKey } : {}),
|
|
353
|
-
...(opts.decideProvider ? { decideProvider: opts.decideProvider } : {}),
|
|
354
|
-
...(opts.decideKey ? { decideApiKey: opts.decideKey } : {}),
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
console.log('');
|
|
358
|
-
console.log(chalk.green(' LLM API key saved successfully'));
|
|
359
|
-
console.log(` ${chalk.gray('Provider:')} ${result.provider}`);
|
|
360
|
-
console.log(` ${chalk.gray('Model:')} ${result.model}`);
|
|
361
|
-
if (result.scanProvider || result.scanModel) {
|
|
362
|
-
console.log(` ${chalk.gray('Scan:')} ${result.scanProvider ?? result.provider} / ${result.scanModel ?? result.model}${opts.scanKey ? ' (own key)' : ''}`);
|
|
363
|
-
}
|
|
364
|
-
if (result.analyzeProvider || result.analyzeModel) {
|
|
365
|
-
console.log(` ${chalk.gray('Analyze:')} ${result.analyzeProvider ?? result.provider} / ${result.analyzeModel ?? result.model}${opts.analyzeKey ? ' (own key)' : ''}`);
|
|
366
|
-
}
|
|
367
|
-
if (result.decideProvider || result.decideModel) {
|
|
368
|
-
console.log(` ${chalk.gray('Decide:')} ${result.decideProvider ?? result.provider} / ${result.decideModel ?? result.model}${opts.decideKey ? ' (own key)' : ''}`);
|
|
369
|
-
}
|
|
370
|
-
console.log(` ${chalk.gray('Key:')} ${apiKey.slice(0, 8)}${'*'.repeat(Math.max(0, apiKey.length - 8))}`);
|
|
371
|
-
console.log('');
|
|
372
|
-
console.log(chalk.dim(' Your key is encrypted at rest. Thesis extraction and coaching are now enabled.'));
|
|
373
|
-
console.log('');
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
-
logger.error({ error: message }, 'Failed to set LLM key');
|
|
378
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
379
|
-
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
// ─── config set-models ───
|
|
383
|
-
configCmd
|
|
384
|
-
.command('set-models')
|
|
385
|
-
.description('Update per-stage model assignments (no API key needed — works with Codex OAuth)')
|
|
386
|
-
.option('-m, --model <model>', 'Default model for all phases')
|
|
387
|
-
.option('--scan-model <model>', 'Model for market scanning')
|
|
388
|
-
.option('--analyze-model <model>', 'Model for deep analysis')
|
|
389
|
-
.option('--decide-model <model>', 'Model for trade decisions')
|
|
390
|
-
.option('--exit-heartbeat-model <model>', 'Model for exit heartbeat checks (cheap, every 4h)')
|
|
391
|
-
.option('--exit-event-model <model>', 'Model for exit event-driven analysis (quality)')
|
|
392
|
-
.action(async (opts) => {
|
|
393
|
-
if (!opts.model && !opts.scanModel && !opts.analyzeModel && !opts.decideModel && !opts.exitHeartbeatModel && !opts.exitEventModel) {
|
|
394
|
-
console.error(chalk.red(' Error: At least one model flag is required.'));
|
|
395
|
-
console.log(chalk.dim(' Example: trading-boy config set-models --scan-model gpt-5.4-mini --analyze-model gpt-5.4'));
|
|
396
|
-
process.exitCode = 1;
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
try {
|
|
400
|
-
const result = await apiRequest('/api/v1/llm-config/models', {
|
|
401
|
-
method: 'PATCH',
|
|
402
|
-
body: {
|
|
403
|
-
...(opts.model ? { model: opts.model } : {}),
|
|
404
|
-
...(opts.scanModel ? { scanModel: opts.scanModel } : {}),
|
|
405
|
-
...(opts.analyzeModel ? { analyzeModel: opts.analyzeModel } : {}),
|
|
406
|
-
...(opts.decideModel ? { decideModel: opts.decideModel } : {}),
|
|
407
|
-
...(opts.exitHeartbeatModel ? { exitHeartbeatModel: opts.exitHeartbeatModel } : {}),
|
|
408
|
-
...(opts.exitEventModel ? { exitEventModel: opts.exitEventModel } : {}),
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
console.log('');
|
|
412
|
-
console.log(chalk.green(' Stage models updated'));
|
|
413
|
-
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
414
|
-
console.log(` ${chalk.gray('Default:')} ${result.model}`);
|
|
415
|
-
if (result.scanModel)
|
|
416
|
-
console.log(` ${chalk.gray('Scan:')} ${result.scanModel}`);
|
|
417
|
-
if (result.analyzeModel)
|
|
418
|
-
console.log(` ${chalk.gray('Analyze:')} ${result.analyzeModel}`);
|
|
419
|
-
if (result.decideModel)
|
|
420
|
-
console.log(` ${chalk.gray('Decide:')} ${result.decideModel}`);
|
|
421
|
-
if (result.exitHeartbeatModel)
|
|
422
|
-
console.log(` ${chalk.gray('Exit heartbeat:')} ${result.exitHeartbeatModel}`);
|
|
423
|
-
if (result.exitEventModel)
|
|
424
|
-
console.log(` ${chalk.gray('Exit event:')} ${result.exitEventModel}`);
|
|
425
|
-
console.log('');
|
|
426
|
-
console.log(chalk.dim(' Agents will use these models on their next tick.'));
|
|
427
|
-
console.log('');
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
431
|
-
logger.error({ error: message }, 'Failed to set stage models');
|
|
432
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
433
|
-
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
// ─── config get-llm-key ───
|
|
437
|
-
configCmd
|
|
438
|
-
.command('get-llm-key')
|
|
439
|
-
.description('Show current LLM configuration (key redacted)')
|
|
440
|
-
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
441
|
-
.action(async (options) => {
|
|
442
|
-
try {
|
|
443
|
-
const result = await apiRequest('/api/v1/llm-config');
|
|
444
|
-
if (options.format === 'json') {
|
|
445
|
-
console.log(JSON.stringify(result, null, 2));
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
console.log('');
|
|
449
|
-
console.log(chalk.bold.cyan(' LLM Configuration'));
|
|
450
|
-
console.log(chalk.gray(' ' + '\u2500'.repeat(40)));
|
|
451
|
-
console.log(` ${chalk.gray('Provider:')} ${result.provider}`);
|
|
452
|
-
console.log(` ${chalk.gray('Model:')} ${result.model}`);
|
|
453
|
-
if (result.scanProvider || result.scanModel) {
|
|
454
|
-
console.log(` ${chalk.gray('Scan:')} ${result.scanProvider ?? result.provider} / ${result.scanModel ?? result.model}`);
|
|
455
|
-
}
|
|
456
|
-
if (result.analyzeProvider || result.analyzeModel) {
|
|
457
|
-
console.log(` ${chalk.gray('Analyze:')} ${result.analyzeProvider ?? result.provider} / ${result.analyzeModel ?? result.model}`);
|
|
458
|
-
}
|
|
459
|
-
if (result.decideProvider || result.decideModel) {
|
|
460
|
-
console.log(` ${chalk.gray('Decide:')} ${result.decideProvider ?? result.provider} / ${result.decideModel ?? result.model}`);
|
|
461
|
-
}
|
|
462
|
-
if (result.exitHeartbeatModel) {
|
|
463
|
-
console.log(` ${chalk.gray('Exit HB:')} ${result.exitHeartbeatModel}`);
|
|
464
|
-
}
|
|
465
|
-
if (result.exitEventModel) {
|
|
466
|
-
console.log(` ${chalk.gray('Exit Event:')} ${result.exitEventModel}`);
|
|
467
|
-
}
|
|
468
|
-
if (result.baseUrl) {
|
|
469
|
-
console.log(` ${chalk.gray('Base URL:')} ${result.baseUrl}`);
|
|
470
|
-
}
|
|
471
|
-
console.log(` ${chalk.gray('Updated:')} ${new Date(result.updatedAt).toLocaleString()}`);
|
|
472
|
-
console.log('');
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
catch (error) {
|
|
476
|
-
if (error instanceof ApiError && error.status === 404) {
|
|
477
|
-
console.log('');
|
|
478
|
-
console.log(chalk.yellow(' No LLM key configured.'));
|
|
479
|
-
console.log(chalk.dim(' Run: trading-boy config set-llm-key <your-api-key>'));
|
|
480
|
-
console.log('');
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
484
|
-
logger.error({ error: message }, 'Failed to get LLM config');
|
|
485
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
486
|
-
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
// ─── config remove-llm-key ───
|
|
491
|
-
configCmd
|
|
492
|
-
.command('remove-llm-key')
|
|
493
|
-
.description('Remove your stored LLM API key')
|
|
494
|
-
.action(async () => {
|
|
495
|
-
try {
|
|
496
|
-
await apiRequest('/api/v1/llm-config', { method: 'DELETE' });
|
|
497
|
-
console.log('');
|
|
498
|
-
console.log(chalk.green(' LLM API key removed.'));
|
|
499
|
-
console.log(chalk.dim(' Thesis extraction and coaching features are now disabled.'));
|
|
500
|
-
console.log('');
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
if (error instanceof ApiError && error.status === 404) {
|
|
504
|
-
console.log(chalk.yellow(' No LLM key configured.'));
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
508
|
-
logger.error({ error: message }, 'Failed to remove LLM key');
|
|
509
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
510
|
-
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
//# sourceMappingURL=config-cmd.js.map
|