@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// ─── Cron CLI Commands ───
|
|
2
|
+
//
|
|
3
|
+
// tb cron create — Create a new cron job
|
|
4
|
+
// tb cron list — List cron jobs
|
|
5
|
+
// tb cron show — Show a single job
|
|
6
|
+
// tb cron pause — Pause a job
|
|
7
|
+
// tb cron resume — Resume a paused job
|
|
8
|
+
// tb cron delete — Delete a job
|
|
9
|
+
// tb cron run — Trigger immediate execution
|
|
10
|
+
// tb cron history — View execution history
|
|
11
|
+
import { Option } from 'commander';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { createLogger } from '@trading-boy/core';
|
|
14
|
+
import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
|
|
15
|
+
import { padRight } from '../utils.js';
|
|
16
|
+
const logger = createLogger('cli-cron');
|
|
17
|
+
// ─── Error Handler ───
|
|
18
|
+
function handleApiError(error, context) {
|
|
19
|
+
if (error instanceof ApiError) {
|
|
20
|
+
switch (error.status) {
|
|
21
|
+
case 401:
|
|
22
|
+
console.error(chalk.red('Error: API key invalid or expired. Run `trading-boy login` to re-authenticate.'));
|
|
23
|
+
break;
|
|
24
|
+
case 403:
|
|
25
|
+
console.error(chalk.red('Error: Subscription inactive. Run `trading-boy billing manage` to update your billing.'));
|
|
26
|
+
break;
|
|
27
|
+
case 404:
|
|
28
|
+
console.error(chalk.red(`Error: Not found — ${error.message}`));
|
|
29
|
+
break;
|
|
30
|
+
case 429:
|
|
31
|
+
console.error(chalk.red(`Error: Limit reached — ${error.message}`));
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
logger.error({ error: message }, context);
|
|
40
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
41
|
+
}
|
|
42
|
+
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
43
|
+
}
|
|
44
|
+
async function ensureRemote() {
|
|
45
|
+
if (!(await isRemoteMode())) {
|
|
46
|
+
console.error(chalk.yellow('Cron commands require a remote API connection.'));
|
|
47
|
+
console.error(chalk.dim(' Run: trading-boy login'));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// ─── Formatters ───
|
|
54
|
+
function formatShortDate(isoString) {
|
|
55
|
+
if (!isoString)
|
|
56
|
+
return chalk.dim('—');
|
|
57
|
+
try {
|
|
58
|
+
return new Date(isoString).toISOString().slice(0, 19).replace('T', ' ');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return isoString;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function formatStatus(status) {
|
|
65
|
+
switch (status) {
|
|
66
|
+
case 'active': return chalk.green('active');
|
|
67
|
+
case 'paused': return chalk.yellow('paused');
|
|
68
|
+
case 'deleted': return chalk.red('deleted');
|
|
69
|
+
case 'completed': return chalk.green('completed');
|
|
70
|
+
case 'failed': return chalk.red('failed');
|
|
71
|
+
case 'running': return chalk.cyan('running');
|
|
72
|
+
default: return status;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ─── Command Registration ───
|
|
76
|
+
export function registerCronCommand(program) {
|
|
77
|
+
const cron = program
|
|
78
|
+
.command('cron')
|
|
79
|
+
.description('Manage scheduled cron jobs');
|
|
80
|
+
// ── create ──────────────────────────────────────────────────────────────────
|
|
81
|
+
cron
|
|
82
|
+
.command('create')
|
|
83
|
+
.description('Create a new cron job')
|
|
84
|
+
.requiredOption('--schedule <schedule>', 'Schedule: "every 15m", "daily at 9am EST", or cron expression')
|
|
85
|
+
.requiredOption('--type <type>', 'Job type: price_alert, custom_prompt, market_scan, portfolio_check, context_refresh')
|
|
86
|
+
.option('--name <name>', 'Job name (auto-generated if omitted)')
|
|
87
|
+
.option('--tokens <symbols>', 'Comma-separated token symbols (for market_scan)')
|
|
88
|
+
.option('--condition <condition>', 'Price condition, e.g. "BTC > 100000" (for price_alert)')
|
|
89
|
+
.option('--prompt <prompt>', 'Prompt text (for custom_prompt)')
|
|
90
|
+
.option('--delivery <channel>', 'Delivery channel: telegram, email, stream, silent', 'silent')
|
|
91
|
+
.option('--delivery-target <target>', 'Delivery target (chat ID, email address)')
|
|
92
|
+
.option('--timezone <tz>', 'Timezone (IANA name or abbreviation)')
|
|
93
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
94
|
+
.action(async (options) => {
|
|
95
|
+
if (!(await ensureRemote()))
|
|
96
|
+
return;
|
|
97
|
+
// Build config from options
|
|
98
|
+
const config = {};
|
|
99
|
+
if (options.tokens)
|
|
100
|
+
config.tokens = options.tokens.split(',').map((t) => t.trim().toUpperCase());
|
|
101
|
+
if (options.condition) {
|
|
102
|
+
// Extract token symbol from condition for price_alert
|
|
103
|
+
const match = options.condition.match(/^(\w+)\s/);
|
|
104
|
+
if (match)
|
|
105
|
+
config.tokenSymbol = match[1].toUpperCase();
|
|
106
|
+
config.condition = options.condition;
|
|
107
|
+
}
|
|
108
|
+
if (options.prompt)
|
|
109
|
+
config.prompt = options.prompt;
|
|
110
|
+
const name = options.name ?? `${options.type}: ${options.schedule}`;
|
|
111
|
+
try {
|
|
112
|
+
const result = await apiRequest('/api/v1/cron', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: {
|
|
115
|
+
name,
|
|
116
|
+
schedule: options.schedule,
|
|
117
|
+
type: options.type,
|
|
118
|
+
config,
|
|
119
|
+
delivery: options.delivery,
|
|
120
|
+
deliveryTarget: options.deliveryTarget,
|
|
121
|
+
timezone: options.timezone,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
if (options.format === 'json') {
|
|
125
|
+
console.log(JSON.stringify(result, null, 2));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(chalk.green(' Cron job created'));
|
|
130
|
+
console.log(` ${chalk.gray('ID:')} ${result.id}`);
|
|
131
|
+
console.log(` ${chalk.gray('Name:')} ${result.name}`);
|
|
132
|
+
console.log(` ${chalk.gray('Schedule:')} ${result.schedule} → ${chalk.dim(result.cronExpression)}`);
|
|
133
|
+
console.log(` ${chalk.gray('Timezone:')} ${result.timezone}`);
|
|
134
|
+
console.log(` ${chalk.gray('Type:')} ${result.type}`);
|
|
135
|
+
console.log(` ${chalk.gray('Next run:')} ${formatShortDate(result.nextRunAt)}`);
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
handleApiError(error, 'Cron create failed');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// ── list ────────────────────────────────────────────────────────────────────
|
|
144
|
+
cron
|
|
145
|
+
.command('list')
|
|
146
|
+
.description('List cron jobs')
|
|
147
|
+
.option('--status <status>', 'Filter by status: active, paused')
|
|
148
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
149
|
+
.action(async (options) => {
|
|
150
|
+
if (!(await ensureRemote()))
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
const query = options.status ? `?status=${options.status}` : '';
|
|
154
|
+
const result = await apiRequest(`/api/v1/cron${query}`);
|
|
155
|
+
if (options.format === 'json') {
|
|
156
|
+
console.log(JSON.stringify(result, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (result.jobs.length === 0) {
|
|
160
|
+
console.log(chalk.dim(' No cron jobs found'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log(' ' +
|
|
165
|
+
padRight('Name', 30) +
|
|
166
|
+
padRight('Type', 16) +
|
|
167
|
+
padRight('Schedule', 20) +
|
|
168
|
+
padRight('Status', 10) +
|
|
169
|
+
padRight('Runs', 6) +
|
|
170
|
+
'Next Run');
|
|
171
|
+
console.log(chalk.gray(' ' + '─'.repeat(100)));
|
|
172
|
+
for (const job of result.jobs) {
|
|
173
|
+
console.log(' ' +
|
|
174
|
+
padRight(job.name.slice(0, 28), 30) +
|
|
175
|
+
padRight(job.type, 16) +
|
|
176
|
+
padRight(job.schedule.slice(0, 18), 20) +
|
|
177
|
+
padRight(formatStatus(job.status), 10) +
|
|
178
|
+
padRight(String(job.runCount), 6) +
|
|
179
|
+
formatShortDate(job.nextRunAt));
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.dim(` ${result.count} job(s)`));
|
|
183
|
+
console.log('');
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
handleApiError(error, 'Cron list failed');
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// ── show ────────────────────────────────────────────────────────────────────
|
|
190
|
+
cron
|
|
191
|
+
.command('show <jobId>')
|
|
192
|
+
.description('Show details of a cron job')
|
|
193
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
194
|
+
.action(async (jobId, options) => {
|
|
195
|
+
if (!(await ensureRemote()))
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
const result = await apiRequest(`/api/v1/cron/${encodeURIComponent(jobId)}`);
|
|
199
|
+
if (options.format === 'json') {
|
|
200
|
+
console.log(JSON.stringify(result.job, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const job = result.job;
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(chalk.bold.cyan(` Cron Job — ${job.name}`));
|
|
206
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)));
|
|
207
|
+
console.log(` ${chalk.gray('ID:')} ${job.id}`);
|
|
208
|
+
console.log(` ${chalk.gray('Status:')} ${formatStatus(job.status)}`);
|
|
209
|
+
console.log(` ${chalk.gray('Type:')} ${job.type}`);
|
|
210
|
+
console.log(` ${chalk.gray('Schedule:')} ${job.schedule} → ${chalk.dim(job.cronExpression)}`);
|
|
211
|
+
console.log(` ${chalk.gray('Timezone:')} ${job.timezone}`);
|
|
212
|
+
console.log(` ${chalk.gray('Delivery:')} ${job.delivery}${job.deliveryTarget ? ` → ${job.deliveryTarget}` : ''}`);
|
|
213
|
+
console.log(` ${chalk.gray('Run count:')} ${job.runCount}`);
|
|
214
|
+
console.log(` ${chalk.gray('Last run:')} ${formatShortDate(job.lastRunAt)}`);
|
|
215
|
+
console.log(` ${chalk.gray('Next run:')} ${formatShortDate(job.nextRunAt)}`);
|
|
216
|
+
console.log(` ${chalk.gray('Created:')} ${formatShortDate(job.createdAt)}`);
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
handleApiError(error, 'Cron show failed');
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// ── pause ───────────────────────────────────────────────────────────────────
|
|
224
|
+
cron
|
|
225
|
+
.command('pause <jobId>')
|
|
226
|
+
.description('Pause a cron job')
|
|
227
|
+
.action(async (jobId) => {
|
|
228
|
+
if (!(await ensureRemote()))
|
|
229
|
+
return;
|
|
230
|
+
try {
|
|
231
|
+
await apiRequest(`/api/v1/cron/${encodeURIComponent(jobId)}`, {
|
|
232
|
+
method: 'PATCH',
|
|
233
|
+
body: { status: 'paused' },
|
|
234
|
+
});
|
|
235
|
+
console.log(chalk.green(` Job ${jobId} paused`));
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
handleApiError(error, 'Cron pause failed');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
// ── resume ──────────────────────────────────────────────────────────────────
|
|
242
|
+
cron
|
|
243
|
+
.command('resume <jobId>')
|
|
244
|
+
.description('Resume a paused cron job')
|
|
245
|
+
.action(async (jobId) => {
|
|
246
|
+
if (!(await ensureRemote()))
|
|
247
|
+
return;
|
|
248
|
+
try {
|
|
249
|
+
await apiRequest(`/api/v1/cron/${encodeURIComponent(jobId)}`, {
|
|
250
|
+
method: 'PATCH',
|
|
251
|
+
body: { status: 'active' },
|
|
252
|
+
});
|
|
253
|
+
console.log(chalk.green(` Job ${jobId} resumed`));
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
handleApiError(error, 'Cron resume failed');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// ── delete ──────────────────────────────────────────────────────────────────
|
|
260
|
+
cron
|
|
261
|
+
.command('delete <jobId>')
|
|
262
|
+
.description('Delete a cron job')
|
|
263
|
+
.action(async (jobId) => {
|
|
264
|
+
if (!(await ensureRemote()))
|
|
265
|
+
return;
|
|
266
|
+
try {
|
|
267
|
+
await apiRequest(`/api/v1/cron/${encodeURIComponent(jobId)}`, {
|
|
268
|
+
method: 'DELETE',
|
|
269
|
+
});
|
|
270
|
+
console.log(chalk.green(` Job ${jobId} deleted`));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
handleApiError(error, 'Cron delete failed');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
// ── run ─────────────────────────────────────────────────────────────────────
|
|
277
|
+
cron
|
|
278
|
+
.command('run <jobId>')
|
|
279
|
+
.description('Trigger immediate execution of a cron job')
|
|
280
|
+
.action(async (jobId) => {
|
|
281
|
+
if (!(await ensureRemote()))
|
|
282
|
+
return;
|
|
283
|
+
try {
|
|
284
|
+
await apiRequest(`/api/v1/cron/${encodeURIComponent(jobId)}/run`, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
});
|
|
287
|
+
console.log(chalk.green(` Job ${jobId} execution triggered`));
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
handleApiError(error, 'Cron run failed');
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// ── history ─────────────────────────────────────────────────────────────────
|
|
294
|
+
cron
|
|
295
|
+
.command('history <jobId>')
|
|
296
|
+
.description('View execution history for a cron job')
|
|
297
|
+
.option('--limit <n>', 'Number of runs to show', '20')
|
|
298
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
299
|
+
.action(async (jobId, options) => {
|
|
300
|
+
if (!(await ensureRemote()))
|
|
301
|
+
return;
|
|
302
|
+
try {
|
|
303
|
+
const limit = parseInt(options.limit, 10) || 20;
|
|
304
|
+
const result = await apiRequest(`/api/v1/cron/${encodeURIComponent(jobId)}/history?limit=${limit}`);
|
|
305
|
+
if (options.format === 'json') {
|
|
306
|
+
console.log(JSON.stringify(result, null, 2));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (result.runs.length === 0) {
|
|
310
|
+
console.log(chalk.dim(' No execution history'));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log(' ' +
|
|
315
|
+
padRight('Time', 22) +
|
|
316
|
+
padRight('Status', 12) +
|
|
317
|
+
padRight('Tokens', 8) +
|
|
318
|
+
padRight('Delivered', 10) +
|
|
319
|
+
'Result');
|
|
320
|
+
console.log(chalk.gray(' ' + '─'.repeat(90)));
|
|
321
|
+
for (const run of result.runs) {
|
|
322
|
+
const summary = run.error
|
|
323
|
+
? chalk.red(run.error.slice(0, 40))
|
|
324
|
+
: (run.resultSummary?.slice(0, 40) ?? chalk.dim('—'));
|
|
325
|
+
console.log(' ' +
|
|
326
|
+
padRight(formatShortDate(run.time), 22) +
|
|
327
|
+
padRight(formatStatus(run.status), 12) +
|
|
328
|
+
padRight(String(run.tokensUsed), 8) +
|
|
329
|
+
padRight(run.delivered ? chalk.green('yes') : chalk.dim('no'), 10) +
|
|
330
|
+
summary);
|
|
331
|
+
}
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(chalk.dim(` ${result.count} run(s)`));
|
|
334
|
+
console.log('');
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
handleApiError(error, 'Cron history failed');
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=cron-cmd.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trading-boy/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Trading Boy CLI — crypto context intelligence for traders and AI agents. Query real-time prices, funding rates, whale activity, and DeFi risk for 100+ Solana tokens and 229 Hyperliquid perpetuals.",
|
|
5
5
|
"homepage": "https://cabal.ventures",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"qrcode-terminal": "~0.12.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@trading-boy/core": "workspace
|
|
56
|
+
"@trading-boy/core": "workspace:*",
|
|
57
57
|
"esbuild": "~0.27.4",
|
|
58
58
|
"typescript": "^5.7.0"
|
|
59
59
|
}
|