@trading-boy/cli 1.2.20 → 1.3.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.
@@ -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.2.20",
3
+ "version": "1.3.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:~1.2.0",
56
+ "@trading-boy/core": "workspace:*",
57
57
  "esbuild": "~0.27.4",
58
58
  "typescript": "^5.7.0"
59
59
  }