@trading-boy/cli 1.2.19 → 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.
- package/dist/cli.bundle.js +5206 -536
- package/dist/cli.js +8 -0
- package/dist/commands/agent-cmd.d.ts +9 -0
- package/dist/commands/agent-cmd.js +522 -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/dist/commands/trader.js +24 -4
- package/dist/commands/watch.js +17 -1
- 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/dist/commands/trader.js
CHANGED
|
@@ -424,11 +424,21 @@ export function registerTraderCommand(program) {
|
|
|
424
424
|
}
|
|
425
425
|
catch (error) {
|
|
426
426
|
if (error instanceof ApiError && error.status === 404) {
|
|
427
|
-
|
|
427
|
+
if (options.format === 'json') {
|
|
428
|
+
console.log(JSON.stringify({ soul: null, message: options.file ? `Trader not found: "${nameOrId}"` : 'No SOUL document found' }));
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.error(chalk.yellow(options.file ? `Trader not found: "${nameOrId}"` : 'No SOUL document found.'));
|
|
432
|
+
}
|
|
428
433
|
}
|
|
429
434
|
else {
|
|
430
435
|
const message = error instanceof Error ? error.message : String(error);
|
|
431
|
-
|
|
436
|
+
if (options.format === 'json') {
|
|
437
|
+
console.log(JSON.stringify({ error: message }));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
441
|
+
}
|
|
432
442
|
}
|
|
433
443
|
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
434
444
|
}
|
|
@@ -484,11 +494,21 @@ export function registerTraderCommand(program) {
|
|
|
484
494
|
}
|
|
485
495
|
catch (error) {
|
|
486
496
|
if (error instanceof ApiError && error.status === 404) {
|
|
487
|
-
|
|
497
|
+
if (options.format === 'json') {
|
|
498
|
+
console.log(JSON.stringify({ purpose: null, message: options.file ? `Trader not found: "${nameOrId}"` : 'No PURPOSE document found' }));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
console.error(chalk.yellow(options.file ? `Trader not found: "${nameOrId}"` : 'No PURPOSE document found.'));
|
|
502
|
+
}
|
|
488
503
|
}
|
|
489
504
|
else {
|
|
490
505
|
const message = error instanceof Error ? error.message : String(error);
|
|
491
|
-
|
|
506
|
+
if (options.format === 'json') {
|
|
507
|
+
console.log(JSON.stringify({ error: message }));
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
511
|
+
}
|
|
492
512
|
}
|
|
493
513
|
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
494
514
|
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import { createLogger } from '@trading-boy/core';
|
|
3
4
|
import { formatContextOutput } from './context.js';
|
|
@@ -61,6 +62,7 @@ export function registerWatchCommand(program) {
|
|
|
61
62
|
.description('Watch token data with periodic refresh')
|
|
62
63
|
.option('-i, --interval <seconds>', 'Refresh interval in seconds', (v) => parseInt(v, 10))
|
|
63
64
|
.option('-c, --context', 'Watch full ContextPackage (default interval: 60s)')
|
|
65
|
+
.addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
|
|
64
66
|
.action(async (symbol, options) => {
|
|
65
67
|
const useContext = options.context === true;
|
|
66
68
|
const defaultInterval = useContext ? DEFAULT_CONTEXT_INTERVAL_SECONDS : DEFAULT_INTERVAL_SECONDS;
|
|
@@ -71,8 +73,22 @@ export function registerWatchCommand(program) {
|
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
73
75
|
const mode = useContext ? 'context' : 'query';
|
|
76
|
+
const isJson = options.format === 'json';
|
|
74
77
|
logger.info({ symbol, intervalSeconds, mode }, 'Starting watch mode');
|
|
75
|
-
const
|
|
78
|
+
const renderFn = isJson
|
|
79
|
+
? async (sym) => {
|
|
80
|
+
try {
|
|
81
|
+
const pkg = await apiRequest(`/api/v1/tokens/${encodeURIComponent(sym.toUpperCase())}/context`);
|
|
82
|
+
console.log(JSON.stringify({ watchedAt: new Date().toISOString(), symbol: sym.toUpperCase(), ...pkg }, null, 2));
|
|
83
|
+
return pkg;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString() }));
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
: renderCycleRemote;
|
|
91
|
+
const state = startWatchLoop(symbol, intervalSeconds, renderFn);
|
|
76
92
|
// Clean exit on SIGINT (Ctrl+C)
|
|
77
93
|
const cleanup = () => {
|
|
78
94
|
logger.info('Watch mode stopped by user');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trading-boy/cli",
|
|
3
|
-
"version": "1.
|
|
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
|
|
56
|
+
"@trading-boy/core": "workspace:*",
|
|
57
57
|
"esbuild": "~0.27.4",
|
|
58
58
|
"typescript": "^5.7.0"
|
|
59
59
|
}
|