@trading-boy/cli 1.6.1 → 1.7.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.
@@ -12,45 +12,9 @@ import { resolve } from 'node:path';
12
12
  import { Option } from 'commander';
13
13
  import chalk from 'chalk';
14
14
  import { createLogger } from '@trading-boy/core';
15
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
16
- import { padRight } from '../utils.js';
15
+ import { apiRequest } from '../api-client.js';
16
+ import { padRight, handleApiError, ensureRemote } from '../utils.js';
17
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
18
  // ─── Formatters ───
55
19
  function formatShortDate(isoString) {
56
20
  if (!isoString)
@@ -241,7 +205,7 @@ export function registerAgentCommand(program) {
241
205
  }
242
206
  }
243
207
  catch (error) {
244
- handleApiError(error, 'Agent create failed');
208
+ handleApiError(error, 'Agent create failed', logger);
245
209
  }
246
210
  });
247
211
  // ── list ────────────────────────────────────────────────────────────────────
@@ -295,7 +259,7 @@ export function registerAgentCommand(program) {
295
259
  console.log('');
296
260
  }
297
261
  catch (error) {
298
- handleApiError(error, 'Agent list failed');
262
+ handleApiError(error, 'Agent list failed', logger);
299
263
  }
300
264
  });
301
265
  // ── show ────────────────────────────────────────────────────────────────────
@@ -375,58 +339,76 @@ export function registerAgentCommand(program) {
375
339
  console.log('');
376
340
  }
377
341
  catch (error) {
378
- handleApiError(error, 'Agent show failed');
342
+ handleApiError(error, 'Agent show failed', logger);
379
343
  }
380
344
  });
381
345
  // ── pause ───────────────────────────────────────────────────────────────────
382
346
  agent
383
347
  .command('pause <agentId>')
384
348
  .description('Pause an agent')
385
- .action(async (agentId) => {
349
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
350
+ .action(async (agentId, options) => {
386
351
  if (!(await ensureRemote()))
387
352
  return;
388
353
  try {
389
354
  await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}/pause`, {
390
355
  method: 'POST',
391
356
  });
392
- console.log(chalk.green(` Agent ${agentId} paused`));
357
+ if (options.format === 'json') {
358
+ console.log(JSON.stringify({ agentId, status: 'paused' }, null, 2));
359
+ }
360
+ else {
361
+ console.log(chalk.green(` Agent ${agentId} paused`));
362
+ }
393
363
  }
394
364
  catch (error) {
395
- handleApiError(error, 'Agent pause failed');
365
+ handleApiError(error, 'Agent pause failed', logger);
396
366
  }
397
367
  });
398
368
  // ── resume ──────────────────────────────────────────────────────────────────
399
369
  agent
400
370
  .command('resume <agentId>')
401
371
  .description('Resume a paused agent')
402
- .action(async (agentId) => {
372
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
373
+ .action(async (agentId, options) => {
403
374
  if (!(await ensureRemote()))
404
375
  return;
405
376
  try {
406
377
  await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}/resume`, {
407
378
  method: 'POST',
408
379
  });
409
- console.log(chalk.green(` Agent ${agentId} resumed`));
380
+ if (options.format === 'json') {
381
+ console.log(JSON.stringify({ agentId, status: 'active' }, null, 2));
382
+ }
383
+ else {
384
+ console.log(chalk.green(` Agent ${agentId} resumed`));
385
+ }
410
386
  }
411
387
  catch (error) {
412
- handleApiError(error, 'Agent resume failed');
388
+ handleApiError(error, 'Agent resume failed', logger);
413
389
  }
414
390
  });
415
391
  // ── delete ──────────────────────────────────────────────────────────────────
416
392
  agent
417
393
  .command('delete <agentId>')
418
394
  .description('Delete an agent')
