@trading-boy/cli 1.2.20 → 1.4.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/dist/cli.js CHANGED
@@ -40,6 +40,8 @@ import { registerStrategyCommand } from './commands/strategy-cmd.js';
40
40
  import { registerReplayCommand } from './commands/replay-cmd.js';
41
41
  import { registerBenchmarkCommand } from './commands/benchmark-cmd.js';
42
42
  import { registerSuggestionsCommand } from './commands/suggestions-cmd.js';
43
+ import { registerCronCommand } from './commands/cron-cmd.js';
44
+ import { registerAgentCommand } from './commands/agent-cmd.js';
43
45
  import { readFileSync } from 'node:fs';
44
46
  import { fileURLToPath } from 'node:url';
45
47
  import { dirname, resolve } from 'node:path';
@@ -90,6 +92,10 @@ export function createCli() {
90
92
  registerReplayCommand(program);
91
93
  registerBenchmarkCommand(program);
92
94
  registerSuggestionsCommand(program);
95
+ // Scheduling
96
+ registerCronCommand(program);
97
+ // Agents
98
+ registerAgentCommand(program);
93
99
  // System management
94
100
  registerConfigCommand(program);
95
101
  registerInfraCommand(program);
@@ -100,6 +106,8 @@ export function createCli() {
100
106
  'Trading Journal': ['journal', 'decisions', 'stats', 'trader', 'behavioral', 'audit'],
101
107
  'Edge & Safety': ['edge', 'edge-guard', 'coaching', 'thesis'],
102
108
  'Strategy & Benchmarking': ['strategy', 'replay', 'benchmark', 'suggestions'],
109
+ 'Scheduling': ['cron'],
110
+ 'Agents': ['agent'],
103
111
  'Account': ['login', 'logout', 'whoami', 'billing', 'subscribe'],
104
112
  'System': ['config', 'infra'],
105
113
  };
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Parse a human-readable interval string (e.g. "1m", "5m", "15m", "1h") to milliseconds.
4
+ * Supported units: s (seconds), m (minutes), h (hours).
5
+ * Returns null if the format is invalid.
6
+ */
7
+ export declare function parseHumanInterval(value: string): number | null;
8
+ export declare function registerAgentCommand(program: Command): void;
9
+ //# sourceMappingURL=agent-cmd.d.ts.map
@@ -0,0 +1,560 @@
1
+ // ─── Agent CLI Commands ───
2
+ //
3
+ // tb agent create — Create a new agent
4
+ // tb agent list — List agents
5
+ // tb agent show — Show agent details + live state
6
+ // tb agent pause — Pause an agent
7
+ // tb agent resume — Resume a paused agent
8
+ // tb agent delete — Delete an agent
9
+ // tb agent update — Update agent config
10
+ import { readFileSync, existsSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import { Option } from 'commander';
13
+ import chalk from 'chalk';
14
+ import { createLogger } from '@trading-boy/core';
15
+ import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
16
+ import { padRight } from '../utils.js';
17
+ const logger = createLogger('cli-agent');
18
+ // ─── Error Handler ───
19
+ function handleApiError(error, context) {
20
+ if (error instanceof ApiError) {
21
+ switch (error.status) {
22
+ case 401:
23
+ console.error(chalk.red('Error: API key invalid or expired. Run `trading-boy login` to re-authenticate.'));
24
+ break;
25
+ case 403:
26
+ console.error(chalk.red(`Error: ${error.message}`));
27
+ break;
28
+ case 404:
29
+ console.error(chalk.red(`Error: Not found — ${error.message}`));
30
+ break;
31
+ case 429:
32
+ console.error(chalk.red(`Error: Limit reached — ${error.message}`));
33
+ break;
34
+ default:
35
+ console.error(chalk.red(`Error: ${error.message}`));
36
+ }
37
+ }
38
+ else {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ logger.error({ error: message }, context);
41
+ console.error(chalk.red(`Error: ${message}`));
42
+ }
43
+ process.exitCode = error instanceof ApiError ? 2 : 1;
44
+ }
45
+ async function ensureRemote() {
46
+ if (!(await isRemoteMode())) {
47
+ console.error(chalk.yellow('Agent commands require a remote API connection.'));
48
+ console.error(chalk.dim(' Run: trading-boy login'));
49
+ process.exitCode = 1;
50
+ return false;
51
+ }
52
+ return true;
53
+ }
54
+ // ─── Formatters ───
55
+ function formatShortDate(isoString) {
56
+ if (!isoString)
57
+ return chalk.dim('—');
58
+ try {
59
+ return new Date(isoString).toISOString().slice(0, 19).replace('T', ' ');
60
+ }
61
+ catch {
62
+ return isoString;
63
+ }
64
+ }
65
+ function formatStatus(status) {
66
+ switch (status) {
67
+ case 'active': return chalk.green('active');
68
+ case 'paused': return chalk.yellow('paused');
69
+ case 'deleted': return chalk.red('deleted');
70
+ default: return status;
71
+ }
72
+ }
73
+ function formatAutonomy(level) {
74
+ switch (level) {
75
+ case 'OBSERVE_ONLY': return chalk.dim('observe');
76
+ case 'SUGGEST': return chalk.blue('suggest');
77
+ case 'AUTO_WITH_APPROVAL': return chalk.yellow('auto+approve');
78
+ case 'FULLY_AUTONOMOUS': return chalk.red('autonomous');
79
+ default: return level;
80
+ }
81
+ }
82
+ function formatInterval(ms) {
83
+ if (ms < 60000)
84
+ return `${(ms / 1000).toFixed(0)}s`;
85
+ if (ms < 3600000)
86
+ return `${(ms / 60000).toFixed(0)}m`;
87
+ return `${(ms / 3600000).toFixed(1)}h`;
88
+ }
89
+ /**
90
+ * Parse a human-readable interval string (e.g. "1m", "5m", "15m", "1h") to milliseconds.
91
+ * Supported units: s (seconds), m (minutes), h (hours).
92
+ * Returns null if the format is invalid.
93
+ */
94
+ export function parseHumanInterval(value) {
95
+ const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h)$/i);
96
+ if (!match)
97
+ return null;
98
+ const num = parseFloat(match[1]);
99
+ const unit = match[2].toLowerCase();
100
+ switch (unit) {
101
+ case 's': return num * 1000;
102
+ case 'm': return num * 60_000;
103
+ case 'h': return num * 3_600_000;
104
+ default: return null;
105
+ }
106
+ }
107
+ const MIN_SCAN_INTERVAL_MS = 60_000;
108
+ // ─── Command Registration ───
109
+ export function registerAgentCommand(program) {
110
+ const agent = program
111
+ .command('agent')
112
+ .description('Manage autonomous trading agents');
113
+ // ── create ──────────────────────────────────────────────────────────────────
114
+ agent
115
+ .command('create')
116
+ .description('Create a new agent')
117
+ .requiredOption('--trader-id <traderId>', 'Trader ID')
118
+ .requiredOption('--strategy-id <strategyId>', 'Strategy ID')
119
+ .option('--name <name>', 'Agent name')
120
+ .option('--autonomy <level>', 'Autonomy level: OBSERVE_ONLY, SUGGEST, AUTO_WITH_APPROVAL, FULLY_AUTONOMOUS', 'OBSERVE_ONLY')
121
+ .option('--scan-interval <ms>', 'Scan interval in ms (min 60000)', '300000')
122
+ .option('--scan-interval-human <duration>', 'Scan interval in human-readable format (e.g. 1m, 5m, 15m, 30m, 1h)')
123
+ .option('--watchlist <symbols>', 'Comma-separated token symbols')
124
+ .option('--max-daily-trades <n>', 'Max daily trades', '10')
125
+ .option('--max-daily-loss <usd>', 'Max daily loss in USD', '500')
126
+ .option('--max-position-size <pct>', 'Max position size as decimal (0.10 = 10%)', '0.10')
127
+ .option('--min-confidence <n>', 'Min confidence threshold (0-1)', '0.60')
128
+ .option('--scan-model <model>', 'LLM model for market scanning')
129
+ .option('--analyze-model <model>', 'LLM model for deep analysis')
130
+ .option('--decide-model <model>', 'LLM model for trade decisions')
131
+ .addOption(new Option('--asset-class <class>', 'Asset class for this agent').choices(['crypto', 'commodities', 'mixed']).default('crypto'))
132
+ .option('--soul-override <text>', 'Custom soul/personality for this agent')
133
+ .option('--purpose-override <text>', 'Custom purpose/mission for this agent')
134
+ .option('--soul-file <path>', 'Load soul from a file')
135
+ .option('--purpose-file <path>', 'Load purpose from a file')
136
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
137
+ .action(async (options) => {
138
+ if (!(await ensureRemote()))
139
+ return;
140
+ const body = {
141
+ traderId: options.traderId,
142
+ strategyId: options.strategyId,
143
+ };
144
+ if (options.name)
145
+ body.name = options.name;
146
+ if (options.autonomy)
147
+ body.autonomyLevel = options.autonomy;
148
+ // Resolve scan interval: --scan-interval-human takes precedence
149
+ if (options.scanIntervalHuman) {
150
+ const ms = parseHumanInterval(options.scanIntervalHuman);
151
+ if (ms === null) {
152
+ console.error(chalk.red(`Error: Invalid duration "${options.scanIntervalHuman}". Use format like 1m, 5m, 15m, 30m, 1h.`));
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+ if (ms < MIN_SCAN_INTERVAL_MS) {
157
+ console.error(chalk.red(`Error: Scan interval must be at least 1m (60000ms). Got ${formatInterval(ms)}.`));
158
+ process.exitCode = 1;
159
+ return;
160
+ }
161
+ body.scanIntervalMs = ms;
162
+ }
163
+ else if (options.scanInterval) {
164
+ body.scanIntervalMs = parseInt(options.scanInterval, 10);
165
+ }
166
+ if (options.watchlist)
167
+ body.watchlist = options.watchlist.split(',').map((s) => s.trim().toUpperCase());
168
+ if (options.maxDailyTrades)
169
+ body.maxDailyTrades = parseInt(options.maxDailyTrades, 10);
170
+ if (options.maxDailyLoss)
171
+ body.maxDailyLossUsd = parseFloat(options.maxDailyLoss);
172
+ if (options.maxPositionSize)
173
+ body.maxPositionSizePct = parseFloat(options.maxPositionSize);
174
+ if (options.minConfidence)
175
+ body.minConfidence = parseFloat(options.minConfidence);
176
+ if (options.scanModel)
177
+ body.scanModel = options.scanModel;
178
+ if (options.analyzeModel)
179
+ body.analyzeModel = options.analyzeModel;
180
+ if (options.decideModel)
181
+ body.decideModel = options.decideModel;
182
+ if (options.assetClass)
183
+ body.assetClass = options.assetClass;
184
+ // Soul override — file takes precedence over inline text
185
+ if (options.soulFile) {
186
+ const path = resolve(options.soulFile);
187
+ if (!existsSync(path)) {
188
+ console.error(chalk.red(`Error: Soul file not found: ${path}`));
189
+ process.exitCode = 1;
190
+ return;
191
+ }
192
+ body.soulOverride = readFileSync(path, 'utf-8');
193
+ }
194
+ else if (options.soulOverride) {
195
+ body.soulOverride = options.soulOverride;
196
+ }
197
+ // Purpose override — file takes precedence over inline text
198
+ if (options.purposeFile) {
199
+ const path = resolve(options.purposeFile);
200
+ if (!existsSync(path)) {
201
+ console.error(chalk.red(`Error: Purpose file not found: ${path}`));
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ body.purposeOverride = readFileSync(path, 'utf-8');
206
+ }
207
+ else if (options.purposeOverride) {
208
+ body.purposeOverride = options.purposeOverride;
209
+ }
210
+ try {
211
+ const result = await apiRequest('/api/v1/agents', {
212
+ method: 'POST',
213
+ body,
214
+ });
215
+ if (options.format === 'json') {
216
+ console.log(JSON.stringify(result, null, 2));
217
+ }
218
+ else {
219
+ console.log('');
220
+ console.log(chalk.green(' Agent created'));
221
+ console.log(` ${chalk.gray('ID:')} ${result.id}`);
222
+ console.log(` ${chalk.gray('Name:')} ${result.name}`);
223
+ console.log(` ${chalk.gray('Trader:')} ${result.traderId}`);
224
+ console.log(` ${chalk.gray('Strategy:')} ${result.strategyId}`);
225
+ console.log(` ${chalk.gray('Autonomy:')} ${formatAutonomy(result.autonomyLevel)}`);
226
+ console.log(` ${chalk.gray('Interval:')} ${formatInterval(result.scanIntervalMs)}`);
227
+ console.log(` ${chalk.gray('Next scan:')} ${formatShortDate(result.nextScanAt)}`);
228
+ console.log('');
229
+ }
230
+ }
231
+ catch (error) {
232
+ handleApiError(error, 'Agent create failed');
233
+ }
234
+ });
235
+ // ── list ────────────────────────────────────────────────────────────────────
236
+ agent
237
+ .command('list')
238
+ .description('List agents')
239
+ .option('--status <status>', 'Filter by status: active, paused')
240
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
241
+ .action(async (options) => {
242
+ if (!(await ensureRemote()))
243
+ return;
244
+ try {
245
+ const query = options.status ? `?status=${options.status}` : '';
246
+ const result = await apiRequest(`/api/v1/agents${query}`);
247
+ if (options.format === 'json') {
248
+ console.log(JSON.stringify(result, null, 2));
249
+ return;
250
+ }
251
+ if (result.agents.length === 0) {
252
+ console.log(chalk.dim(' No agents found'));
253
+ return;
254
+ }
255
+ console.log('');
256
+ console.log(' ' +
257
+ padRight('Name', 24) +
258
+ padRight('Autonomy', 14) +
259
+ padRight('Interval', 10) +
260
+ padRight('Status', 10) +
261
+ padRight('Ticks', 7) +
262
+ padRight('Errors', 8) +
263
+ 'Next Scan');
264
+ console.log(chalk.gray(' ' + '─'.repeat(100)));
265
+ for (const a of result.agents) {
266
+ console.log(' ' +
267
+ padRight(a.name.slice(0, 22), 24) +
268
+ padRight(formatAutonomy(a.autonomyLevel), 14) +
269
+ padRight(formatInterval(a.scanIntervalMs), 10) +
270
+ padRight(formatStatus(a.status), 10) +
271
+ padRight(String(a.tickCount), 7) +
272
+ padRight(String(a.errorCount), 8) +
273
+ formatShortDate(a.nextScanAt));
274
+ }
275
+ console.log('');
276
+ console.log(chalk.dim(` ${result.count} agent(s)`));
277
+ console.log('');
278
+ }
279
+ catch (error) {
280
+ handleApiError(error, 'Agent list failed');
281
+ }
282
+ });
283
+ // ── show ────────────────────────────────────────────────────────────────────
284
+ agent
285
+ .command('show <agentId>')
286
+ .description('Show agent details and live state')
287
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
288
+ .action(async (agentId, options) => {
289
+ if (!(await ensureRemote()))
290
+ return;
291
+ try {
292
+ const result = await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}`);
293
+ if (options.format === 'json') {
294
+ console.log(JSON.stringify(result, null, 2));
295
+ return;
296
+ }
297
+ const a = result.agent;
298
+ const live = result.live;
299
+ console.log('');
300
+ console.log(chalk.bold.cyan(` Agent — ${a.name}`));
301
+ console.log(chalk.gray(' ' + '─'.repeat(50)));
302
+ console.log(` ${chalk.gray('ID:')} ${a.id}`);
303
+ console.log(` ${chalk.gray('Status:')} ${formatStatus(a.status)}`);
304
+ console.log(` ${chalk.gray('Trader:')} ${a.traderId}`);
305
+ console.log(` ${chalk.gray('Strategy:')} ${a.strategyId}`);
306
+ console.log(` ${chalk.gray('Autonomy:')} ${formatAutonomy(a.autonomyLevel)}`);
307
+ if (a.assetClass)
308
+ console.log(` ${chalk.gray('Asset class:')} ${a.assetClass}`);
309
+ if (a.scanModel)
310
+ console.log(` ${chalk.gray('Scan model:')} ${a.scanModel}`);
311
+ if (a.analyzeModel)
312
+ console.log(` ${chalk.gray('Analyze model:')} ${a.analyzeModel}`);
313
+ if (a.decideModel)
314
+ console.log(` ${chalk.gray('Decide model:')} ${a.decideModel}`);
315
+ console.log(` ${chalk.gray('Scan interval:')} ${formatInterval(a.scanIntervalMs)}`);
316
+ console.log(` ${chalk.gray('Max daily trades:')} ${a.maxDailyTrades}`);
317
+ console.log(` ${chalk.gray('Max daily loss:')} $${a.maxDailyLossUsd}`);
318
+ console.log(` ${chalk.gray('Max position:')} ${(a.maxPositionSizePct * 100).toFixed(0)}%`);
319
+ console.log(` ${chalk.gray('Min confidence:')} ${(a.minConfidence * 100).toFixed(0)}%`);
320
+ console.log(` ${chalk.gray('Watchlist:')} ${a.watchlist.length > 0 ? a.watchlist.join(', ') : chalk.dim('(from strategy)')}`);
321
+ if (a.soulOverride) {
322
+ const soulPreview = a.soulOverride.length > 60 ? a.soulOverride.slice(0, 60) + '...' : a.soulOverride;
323
+ console.log(` ${chalk.gray('Soul override:')} ${chalk.white(soulPreview)}`);
324
+ }
325
+ if (a.purposeOverride) {
326
+ const purposePreview = a.purposeOverride.length > 60 ? a.purposeOverride.slice(0, 60) + '...' : a.purposeOverride;
327
+ console.log(` ${chalk.gray('Purpose override:')} ${chalk.white(purposePreview)}`);
328
+ }
329
+ console.log(` ${chalk.gray('Tick count:')} ${a.tickCount}`);
330
+ console.log(` ${chalk.gray('Error count:')} ${a.errorCount}`);
331
+ if (a.lastError) {
332
+ console.log(` ${chalk.gray('Last error:')} ${chalk.red(a.lastError.slice(0, 60))}`);
333
+ }
334
+ console.log(` ${chalk.gray('Last tick:')} ${formatShortDate(a.lastTickAt)}`);
335
+ console.log(` ${chalk.gray('Next scan:')} ${formatShortDate(a.nextScanAt)}`);
336
+ console.log(` ${chalk.gray('Created:')} ${formatShortDate(a.createdAt)}`);
337
+ // Live state
338
+ if (live.state || live.admin) {
339
+ console.log('');
340
+ console.log(chalk.bold(' Live State'));
341
+ console.log(chalk.gray(' ' + '─'.repeat(50)));
342
+ if (live.admin) {
343
+ console.log(` ${chalk.gray('Paused:')} ${live.admin.paused ? chalk.yellow('yes') : chalk.dim('no')}`);
344
+ console.log(` ${chalk.gray('Killed:')} ${live.admin.killed ? chalk.red('yes') : chalk.dim('no')}`);
345
+ console.log(` ${chalk.gray('Override:')} ${live.admin.override ?? chalk.dim('none')}`);
346
+ if (live.admin.autonomyOverride) {
347
+ console.log(` ${chalk.gray('Autonomy override:')} ${formatAutonomy(live.admin.autonomyOverride)}`);
348
+ }
349
+ }
350
+ if (live.state) {
351
+ const currentState = live.state.state;
352
+ if (currentState) {
353
+ console.log(` ${chalk.gray('Agent state:')} ${chalk.cyan(String(currentState))}`);
354
+ }
355
+ }
356
+ }
357
+ console.log('');
358
+ }
359
+ catch (error) {
360
+ handleApiError(error, 'Agent show failed');
361
+ }
362
+ });
363
+ // ── pause ───────────────────────────────────────────────────────────────────
364
+ agent
365
+ .command('pause <agentId>')
366
+ .description('Pause an agent')
367
+ .action(async (agentId) => {
368
+ if (!(await ensureRemote()))
369
+ return;
370
+ try {
371
+ await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}/pause`, {
372
+ method: 'POST',
373
+ });
374
+ console.log(chalk.green(` Agent ${agentId} paused`));
375
+ }
376
+ catch (error) {
377
+ handleApiError(error, 'Agent pause failed');
378
+ }
379
+ });
380
+ // ── resume ──────────────────────────────────────────────────────────────────
381
+ agent
382
+ .command('resume <agentId>')
383
+ .description('Resume a paused agent')
384
+ .action(async (agentId) => {
385
+ if (!(await ensureRemote()))
386
+ return;
387
+ try {
388
+ await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}/resume`, {
389
+ method: 'POST',
390
+ });
391
+ console.log(chalk.green(` Agent ${agentId} resumed`));
392
+ }
393
+ catch (error) {
394
+ handleApiError(error, 'Agent resume failed');
395
+ }
396
+ });
397
+ // ── delete ──────────────────────────────────────────────────────────────────
398
+ agent
399
+ .command('delete <agentId>')
400
+ .description('Delete an agent')
401
+ .action(async (agentId) => {
402
+ if (!(await ensureRemote()))
403
+ return;
404
+ try {
405
+ await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}`, {
406
+ method: 'DELETE',
407
+ });
408
+ console.log(chalk.green(` Agent ${agentId} deleted`));
409
+ }
410
+ catch (error) {
411
+ handleApiError(error, 'Agent delete failed');
412
+ }
413
+ });
414
+ // ── exit ───────────────────────────────────────────────────────────────────
415
+ agent
416
+ .command('exit <agentId>')
417
+ .description('Exit/close an open position for an agent')
418
+ .requiredOption('--symbol <symbol>', 'Token symbol to exit (e.g. xyz:NATGAS)')
419
+ .option('--reason <text>', 'Reason for exit')
420
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
421
+ .action(async (agentId, options) => {
422
+ if (!(await ensureRemote()))
423
+ return;
424
+ try {
425
+ const body = {};
426
+ if (options.reason)
427
+ body.reason = options.reason;
428
+ const result = await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}/positions/${encodeURIComponent(options.symbol)}/exit`, { method: 'POST', body });
429
+ if (options.format === 'json') {
430
+ console.log(JSON.stringify(result, null, 2));
431
+ }
432
+ else {
433
+ const pnlColor = result.pnl >= 0 ? chalk.green : chalk.red;
434
+ const pnlSign = result.pnl >= 0 ? '+' : '';
435
+ console.log('');
436
+ console.log(chalk.green(' Position closed'));
437
+ console.log(` ${chalk.gray('Symbol:')} ${result.symbol}`);
438
+ console.log(` ${chalk.gray('Side:')} ${result.side}`);
439
+ console.log(` ${chalk.gray('Exit price:')} $${result.exitPrice.toLocaleString()}`);
440
+ console.log(` ${chalk.gray('PnL:')} ${pnlColor(`${pnlSign}$${result.pnl.toFixed(2)} (${pnlSign}${result.pnlPct.toFixed(2)}%)`)}`);
441
+ console.log(` ${chalk.gray('Closed at:')} ${formatShortDate(result.closedAt)}`);
442
+ if (options.reason) {
443
+ console.log(` ${chalk.gray('Reason:')} ${options.reason}`);
444
+ }
445
+ console.log('');
446
+ }
447
+ }
448
+ catch (error) {
449
+ handleApiError(error, 'Position exit failed');
450
+ }
451
+ });
452
+ // ── update ──────────────────────────────────────────────────────────────────
453
+ agent
454
+ .command('update <agentId>')
455
+ .description('Update agent config')
456
+ .option('--name <name>', 'Agent name')
457
+ .option('--autonomy <level>', 'Autonomy level')
458
+ .option('--scan-interval <ms>', 'Scan interval in ms')
459
+ .option('--scan-interval-human <duration>', 'Scan interval in human-readable format (e.g. 1m, 5m, 15m, 30m, 1h)')
460
+ .option('--watchlist <symbols>', 'Comma-separated token symbols')
461
+ .option('--max-daily-trades <n>', 'Max daily trades')
462
+ .option('--max-daily-loss <usd>', 'Max daily loss in USD')
463
+ .option('--max-position-size <pct>', 'Max position size as decimal')
464
+ .option('--min-confidence <n>', 'Min confidence threshold')
465
+ .option('--scan-model <model>', 'LLM model for market scanning')
466
+ .option('--analyze-model <model>', 'LLM model for deep analysis')
467
+ .option('--decide-model <model>', 'LLM model for trade decisions')
468
+ .addOption(new Option('--asset-class <class>', 'Asset class for this agent').choices(['crypto', 'commodities', 'mixed']))
469
+ .option('--soul-override <text>', 'Custom soul/personality for this agent')
470
+ .option('--purpose-override <text>', 'Custom purpose/mission for this agent')
471
+ .option('--soul-file <path>', 'Load soul from a file')
472
+ .option('--purpose-file <path>', 'Load purpose from a file')
473
+ .action(async (agentId, options) => {
474
+ if (!(await ensureRemote()))
475
+ return;
476
+ const body = {};
477
+ if (options.name)
478
+ body.name = options.name;
479
+ if (options.autonomy)
480
+ body.autonomyLevel = options.autonomy;
481
+ // Resolve scan interval: --scan-interval-human takes precedence
482
+ if (options.scanIntervalHuman) {
483
+ const ms = parseHumanInterval(options.scanIntervalHuman);
484
+ if (ms === null) {
485
+ console.error(chalk.red(`Error: Invalid duration "${options.scanIntervalHuman}". Use format like 1m, 5m, 15m, 30m, 1h.`));
486
+ process.exitCode = 1;
487
+ return;
488
+ }
489
+ if (ms < MIN_SCAN_INTERVAL_MS) {
490
+ console.error(chalk.red(`Error: Scan interval must be at least 1m (60000ms). Got ${formatInterval(ms)}.`));
491
+ process.exitCode = 1;
492
+ return;
493
+ }
494
+ body.scanIntervalMs = ms;
495
+ }
496
+ else if (options.scanInterval) {
497
+ body.scanIntervalMs = parseInt(options.scanInterval, 10);
498
+ }
499
+ if (options.watchlist)
500
+ body.watchlist = options.watchlist.split(',').map((s) => s.trim().toUpperCase());
501
+ if (options.maxDailyTrades)
502
+ body.maxDailyTrades = parseInt(options.maxDailyTrades, 10);
503
+ if (options.maxDailyLoss)
504
+ body.maxDailyLossUsd = parseFloat(options.maxDailyLoss);
505
+ if (options.maxPositionSize)
506
+ body.maxPositionSizePct = parseFloat(options.maxPositionSize);
507
+ if (options.minConfidence)
508
+ body.minConfidence = parseFloat(options.minConfidence);
509
+ if (options.scanModel)
510
+ body.scanModel = options.scanModel;
511
+ if (options.analyzeModel)
512
+ body.analyzeModel = options.analyzeModel;
513
+ if (options.decideModel)
514
+ body.decideModel = options.decideModel;
515
+ if (options.assetClass)
516
+ body.assetClass = options.assetClass;
517
+ // Soul override — file takes precedence over inline text
518
+ if (options.soulFile) {
519
+ const path = resolve(options.soulFile);
520
+ if (!existsSync(path)) {
521
+ console.error(chalk.red(`Error: Soul file not found: ${path}`));
522
+ process.exitCode = 1;
523
+ return;
524
+ }
525
+ body.soulOverride = readFileSync(path, 'utf-8');
526
+ }
527
+ else if (options.soulOverride) {
528
+ body.soulOverride = options.soulOverride;
529
+ }
530
+ // Purpose override — file takes precedence over inline text
531
+ if (options.purposeFile) {
532
+ const path = resolve(options.purposeFile);
533
+ if (!existsSync(path)) {
534
+ console.error(chalk.red(`Error: Purpose file not found: ${path}`));
535
+ process.exitCode = 1;
536
+ return;
537
+ }
538
+ body.purposeOverride = readFileSync(path, 'utf-8');
539
+ }
540
+ else if (options.purposeOverride) {
541
+ body.purposeOverride = options.purposeOverride;
542
+ }
543
+ if (Object.keys(body).length === 0) {
544
+ console.error(chalk.yellow(' No updates specified. Use --name, --autonomy, --watchlist, etc.'));
545
+ process.exitCode = 1;
546
+ return;
547
+ }
548
+ try {
549
+ await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}`, {
550
+ method: 'PATCH',
551
+ body,
552
+ });
553
+ console.log(chalk.green(` Agent ${agentId} updated`));
554
+ }
555
+ catch (error) {
556
+ handleApiError(error, 'Agent update failed');
557
+ }
558
+ });
559
+ }
560
+ //# sourceMappingURL=agent-cmd.js.map
@@ -323,8 +323,11 @@ export function registerConfigCommand(program) {
323
323
  .command('set-llm-key <apiKey>')
324
324
  .description('Store your LLM API key for thesis extraction + coaching (BYOK)')
325
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')
326
+ .option('-m, --model <model>', 'Model name (default for all phases)')
327
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)')
328
331
  .action(async (apiKey, opts) => {
329
332
  try {
330
333
  const result = await apiRequest('/api/v1/llm-config', {
@@ -334,13 +337,25 @@ export function registerConfigCommand(program) {
334
337
  ...(opts.provider ? { provider: opts.provider } : {}),
335
338
  ...(opts.model ? { model: opts.model } : {}),
336
339
  ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
340
+ ...(opts.scanModel ? { scanModel: opts.scanModel } : {}),
341
+ ...(opts.analyzeModel ? { analyzeModel: opts.analyzeModel } : {}),
342
+ ...(opts.decideModel ? { decideModel: opts.decideModel } : {}),
337
343
  },
338
344
  });
339
345
  console.log('');
340
346
  console.log(chalk.green(' LLM API key saved successfully'));
341
- console.log(` ${chalk.gray('Provider:')} ${result.provider}`);
342
- console.log(` ${chalk.gray('Model:')} ${result.model}`);
343
- console.log(` ${chalk.gray('Key:')} ${apiKey.slice(0, 8)}${'*'.repeat(Math.max(0, apiKey.length - 8))}`);
347
+ console.log(` ${chalk.gray('Provider:')} ${result.provider}`);
348
+ console.log(` ${chalk.gray('Model:')} ${result.model}`);
349
+ if (result.scanModel) {
350
+ console.log(` ${chalk.gray('Scan model:')} ${result.scanModel}`);
351
+ }
352
+ if (result.analyzeModel) {
353
+ console.log(` ${chalk.gray('Analyze model:')} ${result.analyzeModel}`);
354
+ }
355
+ if (result.decideModel) {
356
+ console.log(` ${chalk.gray('Decide model:')} ${result.decideModel}`);
357
+ }
358
+ console.log(` ${chalk.gray('Key:')} ${apiKey.slice(0, 8)}${'*'.repeat(Math.max(0, apiKey.length - 8))}`);
344
359
  console.log('');
345
360
  console.log(chalk.dim(' Your key is encrypted at rest. Thesis extraction and coaching are now enabled.'));
346
361
  console.log('');
@@ -367,12 +382,21 @@ export function registerConfigCommand(program) {
367
382
  console.log('');
368
383
  console.log(chalk.bold.cyan(' LLM Configuration'));
369
384
  console.log(chalk.gray(' ' + '\u2500'.repeat(40)));
370
- console.log(` ${chalk.gray('Provider:')} ${result.provider}`);
371
- console.log(` ${chalk.gray('Model:')} ${result.model}`);
385
+ console.log(` ${chalk.gray('Provider:')} ${result.provider}`);
386
+ console.log(` ${chalk.gray('Model:')} ${result.model}`);
387
+ if (result.scanModel) {
388
+ console.log(` ${chalk.gray('Scan model:')} ${result.scanModel}`);
389
+ }
390
+ if (result.analyzeModel) {
391
+ console.log(` ${chalk.gray('Analyze model:')} ${result.analyzeModel}`);
392
+ }
393
+ if (result.decideModel) {
394
+ console.log(` ${chalk.gray('Decide model:')} ${result.decideModel}`);
395
+ }
372
396
  if (result.baseUrl) {
373
- console.log(` ${chalk.gray('Base URL:')} ${result.baseUrl}`);
397
+ console.log(` ${chalk.gray('Base URL:')} ${result.baseUrl}`);
374
398
  }
375
- console.log(` ${chalk.gray('Updated:')} ${new Date(result.updatedAt).toLocaleString()}`);
399
+ console.log(` ${chalk.gray('Updated:')} ${new Date(result.updatedAt).toLocaleString()}`);
376
400
  console.log('');
377
401
  }
378
402
  }