@trading-boy/cli 1.6.1 → 1.8.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.
@@ -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 ───
@@ -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, padRight } from '../utils.js';
5
- import { isRemoteMode, apiRequest, ApiError } from '../api-client.js';
4
+ import { formatConnectionError, padRight, ensureRemote } from '../utils.js';
5
+ import { apiRequest, ApiError } from '../api-client.js';
6
6
  // ─── Logger ───
7
7
  const logger = createLogger('cli-risk');
8
8
  // ─── Formatters ───
@@ -127,12 +127,8 @@ export function registerRiskCommand(program) {
127
127
  process.exitCode = 1;
128
128
  return;
129
129
  }
130
- if (!(await isRemoteMode())) {
131
- console.error(chalk.yellow('This command requires a remote API connection.'));
132
- console.error(chalk.dim(' Run: trading-boy login or trading-boy subscribe'));
133
- process.exitCode = 1;
130
+ if (!(await ensureRemote()))
134
131
  return;
135
- }
136
132
  try {
137
133
  const apiResult = await apiRequest(`/api/v1/protocols/${encodeURIComponent(protocol.toLowerCase())}/risk`);
138
134
  const result = {
@@ -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-strategy');
8
8
  // ─── Helpers ───
@@ -127,43 +127,6 @@ export function formatHistoryList(response) {
127
127
  lines.push('');
128
128
  return lines.join('\n');
129
129
  }
130
- // ─── Remote Mode Guard ───
131
- async function requireRemote() {
132
- if (!(await isRemoteMode())) {
133
- console.error(chalk.yellow('Strategy commands require a remote API connection.'));
134
- console.error(chalk.dim(' Run: trading-boy login'));
135
- process.exitCode = 1;
136
- return false;
137
- }
138
- return true;
139
- }
140
- // ─── Error Handler ───
141
- function handleApiError(error, context) {
142
- if (error instanceof ApiError) {
143
- switch (error.status) {
144
- case 401:
145
- console.error(chalk.red('Error: API key invalid or expired. Run `trading-boy login` to re-authenticate.'));
146
- break;
147
- case 403:
148
- console.error(chalk.red('Error: Subscription inactive. Run `trading-boy billing manage` to update your billing.'));
149
- break;
150
- case 404:
151
- console.error(chalk.red(`Error: Not found — ${error.message}`));
152
- break;
153
- case 422:
154
- console.error(chalk.red(`Error: Validation failed — ${error.message}`));
155
- break;
156
- default:
157
- console.error(chalk.red(`Error: ${error.message}`));
158
- }
159
- }
160
- else {
161
- const message = error instanceof Error ? error.message : String(error);
162
- logger.error({ error: message }, context);
163
- console.error(chalk.red(`Error: ${message}`));
164
- }
165
- process.exitCode = error instanceof ApiError ? 2 : 1;
166
- }
167
130
  // ─── Command Registration ───
168
131
  export function registerStrategyCommand(program) {
169
132
  const strategy = program
@@ -180,7 +143,7 @@ export function registerStrategyCommand(program) {
180
143
  .option('--setups <types>', 'Comma-separated setup types')
181
144
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
182
145
  .action(async (options) => {
183
- if (!(await requireRemote()))
146
+ if (!(await ensureRemote()))
184
147
  return;
185
148
  const tokens = options.tokens.split(',').map((t) => t.trim().toUpperCase()).filter(Boolean);
186
149
  const setupTypes = options.setups
@@ -220,7 +183,7 @@ export function registerStrategyCommand(program) {
220
183
  }
221
184
  }
222
185
  catch (error) {
223
- handleApiError(error, 'Strategy create failed');
186
+ handleApiError(error, 'Strategy create failed', logger);
224
187
  }
225
188
  });
226
189
  // ─── strategy list ───
@@ -233,7 +196,7 @@ export function registerStrategyCommand(program) {
233
196
  .option('--offset <n>', 'Pagination offset', '0')
234
197
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
235
198
  .action(async (options) => {
236
- if (!(await requireRemote()))
199
+ if (!(await ensureRemote()))
237
200
  return;
238
201
  const params = new URLSearchParams();
239
202
  params.set('traderId', options.traderId);
@@ -251,7 +214,7 @@ export function registerStrategyCommand(program) {
251
214
  }
252
215
  }
253
216
  catch (error) {
254
- handleApiError(error, 'Strategy list failed');
217
+ handleApiError(error, 'Strategy list failed', logger);
255
218
  }
256
219
  });
257
220
  // ─── strategy show <id> ───
@@ -260,7 +223,7 @@ export function registerStrategyCommand(program) {
260
223
  .description('Show full details for a strategy')
261
224
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
262
225
  .action(async (id, options) => {
263
- if (!(await requireRemote()))
226
+ if (!(await ensureRemote()))
264
227
  return;
265
228
  try {
266
229
  const result = await apiRequest(`/api/v1/strategies/${encodeURIComponent(id)}`);
@@ -272,7 +235,7 @@ export function registerStrategyCommand(program) {
272
235
  }
273
236
  }
274
237
  catch (error) {
275
- handleApiError(error, 'Strategy show failed');
238
+ handleApiError(error, 'Strategy show failed', logger);
276
239
  }
277
240
  });
278
241
  // ─── strategy update <id> ───
@@ -284,7 +247,7 @@ export function registerStrategyCommand(program) {
284
247
  .option('--setups <types>', 'New comma-separated setup types (replaces existing)')
285
248
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
286
249
  .action(async (id, options) => {
287
- if (!(await requireRemote()))
250
+ if (!(await ensureRemote()))
288
251
  return;
289
252
  const body = {};
290
253
  if (options.name) {
@@ -311,7 +274,7 @@ export function registerStrategyCommand(program) {
311
274
  }
312
275
  }
313
276
  catch (error) {
314
- handleApiError(error, 'Strategy update failed');
277
+ handleApiError(error, 'Strategy update failed', logger);
315
278
  }
316
279
  });
317
280
  // ─── strategy history <id> ───
@@ -320,7 +283,7 @@ export function registerStrategyCommand(program) {
320
283
  .description('Show version history for a strategy')
321
284
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
322
285
  .action(async (id, options) => {
323
- if (!(await requireRemote()))
286
+ if (!(await ensureRemote()))
324
287
  return;
325
288
  try {
326
289
  const result = await apiRequest(`/api/v1/strategies/${encodeURIComponent(id)}/history`);
@@ -332,7 +295,7 @@ export function registerStrategyCommand(program) {
332
295
  }
333
296
  }
334
297
  catch (error) {
335
- handleApiError(error, 'Strategy history failed');
298
+ handleApiError(error, 'Strategy history failed', logger);
336
299
  }
337
300
  });
338
301
  // ─── strategy export ───
@@ -345,7 +308,7 @@ export function registerStrategyCommand(program) {
345
308
  .default('json'))
346
309
  .option('--output <file>', 'Write output to a file instead of stdout')
347
310
  .action(async (options) => {
348
- if (!(await requireRemote()))
311
+ if (!(await ensureRemote()))
349
312
  return;
350
313
  try {
351
314
  const result = await apiRequest(`/api/v1/strategies/${encodeURIComponent(options.id)}/export?format=${encodeURIComponent(options.format)}`);
@@ -370,7 +333,7 @@ export function registerStrategyCommand(program) {
370
333
  }
371
334
  }
372
335
  catch (error) {
373
- handleApiError(error, 'Strategy export failed');
336
+ handleApiError(error, 'Strategy export failed', logger);
374
337
  }
375
338
  });
376
339
  }
@@ -156,7 +156,7 @@ export async function pollForApiKey(token, onTick) {
156
156
  if (result.status === 'expired' || result.status === 'not_found') {
157
157
  return {
158
158
  success: false,
159
- error: 'Provisioning token expired. If you completed payment, try `trading-boy login` with your API key from the confirmation email.',
159
+ error: 'Provisioning token expired. Your API key will be emailed to you shortly. Then run: `trading-boy login`',
160
160
  };
161
161
  }
162
162
  if (result.status === 'already_retrieved') {
@@ -175,7 +175,7 @@ export async function pollForApiKey(token, onTick) {
175
175
  }
176
176
  return {
177
177
  success: false,
178
- error: 'Timed out waiting for payment confirmation. If you completed payment, try `trading-boy login` with your API key from the confirmation email.',
178
+ error: 'Timed out waiting for payment confirmation. Your API key will be emailed to you shortly. Then run: `trading-boy login`',
179
179
  };
180
180
  }
181
181
  // ─── Credential Storage ───
@@ -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, truncate, formatDate } from '../utils.js';
4
+ import { apiRequest, ApiError } from '../api-client.js';
5
+ import { padRight, truncate, formatDate, ensureRemote } from '../utils.js';
6
6
  // ─── Logger ───
7
7
  const logger = createLogger('cli-suggestions');
8
8
  // ─── Formatters ───
@@ -72,12 +72,8 @@ export function registerSuggestionsCommand(program) {
72
72
  .option('--offset <n>', 'Pagination offset', parseInt, 0)
73
73
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
74
74
  .action(async (options) => {
75
- if (!(await isRemoteMode())) {
76
- console.error(chalk.yellow('This command requires a remote API connection.'));
77
- console.error(chalk.dim(' Run: trading-boy login or trading-boy subscribe'));
78
- process.exitCode = 1;
75
+ if (!(await ensureRemote()))
79
76
  return;
80
- }
81
77
  try {
82
78
  const params = new URLSearchParams();
83
79
  params.set('status', options.status);
@@ -106,12 +102,8 @@ export function registerSuggestionsCommand(program) {
106
102
  .description('Approve a suggestion and auto-apply to strategy')
107
103
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
108
104
  .action(async (id, options) => {
109
- if (!(await isRemoteMode())) {
110
- console.error(chalk.yellow('This command requires a remote API connection.'));
111
- console.error(chalk.dim(' Run: trading-boy login or trading-boy subscribe'));
112
- process.exitCode = 1;
105
+ if (!(await ensureRemote()))
113
106
  return;
114
- }
115
107
  try {
116
108
  const data = await apiRequest(`/api/v1/suggestions/${encodeURIComponent(id)}/approve`, { method: 'POST' });
117
109
  if (options.format === 'json') {
@@ -134,12 +126,8 @@ export function registerSuggestionsCommand(program) {
134
126
  .description('Reject a suggestion')
135
127
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
136
128
  .action(async (id, options) => {
137
- if (!(await isRemoteMode())) {
138
- console.error(chalk.yellow('This command requires a remote API connection.'));
139
- console.error(chalk.dim(' Run: trading-boy login or trading-boy subscribe'));
140
- process.exitCode = 1;
129
+ if (!(await ensureRemote()))
141
130
  return;
142
- }
143
131
  try {
144
132
  const data = await apiRequest(`/api/v1/suggestions/${encodeURIComponent(id)}/reject`, { method: 'POST' });
145
133
  if (options.format === 'json') {
@@ -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-thesis');
7
8
  // ─── BYOK Error Hint ───
@@ -74,12 +75,8 @@ export function registerThesisCommand(program) {
74
75
  .option('--entry-price <n>', 'Entry price', parseFloat)
75
76
  .addOption(new Option('--format <format>', 'Output format').choices(['text', 'json']).default('text'))
76
77
  .action(async (decisionId, options) => {
77
- if (!(await isRemoteMode())) {
78
- console.error(chalk.yellow('Thesis extraction requires a remote API connection.'));
79
- console.error(chalk.dim(' Run: trading-boy login'));
80
- process.exitCode = 1;
78
+ if (!(await ensureRemote()))
81
79
  return;
82
- }
83
80
  // Validate direction if provided
84
81
  if (options.direction && !['LONG', 'SHORT'].includes(options.direction.toUpperCase())) {
85
82
  console.error(chalk.red('Error: --direction must be LONG or SHORT'));
@@ -2,6 +2,7 @@ import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { createLogger } from '@trading-boy/core';
4
4
  import { loadCredentials, redactApiKey } from '../credentials.js';
5
+ import { getApiBase } from '../api-client.js';
5
6
  const logger = createLogger('cli-whoami');
6
7
  // ─── Whoami Logic ───
7
8
  export async function executeWhoami() {
@@ -59,6 +60,7 @@ export function formatWhoamiOutput(result) {
59
60
  const date = new Date(result.storedAt).toLocaleString();
60
61
  lines.push(` ${chalk.gray('Since:')} ${date}`);
61
62
  }
63
+ lines.push(` ${chalk.gray('API:')} ${getApiBase()}`);
62
64
  lines.push('');
63
65
  return lines.join('\n');
64
66
  }
@@ -86,7 +88,7 @@ export function registerWhoamiCommand(program) {
86
88
  }
87
89
  }
88
90
  if (options.format === 'json') {
89
- console.log(JSON.stringify(result, null, 2));
91
+ console.log(JSON.stringify({ ...result, apiUrl: getApiBase() }, null, 2));
90
92
  }
91
93
  else {
92
94
  console.log(formatWhoamiOutput(result));
package/dist/utils.d.ts CHANGED
@@ -48,4 +48,16 @@ export declare function formatDate(isoString: string | null): string;
48
48
  * Parse a string option as an integer.
49
49
  */
50
50
  export declare function parseIntOption(value: string): number;
51
+ /**
52
+ * Centralized API error handler for CLI commands.
53
+ * Prints user-friendly error messages and sets appropriate exit codes.
54
+ */
55
+ export declare function handleApiError(error: unknown, context: string, logger: {
56
+ error: (obj: object, msg: string) => void;
57
+ }): void;
58
+ /**
59
+ * Guard that checks if the CLI is in remote API mode.
60
+ * Returns false (and prints guidance) if not authenticated.
61
+ */
62
+ export declare function ensureRemote(): Promise<boolean>;
51
63
  //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js CHANGED
@@ -115,4 +115,55 @@ export function formatDate(isoString) {
115
115
  export function parseIntOption(value) {
116
116
  return parseInt(value, 10);
117
117
  }
118
+ function isApiError(error) {
119
+ return error instanceof Error && error.name === 'ApiError' && 'status' in error;
120
+ }
121
+ /**
122
+ * Centralized API error handler for CLI commands.
123
+ * Prints user-friendly error messages and sets appropriate exit codes.
124
+ */
125
+ export function handleApiError(error, context, logger) {
126
+ if (isApiError(error)) {
127
+ switch (error.status) {
128
+ case 401:
129
+ console.error(chalk.red('Error: Not authenticated. Run `trading-boy login` or `trading-boy subscribe` to get started.'));
130
+ break;
131
+ case 403:
132
+ console.error(chalk.red(`Error: ${error.message}`));
133
+ break;
134
+ case 404:
135
+ console.error(chalk.red(`Error: Not found — ${error.message}`));
136
+ break;
137
+ case 422:
138
+ console.error(chalk.red(`Error: Validation failed — ${error.message}`));
139
+ break;
140
+ case 429:
141
+ console.error(chalk.red(`Error: Limit reached — ${error.message}`));
142
+ break;
143
+ default:
144
+ console.error(chalk.red(`Error: ${error.message}`));
145
+ }
146
+ }
147
+ else {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ logger.error({ error: message }, context);
150
+ console.error(chalk.red(`Error: ${message}`));
151
+ }
152
+ process.exitCode = isApiError(error) ? 2 : 1;
153
+ }
154
+ // ─── Shared Remote Guard ───
155
+ /**
156
+ * Guard that checks if the CLI is in remote API mode.
157
+ * Returns false (and prints guidance) if not authenticated.
158
+ */
159
+ export async function ensureRemote() {
160
+ const { isRemoteMode } = await import('./api-client.js');
161
+ if (!(await isRemoteMode())) {
162
+ console.error(chalk.yellow('This command requires a remote API connection.'));
163
+ console.error(chalk.dim(' Run: trading-boy login or trading-boy subscribe'));
164
+ process.exitCode = 1;
165
+ return false;
166
+ }
167
+ return true;
168
+ }
118
169
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trading-boy/cli",
3
- "version": "1.6.1",
3
+ "version": "1.8.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": {