419
- .action(async (agentId) => {
395
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
396
+ .action(async (agentId, options) => {
420
397
  if (!(await ensureRemote()))
421
398
  return;
422
399
  try {
423
400
  await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}`, {
424
401
  method: 'DELETE',
425
402
  });
426
- console.log(chalk.green(` Agent ${agentId} deleted`));
403
+ if (options.format === 'json') {
404
+ console.log(JSON.stringify({ agentId, status: 'deleted' }, null, 2));
405
+ }
406
+ else {
407
+ console.log(chalk.green(` Agent ${agentId} deleted`));
408
+ }
427
409
  }
428
410
  catch (error) {
429
- handleApiError(error, 'Agent delete failed');
411
+ handleApiError(error, 'Agent delete failed', logger);
430
412
  }
431
413
  });
432
414
  // ── exit ───────────────────────────────────────────────────────────────────
@@ -464,7 +446,7 @@ export function registerAgentCommand(program) {
464
446
  }
465
447
  }
466
448
  catch (error) {
467
- handleApiError(error, 'Position exit failed');
449
+ handleApiError(error, 'Position exit failed', logger);
468
450
  }
469
451
  });
470
452
  // ── update ──────────────────────────────────────────────────────────────────
@@ -488,6 +470,7 @@ export function registerAgentCommand(program) {
488
470
  .option('--purpose-override <text>', 'Custom purpose/mission for this agent')
489
471
  .option('--soul-file <path>', 'Load soul from a file')
490
472
  .option('--purpose-file <path>', 'Load purpose from a file')
473
+ .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
491
474
  .action(async (agentId, options) => {
492
475
  if (!(await ensureRemote()))
493
476
  return;
@@ -564,14 +547,19 @@ export function registerAgentCommand(program) {
564
547
  return;
565
548
  }
566
549
  try {
567
- await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}`, {
550
+ const result = await apiRequest(`/api/v1/agents/${encodeURIComponent(agentId)}`, {
568
551
  method: 'PATCH',
569
552
  body,
570
553
  });
571
- console.log(chalk.green(` Agent ${agentId} updated`));
554
+ if (options.format === 'json') {
555
+ console.log(JSON.stringify(result, null, 2));
556
+ }
557
+ else {
558
+ console.log(chalk.green(` Agent ${agentId} updated`));
559
+ }
572
560
  }
573
561
  catch (error) {
574
- handleApiError(error, 'Agent update failed');
562
+ handleApiError(error, 'Agent update failed', logger);
575
563
  }
576
564
  });
577
565
  }
@@ -8,7 +8,8 @@
8
8
  // trading-boy benchmark recompute # Trigger recompute
9
9
  import { Option } from 'commander';
10
10
  import chalk from 'chalk';
11
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
11
+ import { apiRequest, ApiError } from '../api-client.js';
12
+ import { ensureRemote } from '../utils.js';
12
13
  // ─── Formatters ───
13
14
  function formatMetric(value, decimals = 2) {
14
15
  if (value === null)
@@ -118,11 +119,8 @@ export function registerBenchmarkCommand(program) {
118
119
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
119
120
  .action(async (options) => {
120
121
  try {
121
- if (!(await isRemoteMode())) {
122
- console.error(chalk.red('Error: benchmark command requires remote mode. Run `trading-boy login` first.'));
123
- process.exitCode = 1;
122
+ if (!(await ensureRemote()))
124
123
  return;
125
- }
126
124
  if (options.entity) {
127
125
  // Single entity
128
126
  const query = new URLSearchParams();
@@ -175,11 +173,8 @@ export function registerBenchmarkCommand(program) {
175
173
  .description('Trigger on-demand benchmark recompute')
176
174
  .action(async () => {
177
175
  try {
178
- if (!(await isRemoteMode())) {
179
- console.error(chalk.red('Error: benchmark recompute requires remote mode. Run `trading-boy login` first.'));
180
- process.exitCode = 1;
176
+ if (!(await ensureRemote()))
181
177
  return;
182
- }
183
178
  console.log(chalk.dim('Recomputing benchmarks...'));
184
179
  const data = await apiRequest('/api/v1/benchmark/recompute', { method: 'POST', body: {} });
185
180
  console.log(chalk.green(`\n ${data.message}`));
@@ -4,6 +4,11 @@ import { createLogger } from '@trading-boy/core';
4
4
  import { apiRequest, ApiError } from '../api-client.js';
5
5
  import { padRight } from '../utils.js';
6
6
  const logger = createLogger('cli-billing');
7
+ // ─── Security ───
8
+ const ALLOWED_PORTAL_DOMAINS = new Set([
9
+ 'billing.stripe.com',
10
+ 'checkout.stripe.com',
11
+ ]);
7
12
  // ─── Formatters ───
8
13
  function formatStatusLabel(status) {
9
14
  switch (status) {
@@ -75,11 +80,14 @@ export function registerBillingCommand(program) {
75
80
  method: 'POST',
76
81
  });
77
82
  spinner.succeed(' Opening billing portal...');
78
- // Validate URL before opening browser (F1: prevent phishing via MITM)
83
+ // Validate URL before opening browser (prevent phishing via MITM)
79
84
  const portalUrl = new URL(result.url);
80
85
  if (portalUrl.protocol !== 'https:') {
81
86
  throw new Error(`Refusing to open non-HTTPS portal URL: ${result.url}`);
82
87
  }
88
+ if (!ALLOWED_PORTAL_DOMAINS.has(portalUrl.hostname)) {
89
+ throw new Error(`Refusing to open URL with untrusted domain: ${portalUrl.hostname}`);
90
+ }
83
91
  const { default: open } = await import('open');
84
92
  await open(result.url);
85
93
  console.log('');
@@ -112,6 +120,18 @@ export function registerBillingCommand(program) {
112
120
  }
113
121
  }
114
122
  catch (error) {
123
+ if (error instanceof ApiError && error.status === 404) {
124
+ if (options.format === 'json') {
125
+ console.log(JSON.stringify({ plan: 'starter', status: 'free' }, null, 2));
126
+ }
127
+ else {
128
+ console.log('');
129
+ console.log(chalk.dim(' You\'re on the free Starter plan.'));
130
+ console.log(chalk.dim(' Run: trading-boy subscribe to upgrade.'));
131
+ console.log('');
132
+ }
133
+ return;
134
+ }
115
135
  const message = error instanceof Error ? error.message : String(error);
116
136
  logger.error({ error: message }, 'Billing status failed');
117
137
  console.error(chalk.red(` Error: ${message}`));
@@ -1,7 +1,8 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { createLogger } from '@trading-boy/core';
4
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
4
+ import { apiRequest, ApiError } from '../api-client.js';
5
+ import { ensureRemote } from '../utils.js';
5
6
  // ─── Logger ───
6
7
  const logger = createLogger('cli-coaching');
7
8
  // ─── BYOK Error Hint ───
@@ -20,16 +21,6 @@ function isByokError(error) {
20
21
  }
21
22
  return false;
22
23
  }
23
- // ─── Remote Mode Guard ───
24
- async function requireRemote() {
25
- if (!(await isRemoteMode())) {
26
- console.error(chalk.yellow('Coaching requires a remote API connection.'));
27
- console.error(chalk.dim(' Run: trading-boy login'));
28
- process.exitCode = 1;
29
- return false;
30
- }
31
- return true;
32
- }
33
24
  // ─── Formatters ───
34
25
  export function formatIntervention(response) {
35
26
  const lines = [];
@@ -80,7 +71,7 @@ export function registerCoachingCommand(program) {
80
71
  .option('--direction <dir>', 'Trade direction (LONG or SHORT)')
81
72
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
82
73
  .action(async (traderId, options) => {
83
- if (!(await requireRemote()))
74
+ if (!(await ensureRemote()))
84
75
  return;
85
76
  // Build extraction object from flags
86
77
  const extraction = {
@@ -143,7 +134,7 @@ export function registerCoachingCommand(program) {
143
134
  .option('--flags <flags>', 'Active behavioral flags (comma-separated)')
144
135
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
145
136
  .action(async (traderId, options) => {
146
- if (!(await requireRemote()))
137
+ if (!(await ensureRemote()))
147
138
  return;
148
139
  const body = {
149
140
  tiltScore: options.tiltScore ?? 0,
@@ -191,7 +182,7 @@ export function registerCoachingCommand(program) {
191
182
  .description('Acknowledge a coaching intervention')
192
183
  .requiredOption('--reason <reason>', 'Reason for acknowledgment')
193
184
  .action(async (traderId, options) => {
194
- if (!(await requireRemote()))
185
+ if (!(await ensureRemote()))
195
186
  return;
196
187
  try {
197
188
  const result = await apiRequest(`/api/v1/traders/${encodeURIComponent(traderId)}/coaching/acknowledge`, { method: 'POST', body: { reason: options.reason } });
@@ -212,7 +203,7 @@ export function registerCoachingCommand(program) {
212
203
  .command('reset <traderId>')
213
204
  .description('Reset coaching session state')
214
205
  .action(async (traderId) => {
215
- if (!(await requireRemote()))
206
+ if (!(await ensureRemote()))
216
207
  return;
217
208
  try {
218
209
  const result = await apiRequest(`/api/v1/traders/${encodeURIComponent(traderId)}/coaching/reset-session`, { method: 'POST', body: {} });
@@ -1,8 +1,8 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { createLogger } from '@trading-boy/core';
4
- import { formatConnectionError, colorChange, colorSentiment, colorRiskScore, formatUsd } from '../utils.js';
5
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
4
+ import { formatConnectionError, colorChange, colorSentiment, colorRiskScore, formatUsd, ensureRemote } from '../utils.js';
5
+ import { apiRequest, ApiError } from '../api-client.js';
6
6
  // ─── Logger ───
7
7
  const logger = createLogger('cli-context');
8
8
  // ─── Formatters ───
@@ -325,12 +325,8 @@ export function registerContextCommand(program) {
325
325
  .action(async (symbol, options) => {
326
326
  try {
327
327
  // ─── Auth pre-flight ───
328
- if (!(await isRemoteMode())) {
329
- console.error(chalk.yellow('This command requires a remote API connection.'));
330
- console.error(chalk.dim(' Run: trading-boy login'));
331
- process.exitCode = 1;
328
+ if (!(await ensureRemote()))
332
329
  return;
333
- }
334
330
  // ─── Validate range options ───
335
331
  if ((options.from && !options.to) || (!options.from && options.to)) {
336
332
  console.error(chalk.red('Error: Both --from and --to must be provided for range queries.'));
@@ -11,45 +11,9 @@
11
11
  import { Option } from 'commander';
12
12
  import chalk from 'chalk';
13
13
  import { createLogger } from '@trading-boy/core';
14
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
15
- import { padRight } from '../utils.js';
14
+ import { apiRequest } from '../api-client.js';
15
+ import { padRight, handleApiError, ensureRemote } from '../utils.js';
16
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
17
  // ─── Formatters ───
54
18
  function formatShortDate(isoString) {
55
19
  if (!isoString)
@@ -137,7 +101,7 @@ export function registerCronCommand(program) {
137
101
  }
138
102
  }
139
103
  catch (error) {
140
- handleApiError(error, 'Cron create failed');
104
+ handleApiError(error, 'Cron create failed', logger);
141
105
  }
142
106
  });
143
107
  // ── list ────────────────────────────────────────────────────────────────────
@@ -183,7 +147,7 @@ export function registerCronCommand(program) {
183
147
  console.log('');
184
148
  }
185
149
  catch (error) {
186
- handleApiError(error, 'Cron list failed');
150
+ handleApiError(error, 'Cron list failed', logger);
187
151
  }
188
152
  });
189
153
  // ── show ────────────────────────────────────────────────────────────────────
@@ -217,7 +181,7 @@ export function registerCronCommand(program) {
217
181
  console.log('');
218
182
  }
219
183
  catch (error) {
220
- handleApiError(error, 'Cron show failed');
184
+ handleApiError(error, 'Cron show failed', logger);
221
185
  }
222
186
  });
223
187
  // ── pause ───────────────────────────────────────────────────────────────────
@@ -235,7 +199,7 @@ export function registerCronCommand(program) {
235
199
  console.log(chalk.green(` Job ${jobId} paused`));
236
200
  }
237
201
  catch (error) {
238
- handleApiError(error, 'Cron pause failed');
202
+ handleApiError(error, 'Cron pause failed', logger);
239
203
  }
240
204
  });
241
205
  // ── resume ──────────────────────────────────────────────────────────────────
@@ -253,7 +217,7 @@ export function registerCronCommand(program) {
253
217
  console.log(chalk.green(` Job ${jobId} resumed`));
254
218
  }
255
219
  catch (error) {
256
- handleApiError(error, 'Cron resume failed');
220
+ handleApiError(error, 'Cron resume failed', logger);
257
221
  }
258
222
  });
259
223
  // ── delete ──────────────────────────────────────────────────────────────────
@@ -270,7 +234,7 @@ export function registerCronCommand(program) {
270
234
  console.log(chalk.green(` Job ${jobId} deleted`));
271
235
  }
272
236
  catch (error) {
273
- handleApiError(error, 'Cron delete failed');
237
+ handleApiError(error, 'Cron delete failed', logger);
274
238
  }
275
239
  });
276
240
  // ── run ─────────────────────────────────────────────────────────────────────
@@ -287,7 +251,7 @@ export function registerCronCommand(program) {
287
251
  console.log(chalk.green(` Job ${jobId} execution triggered`));
288
252
  }
289
253
  catch (error) {
290
- handleApiError(error, 'Cron run failed');
254
+ handleApiError(error, 'Cron run failed', logger);
291
255
  }
292
256
  });
293
257
  // ── history ─────────────────────────────────────────────────────────────────
@@ -334,7 +298,7 @@ export function registerCronCommand(program) {
334
298
  console.log('');
335
299
  }
336
300
  catch (error) {
337
- handleApiError(error, 'Cron history failed');
301
+ handleApiError(error, 'Cron history failed', logger);
338
302
  }
339
303
  });
340
304
  }
@@ -1,6 +1,7 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
3
+ import { apiRequest, ApiError } from '../api-client.js';
4
+ import { ensureRemote } from '../utils.js';
4
5
  // ─── Formatters ───
5
6
  export function formatEdgeOutput(data) {
6
7
  const lines = [];
@@ -145,11 +146,8 @@ export function registerEdgeCommand(program) {
145
146
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
146
147
  .action(async (traderId, options) => {
147
148
  try {
148
- if (!(await isRemoteMode())) {
149
- console.error(chalk.red('Error: edge command requires remote mode. Run `trading-boy login` first.'));
150
- process.exitCode = 1;
149
+ if (!(await ensureRemote()))
151
150
  return;
152
- }
153
151
  const query = new URLSearchParams();
154
152
  if (options.token)
155
153
  query.set('tokenSymbol', options.token.toUpperCase());
@@ -1,6 +1,7 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
3
+ import { apiRequest, ApiError } from '../api-client.js';
4
+ import { ensureRemote } from '../utils.js';
4
5
  // ─── Formatters ───
5
6
  export function formatAssessmentOutput(data) {
6
7
  const lines = [];
@@ -114,11 +115,8 @@ export function registerEdgeGuardCommand(program) {
114
115
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
115
116
  .action(async (traderId, options) => {
116
117
  try {
117
- if (!(await isRemoteMode())) {
118
- console.error(chalk.red('Error: edge-guard command requires remote mode. Run `trading-boy login` first.'));
119
- process.exitCode = 1;
118
+ if (!(await ensureRemote()))
120
119
  return;
121
- }
122
120
  if (options.friction) {
123
121
  const data = await apiRequest(`/api/v1/traders/${encodeURIComponent(traderId)}/edge-guard/friction`);
124
122
  if (options.format === 'json') {
@@ -151,11 +149,8 @@ export function registerEdgeGuardCommand(program) {
151
149
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
152
150
  .action(async (traderId, ackOptions) => {
153
151
  try {
154
- if (!(await isRemoteMode())) {
155
- console.error(chalk.red('Error: edge-guard command requires remote mode. Run `trading-boy login` first.'));
156
- process.exitCode = 1;
152
+ if (!(await ensureRemote()))
157
153
  return;
158
- }
159
154
  const data = await apiRequest(`/api/v1/traders/${encodeURIComponent(traderId)}/edge-guard/acknowledge`, { method: 'POST' });
160
155
  if (ackOptions.format === 'json') {
161
156
  console.log(JSON.stringify(data, null, 2));
@@ -1,8 +1,8 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { createLogger } from '@trading-boy/core';
4
- import { formatConnectionError } from '../utils.js';
5
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
4
+ import { formatConnectionError, ensureRemote } from '../utils.js';
5
+ import { apiRequest, ApiError } from '../api-client.js';
6
6
  // ─── Logger ───
7
7
  const logger = createLogger('cli-query');
8
8
  // ─── Formatter ───
@@ -103,12 +103,8 @@ export function registerQueryCommand(program) {
103
103
  .action(async (symbol, options) => {
104
104
  try {
105
105
  // ─── Auth pre-flight ───
106
- if (!(await isRemoteMode())) {
107
- console.error(chalk.yellow('This command requires a remote API connection.'));
108
- console.error(chalk.dim(' Run: trading-boy login'));
109
- process.exitCode = 1;
106
+ if (!(await ensureRemote()))
110
107
  return;
111
- }
112
108
  const pkg = await apiRequest(`/api/v1/tokens/${encodeURIComponent(symbol.toUpperCase())}/context`);
113
109
  const result = contextToQueryResult(pkg);
114
110
  // Check if the token was actually found
@@ -1,8 +1,8 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { createLogger } from '@trading-boy/core';
4
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
5
- import { padRight } from '../utils.js';
4
+ import { apiRequest } from '../api-client.js';
5
+ import { padRight, handleApiError, ensureRemote } from '../utils.js';
6
6
  // ─── Logger ───
7
7
  const logger = createLogger('cli-replay');
8
8
  // ─── Helpers ───
@@ -96,33 +96,6 @@ async function pollReplayJob(jobId) {
96
96
  }
97
97
  throw new Error(`Replay job ${jobId} did not complete within ${POLL_TIMEOUT_MS / 1000}s timeout.`);
98
98
  }
99
- // ─── Error Handler ───
100
- function handleApiError(error, context) {
101
- if (error instanceof ApiError) {
102
- switch (error.status) {
103
- case 401:
104
- console.error(chalk.red('Error: API key invalid or expired. Run `trading-boy login` to re-authenticate.'));
105
- break;
106
- case 403:
107
- console.error(chalk.red('Error: Subscription inactive. Run `trading-boy billing manage` to update your billing.'));
108
- break;
109
- case 404:
110
- console.error(chalk.red(`Error: Not found — ${error.message}`));
111
- break;
112
- case 422:
113
- console.error(chalk.red(`Error: Validation failed — ${error.message}`));
114
- break;
115
- default:
116
- console.error(chalk.red(`Error: ${error.message}`));
117
- }
118
- }
119
- else {
120
- const message = error instanceof Error ? error.message : String(error);
121
- logger.error({ error: message }, context);
122
- console.error(chalk.red(`Error: ${message}`));
123
- }
124
- process.exitCode = error instanceof ApiError ? 2 : 1;
125
- }
126
99
  // ─── Command Registration ───
127
100
  export function registerReplayCommand(program) {
128
101
  program
@@ -134,12 +107,8 @@ export function registerReplayCommand(program) {
134
107
  .requiredOption('--to <date>', 'End date (ISO-8601, e.g. 2025-03-01)')
135
108
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
136
109
  .action(async (options) => {
137
- if (!(await isRemoteMode())) {
138
- console.error(chalk.yellow('Replay requires a remote API connection.'));
139
- console.error(chalk.dim(' Run: trading-boy login'));
140
- process.exitCode = 1;
110
+ if (!(await ensureRemote()))
141
111
  return;
142
- }
143
112
  // Validate dates
144
113
  const fromDate = new Date(options.from);
145
114
  const toDate = new Date(options.to);
@@ -178,7 +147,7 @@ export function registerReplayCommand(program) {
178
147
  }
179
148
  catch (error) {
180
149
  submitSpinner.fail('Failed to submit replay job');
181
- handleApiError(error, 'Replay submit failed');
150
+ handleApiError(error, 'Replay submit failed', logger);
182
151
  return;
183
152
  }
184
153
  // ─── Poll until complete ───
@@ -190,7 +159,7 @@ export function registerReplayCommand(program) {
190
159
  }
191
160
  catch (error) {
192
161
  pollSpinner.fail('Replay timed out or encountered an error');
193
- handleApiError(error, 'Replay polling failed');
162
+ handleApiError(error, 'Replay polling failed', logger);
194
163
  return;
195
164
  }
196
165
  // ─── Display results ───