@trading-boy/cli 1.12.0 → 2.0.1
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/LICENSE +22 -0
- package/README.md +64 -29
- package/dist/api-client.d.ts +4 -7
- package/dist/api-client.js +8 -13
- package/dist/cli.bundle.js +2314 -33711
- package/dist/credentials.js +1 -1
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -24
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +12 -0
- package/dist/utils.js +3 -3
- package/package.json +30 -16
- package/dist/cli.d.ts +0 -5
- package/dist/cli.js +0 -157
- package/dist/commands/agent-cmd.d.ts +0 -9
- package/dist/commands/agent-cmd.js +0 -567
- package/dist/commands/audit.d.ts +0 -18
- package/dist/commands/audit.js +0 -73
- package/dist/commands/behavioral.d.ts +0 -73
- package/dist/commands/behavioral.js +0 -349
- package/dist/commands/benchmark-cmd.d.ts +0 -3
- package/dist/commands/benchmark-cmd.js +0 -191
- package/dist/commands/billing.d.ts +0 -12
- package/dist/commands/billing.js +0 -142
- package/dist/commands/catalysts.d.ts +0 -17
- package/dist/commands/catalysts.js +0 -151
- package/dist/commands/coaching-cmd.d.ts +0 -16
- package/dist/commands/coaching-cmd.js +0 -222
- package/dist/commands/config-cmd.d.ts +0 -30
- package/dist/commands/config-cmd.js +0 -515
- package/dist/commands/connect-chatgpt.d.ts +0 -5
- package/dist/commands/connect-chatgpt.js +0 -293
- package/dist/commands/connect-claude.d.ts +0 -5
- package/dist/commands/connect-claude.js +0 -280
- package/dist/commands/context.d.ts +0 -41
- package/dist/commands/context.js +0 -405
- package/dist/commands/cron-cmd.d.ts +0 -3
- package/dist/commands/cron-cmd.js +0 -305
- package/dist/commands/decisions.d.ts +0 -57
- package/dist/commands/decisions.js +0 -364
- package/dist/commands/edge-cmd.d.ts +0 -78
- package/dist/commands/edge-cmd.js +0 -183
- package/dist/commands/edge-guard-cmd.d.ts +0 -36
- package/dist/commands/edge-guard-cmd.js +0 -169
- package/dist/commands/events.d.ts +0 -3
- package/dist/commands/events.js +0 -117
- package/dist/commands/infra.d.ts +0 -24
- package/dist/commands/infra.js +0 -137
- package/dist/commands/journal.d.ts +0 -3
- package/dist/commands/journal.js +0 -302
- package/dist/commands/login.d.ts +0 -18
- package/dist/commands/login.js +0 -127
- package/dist/commands/logout.d.ts +0 -8
- package/dist/commands/logout.js +0 -108
- package/dist/commands/narratives.d.ts +0 -3
- package/dist/commands/narratives.js +0 -259
- package/dist/commands/onboarding.d.ts +0 -7
- package/dist/commands/onboarding.js +0 -281
- package/dist/commands/query.d.ts +0 -32
- package/dist/commands/query.js +0 -135
- package/dist/commands/replay-cmd.d.ts +0 -43
- package/dist/commands/replay-cmd.js +0 -184
- package/dist/commands/review.d.ts +0 -3
- package/dist/commands/review.js +0 -443
- package/dist/commands/risk.d.ts +0 -47
- package/dist/commands/risk.js +0 -158
- package/dist/commands/social.d.ts +0 -43
- package/dist/commands/social.js +0 -318
- package/dist/commands/soul-wizard.d.ts +0 -29
- package/dist/commands/soul-wizard.js +0 -155
- package/dist/commands/strategy-cmd.d.ts +0 -44
- package/dist/commands/strategy-cmd.js +0 -335
- package/dist/commands/subscribe.d.ts +0 -78
- package/dist/commands/subscribe.js +0 -552
- package/dist/commands/suggestions-cmd.d.ts +0 -24
- package/dist/commands/suggestions-cmd.js +0 -148
- package/dist/commands/thesis-cmd.d.ts +0 -3
- package/dist/commands/thesis-cmd.js +0 -129
- package/dist/commands/trader.d.ts +0 -30
- package/dist/commands/trader.js +0 -971
- package/dist/commands/watch.d.ts +0 -16
- package/dist/commands/watch.js +0 -104
- package/dist/commands/whoami.d.ts +0 -14
- package/dist/commands/whoami.js +0 -105
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
// ─── Connect ChatGPT Command ───
|
|
2
|
-
//
|
|
3
|
-
// Connects a user's ChatGPT subscription via Codex OAuth (PKCE).
|
|
4
|
-
//
|
|
5
|
-
// Two completion paths run in parallel (whichever wins first):
|
|
6
|
-
// A. Local callback: localhost:1455/auth/callback catches code → POST to API
|
|
7
|
-
// B. Polling: GET /api/v1/llm-config until codexConnected: true
|
|
8
|
-
// (handles production redirect where API server catches the callback directly)
|
|
9
|
-
//
|
|
10
|
-
// Also supports disconnect via --disconnect flag.
|
|
11
|
-
import http from 'node:http';
|
|
12
|
-
import { URL } from 'node:url';
|
|
13
|
-
import chalk from 'chalk';
|
|
14
|
-
import { createLogger } from '@trading-boy/core';
|
|
15
|
-
import { apiRequest, ApiError, isRemoteMode } from '../api-client.js';
|
|
16
|
-
import { formatConnectionError } from '../utils.js';
|
|
17
|
-
// ─── Logger ───
|
|
18
|
-
const logger = createLogger('cli-connect-chatgpt');
|
|
19
|
-
// ─── Constants ───
|
|
20
|
-
const CALLBACK_PORT = 1455;
|
|
21
|
-
const CALLBACK_PATH = '/auth/callback';
|
|
22
|
-
const AUTH_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
|
|
23
|
-
const POLL_INTERVAL_MS = 2_000;
|
|
24
|
-
// ─── Command ───
|
|
25
|
-
export function registerConnectChatgptCommand(program) {
|
|
26
|
-
program
|
|
27
|
-
.command('connect-chatgpt')
|
|
28
|
-
.description('Connect your ChatGPT subscription as your LLM provider (no API key needed)')
|
|
29
|
-
.option('--disconnect', 'Disconnect your ChatGPT account')
|
|
30
|
-
.action(async (options) => {
|
|
31
|
-
try {
|
|
32
|
-
if (!(await isRemoteMode())) {
|
|
33
|
-
console.error(chalk.yellow(' Requires API connection. Run: trading-boy login'));
|
|
34
|
-
process.exitCode = 1;
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (options.disconnect) {
|
|
38
|
-
await handleDisconnect();
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
await handleConnect();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
46
|
-
logger.error({ error: msg }, 'connect-chatgpt failed');
|
|
47
|
-
const connHelp = formatConnectionError(msg);
|
|
48
|
-
if (connHelp) {
|
|
49
|
-
console.error(chalk.red(`\n Connection failed.\n`));
|
|
50
|
-
console.error(connHelp);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
console.error(chalk.red(`\n Error: ${msg}`));
|
|
54
|
-
}
|
|
55
|
-
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
// ─── Connect Flow ───
|
|
60
|
-
async function handleConnect() {
|
|
61
|
-
// Check if already connected
|
|
62
|
-
try {
|
|
63
|
-
const current = await apiRequest('/api/v1/llm-config');
|
|
64
|
-
if (current.codexConnected) {
|
|
65
|
-
console.log('');
|
|
66
|
-
console.log(chalk.green(' ✓ ChatGPT is already connected'));
|
|
67
|
-
console.log(chalk.dim(` Account: ${current.codexAccountId ?? 'unknown'}`));
|
|
68
|
-
console.log(chalk.dim(` Provider: codex / ${current.model}`));
|
|
69
|
-
console.log('');
|
|
70
|
-
console.log(chalk.dim(' To disconnect: trading-boy connect-chatgpt --disconnect'));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// No config yet — that's fine, proceed with connect
|
|
76
|
-
}
|
|
77
|
-
console.log('');
|
|
78
|
-
console.log(chalk.bold.cyan(' Connect ChatGPT'));
|
|
79
|
-
console.log(chalk.gray(' ' + '\u2500'.repeat(50)));
|
|
80
|
-
console.log(chalk.dim(' Use your ChatGPT subscription to power your trading agents.'));
|
|
81
|
-
console.log(chalk.dim(' No API key needed — authenticates via your OpenAI account.'));
|
|
82
|
-
console.log('');
|
|
83
|
-
// Step 1: Get auth URL from API (generates PKCE + stores verifier in Redis)
|
|
84
|
-
const { authUrl, state } = await apiRequest('/api/v1/codex/auth-url');
|
|
85
|
-
// Step 2: Race local callback server vs polling
|
|
86
|
-
// - Local callback wins when redirect_uri points to localhost (dev or matching Codex client)
|
|
87
|
-
// - Polling wins when redirect_uri points to production API server
|
|
88
|
-
const result = await raceForAuth(authUrl, state);
|
|
89
|
-
// Step 3: Handle result
|
|
90
|
-
if (result.via === 'local') {
|
|
91
|
-
// Local server caught the code — forward to API for token exchange
|
|
92
|
-
console.log(chalk.dim(' Exchanging authorization code...'));
|
|
93
|
-
const exchangeResult = await apiRequest('/api/v1/codex/callback', {
|
|
94
|
-
method: 'POST',
|
|
95
|
-
body: { code: result.code, state: result.state },
|
|
96
|
-
});
|
|
97
|
-
printSuccess(exchangeResult.accountId, null);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
// Polling detected connection — API server handled the callback directly
|
|
101
|
-
printSuccess(result.config.codexAccountId, result.config.model);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
function printSuccess(accountId, model) {
|
|
105
|
-
console.log('');
|
|
106
|
-
console.log(chalk.green(' ✓ ChatGPT connected!'));
|
|
107
|
-
console.log('');
|
|
108
|
-
if (accountId)
|
|
109
|
-
console.log(` ${chalk.gray('Account:')} ${accountId}`);
|
|
110
|
-
console.log(` ${chalk.gray('Provider:')} codex`);
|
|
111
|
-
if (model)
|
|
112
|
-
console.log(` ${chalk.gray('Model:')} ${model}`);
|
|
113
|
-
console.log('');
|
|
114
|
-
console.log(chalk.dim(' Your agents will now use your ChatGPT subscription for LLM calls.'));
|
|
115
|
-
console.log(chalk.dim(' Change model: trading-boy config set-llm-key --provider codex --model <model>'));
|
|
116
|
-
}
|
|
117
|
-
// ─── Race: Local Callback vs Polling ───
|
|
118
|
-
async function raceForAuth(authUrl, state) {
|
|
119
|
-
const controller = new AbortController();
|
|
120
|
-
const localCallback = startLocalCallbackServer(state, controller.signal);
|
|
121
|
-
const polling = pollForConnection(controller.signal);
|
|
122
|
-
// Open browser after server is ready (small delay to ensure listen completes)
|
|
123
|
-
console.log(chalk.white(' Opening OpenAI login in your browser...'));
|
|
124
|
-
await openBrowser(authUrl);
|
|
125
|
-
console.log(chalk.dim(' Complete sign-in in your browser to continue.'));
|
|
126
|
-
console.log(chalk.dim(' Waiting for authentication...'));
|
|
127
|
-
console.log('');
|
|
128
|
-
try {
|
|
129
|
-
const result = await Promise.race([localCallback, polling]);
|
|
130
|
-
controller.abort(); // cancel the loser
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
controller.abort();
|
|
135
|
-
throw err;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function startLocalCallbackServer(state, signal) {
|
|
139
|
-
return new Promise((resolve, reject) => {
|
|
140
|
-
if (signal.aborted) {
|
|
141
|
-
reject(new Error('Cancelled'));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const timeout = setTimeout(() => {
|
|
145
|
-
server.close();
|
|
146
|
-
reject(new Error('Timed out waiting for OpenAI authentication (3 minutes)'));
|
|
147
|
-
}, AUTH_TIMEOUT_MS);
|
|
148
|
-
signal.addEventListener('abort', () => {
|
|
149
|
-
clearTimeout(timeout);
|
|
150
|
-
server.close();
|
|
151
|
-
}, { once: true });
|
|
152
|
-
const server = http.createServer((req, res) => {
|
|
153
|
-
const url = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`);
|
|
154
|
-
if (url.pathname !== CALLBACK_PATH) {
|
|
155
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
156
|
-
res.end('Not found');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const errorParam = url.searchParams.get('error');
|
|
160
|
-
const errorDesc = url.searchParams.get('error_description');
|
|
161
|
-
const codeParam = url.searchParams.get('code');
|
|
162
|
-
if (errorParam) {
|
|
163
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
164
|
-
res.end(callbackHtml(false, errorDesc || errorParam));
|
|
165
|
-
clearTimeout(timeout);
|
|
166
|
-
server.close();
|
|
167
|
-
reject(new Error(`OpenAI returned error: ${errorDesc || errorParam}`));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (!codeParam) {
|
|
171
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
172
|
-
res.end(callbackHtml(false, 'Missing authorization code'));
|
|
173
|
-
clearTimeout(timeout);
|
|
174
|
-
server.close();
|
|
175
|
-
reject(new Error('No authorization code in callback'));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
// Success — send HTML to browser, resolve
|
|
179
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
180
|
-
res.end(callbackHtml(true));
|
|
181
|
-
clearTimeout(timeout);
|
|
182
|
-
server.close();
|
|
183
|
-
resolve({ via: 'local', code: codeParam, state });
|
|
184
|
-
});
|
|
185
|
-
server.on('error', (err) => {
|
|
186
|
-
clearTimeout(timeout);
|
|
187
|
-
if (err.code === 'EADDRINUSE') {
|
|
188
|
-
// Port in use — don't fail hard, let polling take over
|
|
189
|
-
logger.debug('Port %d in use, falling back to polling only', CALLBACK_PORT);
|
|
190
|
-
// Return a never-resolving promise (polling will win)
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
reject(err);
|
|
194
|
-
});
|
|
195
|
-
server.listen(CALLBACK_PORT, '127.0.0.1');
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
async function pollForConnection(signal) {
|
|
199
|
-
const deadline = Date.now() + AUTH_TIMEOUT_MS;
|
|
200
|
-
while (Date.now() < deadline) {
|
|
201
|
-
if (signal.aborted)
|
|
202
|
-
throw new Error('Cancelled');
|
|
203
|
-
await sleep(POLL_INTERVAL_MS);
|
|
204
|
-
if (signal.aborted)
|
|
205
|
-
throw new Error('Cancelled');
|
|
206
|
-
try {
|
|
207
|
-
const config = await apiRequest('/api/v1/llm-config');
|
|
208
|
-
if (config.codexConnected) {
|
|
209
|
-
return { via: 'poll', config };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// Poll failures are expected — API might briefly be unavailable
|
|
214
|
-
logger.debug('Poll attempt failed, retrying...');
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
throw new Error('Timed out waiting for OpenAI authentication (3 minutes)');
|
|
218
|
-
}
|
|
219
|
-
// ─── Disconnect Flow ───
|
|
220
|
-
async function handleDisconnect() {
|
|
221
|
-
const { confirm } = await import('@inquirer/prompts');
|
|
222
|
-
const yes = await confirm({
|
|
223
|
-
message: 'Disconnect your ChatGPT account?',
|
|
224
|
-
default: false,
|
|
225
|
-
});
|
|
226
|
-
if (!yes) {
|
|
227
|
-
console.log(chalk.dim(' Cancelled.'));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
await apiRequest('/api/v1/codex/disconnect', {
|
|
231
|
-
method: 'POST',
|
|
232
|
-
});
|
|
233
|
-
console.log(chalk.green(' ✓ ChatGPT disconnected.'));
|
|
234
|
-
console.log(chalk.dim(' Your agents will need an API key to continue. Set one:'));
|
|
235
|
-
console.log(chalk.dim(' trading-boy config set-llm-key <your-api-key>'));
|
|
236
|
-
}
|
|
237
|
-
// ─── HTML for Browser Callback ───
|
|
238
|
-
function callbackHtml(success, errorMsg) {
|
|
239
|
-
const title = success ? 'ChatGPT Connected' : 'Connection Failed';
|
|
240
|
-
const icon = success ? '✓' : '✗';
|
|
241
|
-
const color = success ? '#22c55e' : '#ef4444';
|
|
242
|
-
const message = success
|
|
243
|
-
? 'Your ChatGPT subscription is now connected to Trading Boy.<br>You can close this tab.'
|
|
244
|
-
: `Something went wrong: ${escapeHtml(errorMsg ?? 'Unknown error')}.<br>Please try again.`;
|
|
245
|
-
return `<!DOCTYPE html>
|
|
246
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
247
|
-
<title>${title} — Trading Boy</title>
|
|
248
|
-
<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0a0a0a;color:#e5e5e5}
|
|
249
|
-
.card{text-align:center;padding:3rem;max-width:480px}.icon{font-size:4rem;color:${color};margin-bottom:1rem}h1{font-size:1.5rem;margin-bottom:1rem}p{color:#a3a3a3;line-height:1.6}</style>
|
|
250
|
-
</head><body><div class="card"><div class="icon">${icon}</div><h1>${title}</h1><p>${message}</p></div></body></html>`;
|
|
251
|
-
}
|
|
252
|
-
function escapeHtml(str) {
|
|
253
|
-
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
254
|
-
}
|
|
255
|
-
// ─── Helpers ───
|
|
256
|
-
function sleep(ms) {
|
|
257
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
258
|
-
}
|
|
259
|
-
/** Allowed domains for the OAuth browser redirect. */
|
|
260
|
-
const ALLOWED_AUTH_DOMAINS = new Set([
|
|
261
|
-
'auth.openai.com',
|
|
262
|
-
'auth0.openai.com',
|
|
263
|
-
'chat.openai.com',
|
|
264
|
-
]);
|
|
265
|
-
function validateAuthUrl(url) {
|
|
266
|
-
const parsed = new URL(url);
|
|
267
|
-
if (parsed.protocol !== 'https:') {
|
|
268
|
-
throw new Error(`Refusing to open non-HTTPS URL: ${url}`);
|
|
269
|
-
}
|
|
270
|
-
if (!ALLOWED_AUTH_DOMAINS.has(parsed.hostname)) {
|
|
271
|
-
throw new Error(`Refusing to open URL with untrusted domain: ${parsed.hostname}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async function openBrowser(url) {
|
|
275
|
-
try {
|
|
276
|
-
validateAuthUrl(url);
|
|
277
|
-
const { default: open } = await import('open');
|
|
278
|
-
await open(url);
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
282
|
-
if (message.startsWith('Refusing to open')) {
|
|
283
|
-
console.error(chalk.red(`\n ${message}`));
|
|
284
|
-
throw err;
|
|
285
|
-
}
|
|
286
|
-
console.log(chalk.yellow(`\n Could not open browser automatically.`));
|
|
287
|
-
console.log(chalk.yellow(` Please open this URL manually:\n`));
|
|
288
|
-
console.log(` ${chalk.cyan.underline(url)}\n`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// ─── Exported for onboarding ───
|
|
292
|
-
export { handleConnect as connectChatgpt };
|
|
293
|
-
//# sourceMappingURL=connect-chatgpt.js.map
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
// ─── Connect Claude Command ───
|
|
2
|
-
//
|
|
3
|
-
// Connects a user's Claude subscription via Anthropic OAuth (PKCE).
|
|
4
|
-
//
|
|
5
|
-
// Two completion paths run in parallel (whichever wins first):
|
|
6
|
-
// A. Local callback: localhost:1456/auth/callback catches code → POST to API
|
|
7
|
-
// B. Polling: GET /api/v1/llm-config until anthropicOAuthConnected: true
|
|
8
|
-
// (handles production redirect where API server catches the callback directly)
|
|
9
|
-
//
|
|
10
|
-
// Also supports disconnect via --disconnect flag.
|
|
11
|
-
import http from 'node:http';
|
|
12
|
-
import { URL } from 'node:url';
|
|
13
|
-
import chalk from 'chalk';
|
|
14
|
-
import { createLogger } from '@trading-boy/core';
|
|
15
|
-
import { apiRequest, ApiError, isRemoteMode } from '../api-client.js';
|
|
16
|
-
import { formatConnectionError } from '../utils.js';
|
|
17
|
-
// ─── Logger ───
|
|
18
|
-
const logger = createLogger('cli-connect-claude');
|
|
19
|
-
// ─── Constants ───
|
|
20
|
-
const CALLBACK_PORT = 1456;
|
|
21
|
-
const CALLBACK_PATH = '/callback';
|
|
22
|
-
const AUTH_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
|
|
23
|
-
const POLL_INTERVAL_MS = 2_000;
|
|
24
|
-
// ─── Command ───
|
|
25
|
-
export function registerConnectClaudeCommand(program) {
|
|
26
|
-
program
|
|
27
|
-
.command('connect-claude')
|
|
28
|
-
.description('Connect your Claude subscription as your LLM provider (no API key needed)')
|
|
29
|
-
.option('--disconnect', 'Disconnect your Claude account')
|
|
30
|
-
.action(async (options) => {
|
|
31
|
-
try {
|
|
32
|
-
if (!(await isRemoteMode())) {
|
|
33
|
-
console.error(chalk.yellow(' Requires API connection. Run: trading-boy login'));
|
|
34
|
-
process.exitCode = 1;
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (options.disconnect) {
|
|
38
|
-
await handleDisconnect();
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
await handleConnect();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
46
|
-
logger.error({ error: msg }, 'connect-claude failed');
|
|
47
|
-
const connHelp = formatConnectionError(msg);
|
|
48
|
-
if (connHelp) {
|
|
49
|
-
console.error(chalk.red(`\n Connection failed.\n`));
|
|
50
|
-
console.error(connHelp);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
console.error(chalk.red(`\n Error: ${msg}`));
|
|
54
|
-
}
|
|
55
|
-
process.exitCode = error instanceof ApiError ? 2 : 1;
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
// ─── Connect Flow ───
|
|
60
|
-
async function handleConnect() {
|
|
61
|
-
// Check if already connected
|
|
62
|
-
try {
|
|
63
|
-
const current = await apiRequest('/api/v1/llm-config');
|
|
64
|
-
if (current.anthropicOAuthConnected) {
|
|
65
|
-
console.log('');
|
|
66
|
-
console.log(chalk.green(' ✓ Claude is already connected'));
|
|
67
|
-
console.log(chalk.dim(` Provider: anthropic / ${current.model}`));
|
|
68
|
-
console.log('');
|
|
69
|
-
console.log(chalk.dim(' To disconnect: trading-boy connect-claude --disconnect'));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// No config yet — that's fine, proceed with connect
|
|
75
|
-
}
|
|
76
|
-
console.log('');
|
|
77
|
-
console.log(chalk.bold.cyan(' Connect Claude'));
|
|
78
|
-
console.log(chalk.gray(' ' + '\u2500'.repeat(50)));
|
|
79
|
-
console.log(chalk.dim(' Use your Claude subscription to power your trading agents.'));
|
|
80
|
-
console.log(chalk.dim(' No API key needed — authenticates via your Anthropic account.'));
|
|
81
|
-
console.log('');
|
|
82
|
-
// Step 1: Get auth URL from API (generates PKCE + stores verifier in Redis)
|
|
83
|
-
const { authUrl, state } = await apiRequest('/api/v1/anthropic-oauth/auth-url');
|
|
84
|
-
// Step 2: Race local callback server vs polling
|
|
85
|
-
const result = await raceForAuth(authUrl, state);
|
|
86
|
-
// Step 3: Handle result
|
|
87
|
-
if (result.via === 'local') {
|
|
88
|
-
console.log(chalk.dim(' Exchanging authorization code...'));
|
|
89
|
-
await apiRequest('/api/v1/anthropic-oauth/callback', {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
body: { code: result.code, state: result.state },
|
|
92
|
-
});
|
|
93
|
-
printSuccess(null);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
printSuccess(result.config.model);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function printSuccess(model) {
|
|
100
|
-
console.log('');
|
|
101
|
-
console.log(chalk.green(' ✓ Claude connected!'));
|
|
102
|
-
console.log('');
|
|
103
|
-
console.log(` ${chalk.gray('Provider:')} anthropic`);
|
|
104
|
-
if (model)
|
|
105
|
-
console.log(` ${chalk.gray('Model:')} ${model}`);
|
|
106
|
-
console.log('');
|
|
107
|
-
console.log(chalk.dim(' Your agents will now use your Claude subscription for LLM calls.'));
|
|
108
|
-
console.log(chalk.dim(' Change model: trading-boy config set-llm-key --provider anthropic --model <model>'));
|
|
109
|
-
}
|
|
110
|
-
// ─── Race: Local Callback vs Polling ───
|
|
111
|
-
async function raceForAuth(authUrl, state) {
|
|
112
|
-
const controller = new AbortController();
|
|
113
|
-
const localCallback = startLocalCallbackServer(state, controller.signal);
|
|
114
|
-
const polling = pollForConnection(controller.signal);
|
|
115
|
-
console.log(chalk.white(' Opening Claude login in your browser...'));
|
|
116
|
-
await openBrowser(authUrl);
|
|
117
|
-
console.log(chalk.dim(' Complete sign-in in your browser to continue.'));
|
|
118
|
-
console.log(chalk.dim(' Waiting for authentication...'));
|
|
119
|
-
console.log('');
|
|
120
|
-
try {
|
|
121
|
-
const result = await Promise.race([localCallback, polling]);
|
|
122
|
-
controller.abort();
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
controller.abort();
|
|
127
|
-
throw err;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function startLocalCallbackServer(state, signal) {
|
|
131
|
-
return new Promise((resolve, reject) => {
|
|
132
|
-
if (signal.aborted) {
|
|
133
|
-
reject(new Error('Cancelled'));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const timeout = setTimeout(() => {
|
|
137
|
-
server.close();
|
|
138
|
-
reject(new Error('Timed out waiting for Claude authentication (3 minutes)'));
|
|
139
|
-
}, AUTH_TIMEOUT_MS);
|
|
140
|
-
signal.addEventListener('abort', () => {
|
|
141
|
-
clearTimeout(timeout);
|
|
142
|
-
server.close();
|
|
143
|
-
}, { once: true });
|
|
144
|
-
const server = http.createServer((req, res) => {
|
|
145
|
-
const url = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`);
|
|
146
|
-
if (url.pathname !== CALLBACK_PATH) {
|
|
147
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
148
|
-
res.end('Not found');
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const errorParam = url.searchParams.get('error');
|
|
152
|
-
const errorDesc = url.searchParams.get('error_description');
|
|
153
|
-
const codeParam = url.searchParams.get('code');
|
|
154
|
-
if (errorParam) {
|
|
155
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
156
|
-
res.end(callbackHtml(false, errorDesc || errorParam));
|
|
157
|
-
clearTimeout(timeout);
|
|
158
|
-
server.close();
|
|
159
|
-
reject(new Error(`Anthropic returned error: ${errorDesc || errorParam}`));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (!codeParam) {
|
|
163
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
164
|
-
res.end(callbackHtml(false, 'Missing authorization code'));
|
|
165
|
-
clearTimeout(timeout);
|
|
166
|
-
server.close();
|
|
167
|
-
reject(new Error('No authorization code in callback'));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
171
|
-
res.end(callbackHtml(true));
|
|
172
|
-
clearTimeout(timeout);
|
|
173
|
-
server.close();
|
|
174
|
-
resolve({ via: 'local', code: codeParam, state });
|
|
175
|
-
});
|
|
176
|
-
server.on('error', (err) => {
|
|
177
|
-
clearTimeout(timeout);
|
|
178
|
-
if (err.code === 'EADDRINUSE') {
|
|
179
|
-
logger.debug('Port %d in use, falling back to polling only', CALLBACK_PORT);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
reject(err);
|
|
183
|
-
});
|
|
184
|
-
server.listen(CALLBACK_PORT, '127.0.0.1');
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
async function pollForConnection(signal) {
|
|
188
|
-
const deadline = Date.now() + AUTH_TIMEOUT_MS;
|
|
189
|
-
while (Date.now() < deadline) {
|
|
190
|
-
if (signal.aborted)
|
|
191
|
-
throw new Error('Cancelled');
|
|
192
|
-
await sleep(POLL_INTERVAL_MS);
|
|
193
|
-
if (signal.aborted)
|
|
194
|
-
throw new Error('Cancelled');
|
|
195
|
-
try {
|
|
196
|
-
const config = await apiRequest('/api/v1/llm-config');
|
|
197
|
-
if (config.anthropicOAuthConnected) {
|
|
198
|
-
return { via: 'poll', config };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
logger.debug('Poll attempt failed, retrying...');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
throw new Error('Timed out waiting for Claude authentication (3 minutes)');
|
|
206
|
-
}
|
|
207
|
-
// ─── Disconnect Flow ───
|
|
208
|
-
async function handleDisconnect() {
|
|
209
|
-
const { confirm } = await import('@inquirer/prompts');
|
|
210
|
-
const yes = await confirm({
|
|
211
|
-
message: 'Disconnect your Claude account?',
|
|
212
|
-
default: false,
|
|
213
|
-
});
|
|
214
|
-
if (!yes) {
|
|
215
|
-
console.log(chalk.dim(' Cancelled.'));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
await apiRequest('/api/v1/anthropic-oauth/disconnect', {
|
|
219
|
-
method: 'POST',
|
|
220
|
-
});
|
|
221
|
-
console.log(chalk.green(' ✓ Claude disconnected.'));
|
|
222
|
-
console.log(chalk.dim(' Your agents will need an API key to continue. Set one:'));
|
|
223
|
-
console.log(chalk.dim(' trading-boy config set-llm-key <your-api-key>'));
|
|
224
|
-
}
|
|
225
|
-
// ─── HTML for Browser Callback ───
|
|
226
|
-
function callbackHtml(success, errorMsg) {
|
|
227
|
-
const title = success ? 'Claude Connected' : 'Connection Failed';
|
|
228
|
-
const icon = success ? '✓' : '✗';
|
|
229
|
-
const color = success ? '#22c55e' : '#ef4444';
|
|
230
|
-
const message = success
|
|
231
|
-
? 'Your Claude subscription is now connected to Trading Boy.<br>You can close this tab.'
|
|
232
|
-
: `Something went wrong: ${escapeHtml(errorMsg ?? 'Unknown error')}.<br>Please try again.`;
|
|
233
|
-
return `<!DOCTYPE html>
|
|
234
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
235
|
-
<title>${title} — Trading Boy</title>
|
|
236
|
-
<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0a0a0a;color:#e5e5e5}
|
|
237
|
-
.card{text-align:center;padding:3rem;max-width:480px}.icon{font-size:4rem;color:${color};margin-bottom:1rem}h1{font-size:1.5rem;margin-bottom:1rem}p{color:#a3a3a3;line-height:1.6}</style>
|
|
238
|
-
</head><body><div class="card"><div class="icon">${icon}</div><h1>${title}</h1><p>${message}</p></div></body></html>`;
|
|
239
|
-
}
|
|
240
|
-
function escapeHtml(str) {
|
|
241
|
-
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
242
|
-
}
|
|
243
|
-
// ─── Helpers ───
|
|
244
|
-
function sleep(ms) {
|
|
245
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
-
}
|
|
247
|
-
const ALLOWED_AUTH_DOMAINS = new Set([
|
|
248
|
-
'platform.claude.com',
|
|
249
|
-
'claude.ai',
|
|
250
|
-
'claude.com',
|
|
251
|
-
]);
|
|
252
|
-
function validateAuthUrl(url) {
|
|
253
|
-
const parsed = new URL(url);
|
|
254
|
-
if (parsed.protocol !== 'https:') {
|
|
255
|
-
throw new Error(`Refusing to open non-HTTPS URL: ${url}`);
|
|
256
|
-
}
|
|
257
|
-
if (!ALLOWED_AUTH_DOMAINS.has(parsed.hostname)) {
|
|
258
|
-
throw new Error(`Refusing to open URL with untrusted domain: ${parsed.hostname}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async function openBrowser(url) {
|
|
262
|
-
try {
|
|
263
|
-
validateAuthUrl(url);
|
|
264
|
-
const { default: open } = await import('open');
|
|
265
|
-
await open(url);
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
269
|
-
if (message.startsWith('Refusing to open')) {
|
|
270
|
-
console.error(chalk.red(`\n ${message}`));
|
|
271
|
-
throw err;
|
|
272
|
-
}
|
|
273
|
-
console.log(chalk.yellow(`\n Could not open browser automatically.`));
|
|
274
|
-
console.log(chalk.yellow(` Please open this URL manually:\n`));
|
|
275
|
-
console.log(` ${chalk.cyan.underline(url)}\n`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// ─── Exported for onboarding ───
|
|
279
|
-
export { handleConnect as connectClaude };
|
|
280
|
-
//# sourceMappingURL=connect-claude.js.map
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import type { AssembledContextPackage, MarketData, OnchainData, DefiRiskProfile, Catalysts, SocialContext, Synthesis, ContextMeta } from '@trading-boy/core';
|
|
3
|
-
/**
|
|
4
|
-
* Format the market section of a ContextPackage.
|
|
5
|
-
*/
|
|
6
|
-
export declare function formatMarketSection(market: MarketData | null): string;
|
|
7
|
-
/**
|
|
8
|
-
* Format the onchain section of a ContextPackage.
|
|
9
|
-
*/
|
|
10
|
-
export declare function formatOnchainSection(onchain: OnchainData | null): string;
|
|
11
|
-
/**
|
|
12
|
-
* Format the DeFi risk section of a ContextPackage.
|
|
13
|
-
*/
|
|
14
|
-
export declare function formatRiskSection(defiRisk: DefiRiskProfile | null): string;
|
|
15
|
-
/**
|
|
16
|
-
* Format the catalysts section of a ContextPackage.
|
|
17
|
-
*/
|
|
18
|
-
export declare function formatCatalystsSection(catalysts: Catalysts | null): string;
|
|
19
|
-
/**
|
|
20
|
-
* Format the social section of a ContextPackage.
|
|
21
|
-
*/
|
|
22
|
-
export declare function formatSocialSection(social: SocialContext | null): string;
|
|
23
|
-
/**
|
|
24
|
-
* Format the synthesis section of a ContextPackage.
|
|
25
|
-
*/
|
|
26
|
-
export declare function formatSynthesisSection(synthesis: Synthesis | null): string;
|
|
27
|
-
/**
|
|
28
|
-
* Format the _meta section of a ContextPackage.
|
|
29
|
-
*/
|
|
30
|
-
export declare function formatMetaSection(meta: ContextMeta): string;
|
|
31
|
-
/**
|
|
32
|
-
* Format a full AssembledContextPackage for colored terminal output.
|
|
33
|
-
*/
|
|
34
|
-
export declare function formatContextOutput(pkg: AssembledContextPackage): string;
|
|
35
|
-
export declare function formatSnapshotRange(snapshots: Array<{
|
|
36
|
-
time: string;
|
|
37
|
-
regime: string | null;
|
|
38
|
-
context: AssembledContextPackage;
|
|
39
|
-
}>, symbol: string): string;
|
|
40
|
-
export declare function registerContextCommand(program: Command): void;
|
|
41
|
-
//# sourceMappingURL=context.d.ts.map
|