@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.bundle.js +5197 -530
- package/dist/cli.js +8 -0
- package/dist/commands/agent-cmd.d.ts +9 -0
- package/dist/commands/agent-cmd.js +560 -0
- package/dist/commands/config-cmd.js +32 -8
- package/dist/commands/cron-cmd.d.ts +3 -0
- package/dist/commands/cron-cmd.js +341 -0
- package/package.json +2 -2
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:')}
|
|
342
|
-
console.log(` ${chalk.gray('Model:')}
|
|
343
|
-
|
|
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:')}
|
|
371
|
-
console.log(` ${chalk.gray('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:')}
|
|
397
|
+
console.log(` ${chalk.gray('Base URL:')} ${result.baseUrl}`);
|
|
374
398
|
}
|
|
375
|
-
console.log(` ${chalk.gray('Updated:')}
|
|
399
|
+
console.log(` ${chalk.gray('Updated:')} ${new Date(result.updatedAt).toLocaleString()}`);
|
|
376
400
|
console.log('');
|
|
377
401
|
}
|
|
378
402
|
}
|