@trading-boy/cli 1.10.0 → 1.12.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.
@@ -35650,6 +35650,13 @@ var init_codex_tokens = __esm({
35650
35650
  }
35651
35651
  });
35652
35652
 
35653
+ // ../core/dist/llm/anthropic-tokens.js
35654
+ var init_anthropic_tokens = __esm({
35655
+ "../core/dist/llm/anthropic-tokens.js"() {
35656
+ "use strict";
35657
+ }
35658
+ });
35659
+
35653
35660
  // ../core/dist/llm/index.js
35654
35661
  var init_llm = __esm({
35655
35662
  "../core/dist/llm/index.js"() {
@@ -35658,6 +35665,42 @@ var init_llm = __esm({
35658
35665
  init_structured_output();
35659
35666
  init_config2();
35660
35667
  init_codex_tokens();
35668
+ init_anthropic_tokens();
35669
+ }
35670
+ });
35671
+
35672
+ // ../core/dist/strategy-utils.js
35673
+ function generateRegimeBehavior(setupTypes = ["BREAKOUT", "MOMENTUM", "MEAN-REVERSION", "CATALYST"]) {
35674
+ return {
35675
+ MARKUP: {
35676
+ enabled: true,
35677
+ maxPositionSize: 0.5,
35678
+ confidenceThreshold: 0,
35679
+ preferredSetups: setupTypes
35680
+ },
35681
+ ACCUMULATION: {
35682
+ enabled: true,
35683
+ maxPositionSize: 0.25,
35684
+ confidenceThreshold: 0,
35685
+ preferredSetups: setupTypes
35686
+ },
35687
+ DISTRIBUTION: {
35688
+ enabled: true,
35689
+ maxPositionSize: 0.15,
35690
+ confidenceThreshold: 0,
35691
+ preferredSetups: setupTypes
35692
+ },
35693
+ MARKDOWN: {
35694
+ enabled: true,
35695
+ maxPositionSize: 0.1,
35696
+ confidenceThreshold: 0,
35697
+ preferredSetups: setupTypes
35698
+ }
35699
+ };
35700
+ }
35701
+ var init_strategy_utils = __esm({
35702
+ "../core/dist/strategy-utils.js"() {
35703
+ "use strict";
35661
35704
  }
35662
35705
  });
35663
35706
 
@@ -35680,6 +35723,7 @@ var init_dist2 = __esm({
35680
35723
  init_errors3();
35681
35724
  init_crypto();
35682
35725
  init_llm();
35726
+ init_strategy_utils();
35683
35727
  }
35684
35728
  });
35685
35729
 
@@ -56414,6 +56458,49 @@ function registerConfigCommand(program2) {
56414
56458
  process.exitCode = error49 instanceof ApiError ? 2 : 1;
56415
56459
  }
56416
56460
  });
56461
+ configCmd.command("set-models").description("Update per-stage model assignments (no API key needed \u2014 works with Codex OAuth)").option("-m, --model <model>", "Default model for all phases").option("--scan-model <model>", "Model for market scanning").option("--analyze-model <model>", "Model for deep analysis").option("--decide-model <model>", "Model for trade decisions").option("--exit-heartbeat-model <model>", "Model for exit heartbeat checks (cheap, every 4h)").option("--exit-event-model <model>", "Model for exit event-driven analysis (quality)").action(async (opts) => {
56462
+ if (!opts.model && !opts.scanModel && !opts.analyzeModel && !opts.decideModel && !opts.exitHeartbeatModel && !opts.exitEventModel) {
56463
+ console.error(source_default.red(" Error: At least one model flag is required."));
56464
+ console.log(source_default.dim(" Example: trading-boy config set-models --scan-model gpt-5.4-mini --analyze-model gpt-5.4"));
56465
+ process.exitCode = 1;
56466
+ return;
56467
+ }
56468
+ try {
56469
+ const result = await apiRequest("/api/v1/llm-config/models", {
56470
+ method: "PATCH",
56471
+ body: {
56472
+ ...opts.model ? { model: opts.model } : {},
56473
+ ...opts.scanModel ? { scanModel: opts.scanModel } : {},
56474
+ ...opts.analyzeModel ? { analyzeModel: opts.analyzeModel } : {},
56475
+ ...opts.decideModel ? { decideModel: opts.decideModel } : {},
56476
+ ...opts.exitHeartbeatModel ? { exitHeartbeatModel: opts.exitHeartbeatModel } : {},
56477
+ ...opts.exitEventModel ? { exitEventModel: opts.exitEventModel } : {}
56478
+ }
56479
+ });
56480
+ console.log("");
56481
+ console.log(source_default.green(" Stage models updated"));
56482
+ console.log(source_default.gray(" " + "\u2500".repeat(40)));
56483
+ console.log(` ${source_default.gray("Default:")} ${result.model}`);
56484
+ if (result.scanModel)
56485
+ console.log(` ${source_default.gray("Scan:")} ${result.scanModel}`);
56486
+ if (result.analyzeModel)
56487
+ console.log(` ${source_default.gray("Analyze:")} ${result.analyzeModel}`);
56488
+ if (result.decideModel)
56489
+ console.log(` ${source_default.gray("Decide:")} ${result.decideModel}`);
56490
+ if (result.exitHeartbeatModel)
56491
+ console.log(` ${source_default.gray("Exit heartbeat:")} ${result.exitHeartbeatModel}`);
56492
+ if (result.exitEventModel)
56493
+ console.log(` ${source_default.gray("Exit event:")} ${result.exitEventModel}`);
56494
+ console.log("");
56495
+ console.log(source_default.dim(" Agents will use these models on their next tick."));
56496
+ console.log("");
56497
+ } catch (error49) {
56498
+ const message = error49 instanceof Error ? error49.message : String(error49);
56499
+ logger17.error({ error: message }, "Failed to set stage models");
56500
+ console.error(source_default.red(`Error: ${message}`));
56501
+ process.exitCode = error49 instanceof ApiError ? 2 : 1;
56502
+ }
56503
+ });
56417
56504
  configCmd.command("get-llm-key").description("Show current LLM configuration (key redacted)").addOption(new Option("--format <format>", "Output format").choices(["text", "json"]).default("text")).action(async (options) => {
56418
56505
  try {
56419
56506
  const result = await apiRequest("/api/v1/llm-config");
@@ -56434,6 +56521,12 @@ function registerConfigCommand(program2) {
56434
56521
  if (result.decideProvider || result.decideModel) {
56435
56522
  console.log(` ${source_default.gray("Decide:")} ${result.decideProvider ?? result.provider} / ${result.decideModel ?? result.model}`);
56436
56523
  }
56524
+ if (result.exitHeartbeatModel) {
56525
+ console.log(` ${source_default.gray("Exit HB:")} ${result.exitHeartbeatModel}`);
56526
+ }
56527
+ if (result.exitEventModel) {
56528
+ console.log(` ${source_default.gray("Exit Event:")} ${result.exitEventModel}`);
56529
+ }
56437
56530
  if (result.baseUrl) {
56438
56531
  console.log(` ${source_default.gray("Base URL:")} ${result.baseUrl}`);
56439
56532
  }
@@ -57018,9 +57111,13 @@ init_source();
57018
57111
  init_dist2();
57019
57112
  init_api_client();
57020
57113
  init_utils3();
57114
+ import http from "node:http";
57115
+ import { URL as URL2 } from "node:url";
57021
57116
  var logger23 = createLogger("cli-connect-chatgpt");
57117
+ var CALLBACK_PORT = 1455;
57118
+ var CALLBACK_PATH = "/auth/callback";
57119
+ var AUTH_TIMEOUT_MS = 3 * 60 * 1e3;
57022
57120
  var POLL_INTERVAL_MS = 2e3;
57023
- var POLL_TIMEOUT_MS = 3 * 60 * 1e3;
57024
57121
  function registerConnectChatgptCommand(program2) {
57025
57122
  program2.command("connect-chatgpt").description("Connect your ChatGPT subscription as your LLM provider (no API key needed)").option("--disconnect", "Disconnect your ChatGPT account").action(async (options) => {
57026
57123
  try {
@@ -57071,38 +57168,125 @@ async function handleConnect() {
57071
57168
  console.log(source_default.dim(" Use your ChatGPT subscription to power your trading agents."));
57072
57169
  console.log(source_default.dim(" No API key needed \u2014 authenticates via your OpenAI account."));
57073
57170
  console.log("");
57074
- const { authUrl } = await apiRequest("/api/v1/codex/auth-url");
57171
+ const { authUrl, state } = await apiRequest("/api/v1/codex/auth-url");
57172
+ const result = await raceForAuth(authUrl, state);
57173
+ if (result.via === "local") {
57174
+ console.log(source_default.dim(" Exchanging authorization code..."));
57175
+ const exchangeResult = await apiRequest("/api/v1/codex/callback", {
57176
+ method: "POST",
57177
+ body: { code: result.code, state: result.state }
57178
+ });
57179
+ printSuccess(exchangeResult.accountId, null);
57180
+ } else {
57181
+ printSuccess(result.config.codexAccountId, result.config.model);
57182
+ }
57183
+ }
57184
+ function printSuccess(accountId, model) {
57185
+ console.log("");
57186
+ console.log(source_default.green(" \u2713 ChatGPT connected!"));
57187
+ console.log("");
57188
+ if (accountId)
57189
+ console.log(` ${source_default.gray("Account:")} ${accountId}`);
57190
+ console.log(` ${source_default.gray("Provider:")} codex`);
57191
+ if (model)
57192
+ console.log(` ${source_default.gray("Model:")} ${model}`);
57193
+ console.log("");
57194
+ console.log(source_default.dim(" Your agents will now use your ChatGPT subscription for LLM calls."));
57195
+ console.log(source_default.dim(" Change model: trading-boy config set-llm-key --provider codex --model <model>"));
57196
+ }
57197
+ async function raceForAuth(authUrl, state) {
57198
+ const controller = new AbortController();
57199
+ const localCallback = startLocalCallbackServer(state, controller.signal);
57200
+ const polling = pollForConnection(controller.signal);
57075
57201
  console.log(source_default.white(" Opening OpenAI login in your browser..."));
57076
57202
  await openBrowser(authUrl);
57077
57203
  console.log(source_default.dim(" Complete sign-in in your browser to continue."));
57204
+ console.log(source_default.dim(" Waiting for authentication..."));
57078
57205
  console.log("");
57079
- const spinner = (await createSpinner(" Waiting for authentication...")).start();
57080
- const start = Date.now();
57081
- while (Date.now() - start < POLL_TIMEOUT_MS) {
57206
+ try {
57207
+ const result = await Promise.race([localCallback, polling]);
57208
+ controller.abort();
57209
+ return result;
57210
+ } catch (err) {
57211
+ controller.abort();
57212
+ throw err;
57213
+ }
57214
+ }
57215
+ function startLocalCallbackServer(state, signal) {
57216
+ return new Promise((resolve4, reject) => {
57217
+ if (signal.aborted) {
57218
+ reject(new Error("Cancelled"));
57219
+ return;
57220
+ }
57221
+ const timeout = setTimeout(() => {
57222
+ server.close();
57223
+ reject(new Error("Timed out waiting for OpenAI authentication (3 minutes)"));
57224
+ }, AUTH_TIMEOUT_MS);
57225
+ signal.addEventListener("abort", () => {
57226
+ clearTimeout(timeout);
57227
+ server.close();
57228
+ }, { once: true });
57229
+ const server = http.createServer((req, res) => {
57230
+ const url2 = new URL2(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
57231
+ if (url2.pathname !== CALLBACK_PATH) {
57232
+ res.writeHead(404, { "Content-Type": "text/plain" });
57233
+ res.end("Not found");
57234
+ return;
57235
+ }
57236
+ const errorParam = url2.searchParams.get("error");
57237
+ const errorDesc = url2.searchParams.get("error_description");
57238
+ const codeParam = url2.searchParams.get("code");
57239
+ if (errorParam) {
57240
+ res.writeHead(200, { "Content-Type": "text/html" });
57241
+ res.end(callbackHtml(false, errorDesc || errorParam));
57242
+ clearTimeout(timeout);
57243
+ server.close();
57244
+ reject(new Error(`OpenAI returned error: ${errorDesc || errorParam}`));
57245
+ return;
57246
+ }
57247
+ if (!codeParam) {
57248
+ res.writeHead(200, { "Content-Type": "text/html" });
57249
+ res.end(callbackHtml(false, "Missing authorization code"));
57250
+ clearTimeout(timeout);
57251
+ server.close();
57252
+ reject(new Error("No authorization code in callback"));
57253
+ return;
57254
+ }
57255
+ res.writeHead(200, { "Content-Type": "text/html" });
57256
+ res.end(callbackHtml(true));
57257
+ clearTimeout(timeout);
57258
+ server.close();
57259
+ resolve4({ via: "local", code: codeParam, state });
57260
+ });
57261
+ server.on("error", (err) => {
57262
+ clearTimeout(timeout);
57263
+ if (err.code === "EADDRINUSE") {
57264
+ logger23.debug("Port %d in use, falling back to polling only", CALLBACK_PORT);
57265
+ return;
57266
+ }
57267
+ reject(err);
57268
+ });
57269
+ server.listen(CALLBACK_PORT, "127.0.0.1");
57270
+ });
57271
+ }
57272
+ async function pollForConnection(signal) {
57273
+ const deadline = Date.now() + AUTH_TIMEOUT_MS;
57274
+ while (Date.now() < deadline) {
57275
+ if (signal.aborted)
57276
+ throw new Error("Cancelled");
57082
57277
  await sleep3(POLL_INTERVAL_MS);
57083
- const elapsed = Math.round((Date.now() - start) / 1e3);
57084
- spinner.text = ` Waiting for authentication... (${elapsed}s)`;
57278
+ if (signal.aborted)
57279
+ throw new Error("Cancelled");
57085
57280
  try {
57086
57281
  const config2 = await apiRequest("/api/v1/llm-config");
57087
57282
  if (config2.codexConnected) {
57088
- spinner.succeed(source_default.green(" ChatGPT connected!"));
57089
- console.log("");
57090
- console.log(` ${source_default.gray("Account:")} ${config2.codexAccountId ?? "connected"}`);
57091
- console.log(` ${source_default.gray("Provider:")} codex`);
57092
- console.log(` ${source_default.gray("Model:")} ${config2.model}`);
57093
- console.log("");
57094
- console.log(source_default.dim(" Your agents will now use your ChatGPT subscription for LLM calls."));
57095
- console.log(source_default.dim(" Change model: trading-boy config set-llm-key --provider codex --model <model>"));
57096
- return;
57283
+ return { via: "poll", config: config2 };
57097
57284
  }
57098
- } catch (error49) {
57099
- logger23.debug({ error: error49 }, "Poll attempt failed");
57285
+ } catch {
57286
+ logger23.debug("Poll attempt failed, retrying...");
57100
57287
  }
57101
57288
  }
57102
- spinner.fail(source_default.yellow(" Timed out waiting for authentication."));
57103
- console.log("");
57104
- console.log(source_default.dim(" The browser window may still be open \u2014 complete sign-in there,"));
57105
- console.log(source_default.dim(" then run this command again to check the connection status."));
57289
+ throw new Error("Timed out waiting for OpenAI authentication (3 minutes)");
57106
57290
  }
57107
57291
  async function handleDisconnect() {
57108
57292
  const { confirm } = await Promise.resolve().then(() => (init_esm15(), esm_exports));
@@ -57121,6 +57305,21 @@ async function handleDisconnect() {
57121
57305
  console.log(source_default.dim(" Your agents will need an API key to continue. Set one:"));
57122
57306
  console.log(source_default.dim(" trading-boy config set-llm-key <your-api-key>"));
57123
57307
  }
57308
+ function callbackHtml(success3, errorMsg) {
57309
+ const title = success3 ? "ChatGPT Connected" : "Connection Failed";
57310
+ const icon = success3 ? "&#10003;" : "&#10007;";
57311
+ const color = success3 ? "#22c55e" : "#ef4444";
57312
+ const message = success3 ? "Your ChatGPT subscription is now connected to Trading Boy.<br>You can close this tab." : `Something went wrong: ${escapeHtml(errorMsg ?? "Unknown error")}.<br>Please try again.`;
57313
+ return `<!DOCTYPE html>
57314
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
57315
+ <title>${title} \u2014 Trading Boy</title>
57316
+ <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}
57317
+ .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>
57318
+ </head><body><div class="card"><div class="icon">${icon}</div><h1>${title}</h1><p>${message}</p></div></body></html>`;
57319
+ }
57320
+ function escapeHtml(str2) {
57321
+ return str2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
57322
+ }
57124
57323
  function sleep3(ms) {
57125
57324
  return new Promise((resolve4) => setTimeout(resolve4, ms));
57126
57325
  }
@@ -57130,7 +57329,7 @@ var ALLOWED_AUTH_DOMAINS = /* @__PURE__ */ new Set([
57130
57329
  "chat.openai.com"
57131
57330
  ]);
57132
57331
  function validateAuthUrl(url2) {
57133
- const parsed = new URL(url2);
57332
+ const parsed = new URL2(url2);
57134
57333
  if (parsed.protocol !== "https:") {
57135
57334
  throw new Error(`Refusing to open non-HTTPS URL: ${url2}`);
57136
57335
  }
@@ -57359,11 +57558,15 @@ async function runOnboarding() {
57359
57558
  console.log("");
57360
57559
  console.log(source_default.dim(" Once you create an agent, it runs continuously on our servers:"));
57361
57560
  console.log("");
57362
- console.log(` ${source_default.white("Scan")} ${source_default.dim("Every 5 min (configurable) \u2014 scans your watchlist for setups")}`);
57561
+ console.log(` ${source_default.white("Scan")} ${source_default.dim("Every 30 min by default \u2014 scans your watchlist for setups")}`);
57363
57562
  console.log(` ${source_default.white("Analyze")} ${source_default.dim("When a setup is found \u2014 deep context analysis via your LLM")}`);
57364
57563
  console.log(` ${source_default.white("Decide")} ${source_default.dim("After analysis \u2014 enter/exit/hold based on your strategy")}`);
57365
57564
  console.log(` ${source_default.white("Learn")} ${source_default.dim("After trades close \u2014 updates edge profile from outcomes")}`);
57366
57565
  console.log("");
57566
+ console.log(source_default.dim(" Scan interval controls how often your agent checks for setups."));
57567
+ console.log(source_default.dim(" Lower intervals = more LLM calls = higher cost. Recommended: 30m."));
57568
+ console.log(source_default.dim(" Change it: trading-boy agent update <name> --scan-interval 15m"));
57569
+ console.log("");
57367
57570
  console.log(source_default.dim(" Autonomy levels:"));
57368
57571
  console.log(` ${source_default.cyan("OBSERVE_ONLY")} ${source_default.dim("Scans and analyzes \u2014 takes no action")}`);
57369
57572
  console.log(` ${source_default.cyan("SUGGEST")} ${source_default.dim("Sends trade ideas to your Telegram")}`);
@@ -57398,7 +57601,7 @@ function isUserAbort(error49) {
57398
57601
  // dist/commands/subscribe.js
57399
57602
  var logger24 = createLogger("cli-subscribe");
57400
57603
  var POLL_INTERVAL_MS2 = 3e3;
57401
- var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
57604
+ var POLL_TIMEOUT_MS = 5 * 60 * 1e3;
57402
57605
  async function createCheckoutSession(email3, plan = "starter") {
57403
57606
  const response = await fetch(`${getApiBase()}/api/v1/billing/checkout`, {
57404
57607
  method: "POST",
@@ -57450,7 +57653,7 @@ async function checkCryptoPayment(reference) {
57450
57653
  async function pollCryptoPayment(reference, provisioningToken, onTick) {
57451
57654
  const start = Date.now();
57452
57655
  const CRYPTO_POLL_INTERVAL = 5e3;
57453
- while (Date.now() - start < POLL_TIMEOUT_MS2) {
57656
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
57454
57657
  onTick?.(Date.now() - start, "payment");
57455
57658
  try {
57456
57659
  const result = await checkCryptoPayment(reference);
@@ -57464,10 +57667,10 @@ async function pollCryptoPayment(reference, provisioningToken, onTick) {
57464
57667
  }
57465
57668
  await sleep4(CRYPTO_POLL_INTERVAL);
57466
57669
  }
57467
- if (Date.now() - start >= POLL_TIMEOUT_MS2) {
57670
+ if (Date.now() - start >= POLL_TIMEOUT_MS) {
57468
57671
  return { success: false, error: "Timed out waiting for USDC payment. If you sent the payment, contact support." };
57469
57672
  }
57470
- while (Date.now() - start < POLL_TIMEOUT_MS2) {
57673
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
57471
57674
  onTick?.(Date.now() - start, "provisioning");
57472
57675
  try {
57473
57676
  const result = await pollProvisioningToken(provisioningToken);
@@ -57487,7 +57690,7 @@ async function pollCryptoPayment(reference, provisioningToken, onTick) {
57487
57690
  async function pollForApiKey(token, onTick) {
57488
57691
  const start = Date.now();
57489
57692
  let elapsed = Date.now() - start;
57490
- while (elapsed < POLL_TIMEOUT_MS2) {
57693
+ while (elapsed < POLL_TIMEOUT_MS) {
57491
57694
  onTick?.(elapsed);
57492
57695
  try {
57493
57696
  const result = await pollProvisioningToken(token);
@@ -58582,12 +58785,7 @@ function registerStrategyCommand(program2) {
58582
58785
  agentId: options.agentId,
58583
58786
  tokens,
58584
58787
  setupTypes,
58585
- regimeBehavior: {
58586
- ACCUMULATION: { enabled: true, maxPositionSize: 0.25, confidenceThreshold: 0.6, preferredSetups: setupTypes },
58587
- MARKUP: { enabled: true, maxPositionSize: 0.5, confidenceThreshold: 0.5, preferredSetups: setupTypes },
58588
- DISTRIBUTION: { enabled: true, maxPositionSize: 0.15, confidenceThreshold: 0.7, preferredSetups: setupTypes },
58589
- MARKDOWN: { enabled: false, maxPositionSize: 0.1, confidenceThreshold: 0.8, preferredSetups: setupTypes }
58590
- },
58788
+ regimeBehavior: generateRegimeBehavior(setupTypes),
58591
58789
  riskLimits: {
58592
58790
  maxDrawdown: 0.2,
58593
58791
  maxConcurrentPositions: 5,
@@ -58788,9 +58986,9 @@ function formatWinRate2(rate) {
58788
58986
  return source_default.red(str2);
58789
58987
  }
58790
58988
  var POLL_INTERVAL_MS3 = 2e3;
58791
- var POLL_TIMEOUT_MS3 = 6e4;
58989
+ var POLL_TIMEOUT_MS2 = 6e4;
58792
58990
  async function pollReplayJob(jobId) {
58793
- const deadline = Date.now() + POLL_TIMEOUT_MS3;
58991
+ const deadline = Date.now() + POLL_TIMEOUT_MS2;
58794
58992
  while (Date.now() < deadline) {
58795
58993
  const status = await apiRequest(`/api/v1/replay/${encodeURIComponent(jobId)}`);
58796
58994
  if (status.status === "COMPLETE" || status.status === "FAILED") {
@@ -58798,7 +58996,7 @@ async function pollReplayJob(jobId) {
58798
58996
  }
58799
58997
  await new Promise((resolve4) => setTimeout(resolve4, POLL_INTERVAL_MS3));
58800
58998
  }
58801
- throw new Error(`Replay job ${jobId} did not complete within ${POLL_TIMEOUT_MS3 / 1e3}s timeout.`);
58999
+ throw new Error(`Replay job ${jobId} did not complete within ${POLL_TIMEOUT_MS2 / 1e3}s timeout.`);
58802
59000
  }
58803
59001
  function registerReplayCommand(program2) {
58804
59002
  program2.command("replay").description("Run strategy replay (backtest) over a historical period").requiredOption("--strategy <id>", "Strategy ID").requiredOption("--token <symbol>", "Token symbol").requiredOption("--from <date>", "Start date (ISO-8601, e.g. 2025-01-01)").requiredOption("--to <date>", "End date (ISO-8601, e.g. 2025-03-01)").addOption(new Option("--format <format>", "Output format").choices(["text", "json"]).default("text")).action(async (options) => {
@@ -59413,7 +59611,7 @@ function parseHumanInterval(value) {
59413
59611
  var MIN_SCAN_INTERVAL_MS = 6e4;
59414
59612
  function registerAgentCommand(program2) {
59415
59613
  const agent = program2.command("agent").description("Manage autonomous trading agents");
59416
- agent.command("create").description("Create a new agent").option("--trader-id <traderId>", "Trader ID").option("--strategy-id <strategyId>", "Strategy ID").option("--name <name>", "Agent name").option("--autonomy <level>", "Autonomy level: OBSERVE_ONLY, SUGGEST, AUTO_WITH_APPROVAL, FULLY_AUTONOMOUS", "OBSERVE_ONLY").option("--scan-interval <ms>", "Scan interval in ms (min 60000)", "300000").option("--scan-interval-human <duration>", "Scan interval in human-readable format (e.g. 1m, 5m, 15m, 30m, 1h)").option("--watchlist <symbols>", "Comma-separated token symbols").option("--max-daily-trades <n>", "Max daily trades", "10").option("--max-daily-loss <usd>", "Max daily loss in USD", "500").option("--max-position-size <pct>", "Max position size as decimal (0.10 = 10%)", "0.10").option("--min-confidence <n>", "Min confidence threshold (0-1)", "0.60").option("--scan-model <model>", "LLM model for market scanning").option("--analyze-model <model>", "LLM model for deep analysis").option("--decide-model <model>", "LLM model for trade decisions").addOption(new Option("--asset-class <class>", "Asset class for this agent").choices(["crypto", "commodities", "mixed"]).default("crypto")).option("--soul-override <text>", "Custom soul/personality for this agent").option("--purpose-override <text>", "Custom purpose/mission for this agent").option("--soul-file <path>", "Load soul from a file").option("--purpose-file <path>", "Load purpose from a file").option("--exit-reasoner", "Enable LLM-powered exit reasoning for this agent").addOption(new Option("--format <format>", "Output format").choices(["text", "json"]).default("text")).action(async (options) => {
59614
+ agent.command("create").description("Create a new agent").option("--trader-id <traderId>", "Trader ID").option("--strategy-id <strategyId>", "Strategy ID (optional \u2014 auto-creates if omitted)").option("--name <name>", "Agent name").option("--autonomy <level>", "Autonomy level: OBSERVE_ONLY, SUGGEST, AUTO_WITH_APPROVAL, FULLY_AUTONOMOUS", "OBSERVE_ONLY").option("--scan-interval <ms>", "Scan interval in ms (min 60000)", "300000").option("--scan-interval-human <duration>", "Scan interval in human-readable format (e.g. 1m, 5m, 15m, 30m, 1h)").option("--watchlist <symbols>", "Comma-separated token symbols").option("--max-daily-trades <n>", "Max daily trades", "10").option("--max-daily-loss <usd>", "Max daily loss in USD", "500").option("--max-position-size <pct>", "Max position size as decimal (0.10 = 10%)", "0.10").option("--min-confidence <n>", "Min confidence threshold (0-1)", "0.60").option("--scan-model <model>", "LLM model for market scanning").option("--analyze-model <model>", "LLM model for deep analysis").option("--decide-model <model>", "LLM model for trade decisions").addOption(new Option("--asset-class <class>", "Asset class for this agent").choices(["crypto", "commodities", "mixed"]).default("crypto")).option("--soul-override <text>", "Custom soul/personality for this agent").option("--purpose-override <text>", "Custom purpose/mission for this agent").option("--soul-file <path>", "Load soul from a file").option("--purpose-file <path>", "Load purpose from a file").option("--exit-reasoner", "Enable LLM-powered exit reasoning for this agent").addOption(new Option("--format <format>", "Output format").choices(["text", "json"]).default("text")).action(async (options) => {
59417
59615
  if (!await ensureRemote())
59418
59616
  return;
59419
59617
  if (!options.traderId) {
@@ -59422,16 +59620,11 @@ function registerAgentCommand(program2) {
59422
59620
  process.exitCode = 1;
59423
59621
  return;
59424
59622
  }
59425
- if (!options.strategyId) {
59426
- console.error(source_default.red("Error: --strategy-id is required."));
59427
- console.log(source_default.dim(" Find yours with: trading-boy strategy list"));
59428
- process.exitCode = 1;
59429
- return;
59430
- }
59431
59623
  const body = {
59432
- traderId: options.traderId,
59433
- strategyId: options.strategyId
59624
+ traderId: options.traderId
59434
59625
  };
59626
+ if (options.strategyId)
59627
+ body.strategyId = options.strategyId;
59435
59628
  if (options.name)
59436
59629
  body.name = options.name;
59437
59630
  if (options.autonomy)
@@ -59507,7 +59700,7 @@ function registerAgentCommand(program2) {
59507
59700
  console.log(` ${source_default.gray("ID:")} ${result.id}`);
59508
59701
  console.log(` ${source_default.gray("Name:")} ${result.name}`);
59509
59702
  console.log(` ${source_default.gray("Trader:")} ${result.traderId}`);
59510
- console.log(` ${source_default.gray("Strategy:")} ${result.strategyId}`);
59703
+ console.log(` ${source_default.gray("Strategy:")} ${result.strategyId}${result.autoStrategyCreated ? source_default.dim(" (auto-created)") : ""}`);
59511
59704
  console.log(` ${source_default.gray("Autonomy:")} ${formatAutonomy(result.autonomyLevel)}`);
59512
59705
  console.log(` ${source_default.gray("Interval:")} ${formatInterval(result.scanIntervalMs)}`);
59513
59706
  console.log(` ${source_default.gray("Next scan:")} ${formatShortDate5(result.nextScanAt)}`);
package/dist/cli.js CHANGED
@@ -43,6 +43,7 @@ import { registerSuggestionsCommand } from './commands/suggestions-cmd.js';
43
43
  import { registerCronCommand } from './commands/cron-cmd.js';
44
44
  import { registerAgentCommand } from './commands/agent-cmd.js';
45
45
  import { registerConnectChatgptCommand } from './commands/connect-chatgpt.js';
46
+ // import { registerConnectClaudeCommand } from './commands/connect-claude.js'; // Hidden — Anthropic ToS prohibits OAuth for third-party use
46
47
  import { readFileSync } from 'node:fs';
47
48
  import { fileURLToPath } from 'node:url';
48
49
  import { dirname, resolve } from 'node:path';
@@ -86,6 +87,7 @@ export function createCli() {
86
87
  registerBillingCommand(program);
87
88
  registerSubscribeCommand(program);
88
89
  registerConnectChatgptCommand(program);
90
+ // registerConnectClaudeCommand(program); // Hidden — Anthropic ToS prohibits OAuth for third-party use
89
91
  // Learning & coaching
90
92
  registerCoachingCommand(program);
91
93
  registerThesisCommand(program);
@@ -79,7 +79,7 @@ export function registerAgentCommand(program) {
79
79
  .command('create')
80
80
  .description('Create a new agent')
81
81
  .option('--trader-id <traderId>', 'Trader ID')
82
- .option('--strategy-id <strategyId>', 'Strategy ID')
82
+ .option('--strategy-id <strategyId>', 'Strategy ID (optional — auto-creates if omitted)')
83
83
  .option('--name <name>', 'Agent name')
84
84
  .option('--autonomy <level>', 'Autonomy level: OBSERVE_ONLY, SUGGEST, AUTO_WITH_APPROVAL, FULLY_AUTONOMOUS', 'OBSERVE_ONLY')
85
85
  .option('--scan-interval <ms>', 'Scan interval in ms (min 60000)', '300000')
@@ -108,16 +108,11 @@ export function registerAgentCommand(program) {
108
108
  process.exitCode = 1;
109
109
  return;
110
110
  }
111
- if (!options.strategyId) {
112
- console.error(chalk.red('Error: --strategy-id is required.'));
113
- console.log(chalk.dim(' Find yours with: trading-boy strategy list'));
114
- process.exitCode = 1;
115
- return;
116
- }
117
111
  const body = {
118
112
  traderId: options.traderId,
119
- strategyId: options.strategyId,
120
113
  };
114
+ if (options.strategyId)
115
+ body.strategyId = options.strategyId;
121
116
  if (options.name)
122
117
  body.name = options.name;
123
118
  if (options.autonomy)
@@ -200,7 +195,7 @@ export function registerAgentCommand(program) {
200
195
  console.log(` ${chalk.gray('ID:')} ${result.id}`);
201
196
  console.log(` ${chalk.gray('Name:')} ${result.name}`);
202
197
  console.log(` ${chalk.gray('Trader:')} ${result.traderId}`);
203
- console.log(` ${chalk.gray('Strategy:')} ${result.strategyId}`);
198
+ console.log(` ${chalk.gray('Strategy:')} ${result.strategyId}${result.autoStrategyCreated ? chalk.dim(' (auto-created)') : ''}`);
204
199
  console.log(` ${chalk.gray('Autonomy:')} ${formatAutonomy(result.autonomyLevel)}`);
205
200
  console.log(` ${chalk.gray('Interval:')} ${formatInterval(result.scanIntervalMs)}`);
206
201
  console.log(` ${chalk.gray('Next scan:')} ${formatShortDate(result.nextScanAt)}`);
@@ -379,6 +379,60 @@ export function registerConfigCommand(program) {
379
379
  process.exitCode = error instanceof ApiError ? 2 : 1;
380
380
  }
381
381
  });
382
+ // ─── config set-models ───
383
+ configCmd
384
+ .command('set-models')
385
+ .description('Update per-stage model assignments (no API key needed — works with Codex OAuth)')
386
+ .option('-m, --model <model>', 'Default model for all phases')
387
+ .option('--scan-model <model>', 'Model for market scanning')
388
+ .option('--analyze-model <model>', 'Model for deep analysis')
389
+ .option('--decide-model <model>', 'Model for trade decisions')
390
+ .option('--exit-heartbeat-model <model>', 'Model for exit heartbeat checks (cheap, every 4h)')
391
+ .option('--exit-event-model <model>', 'Model for exit event-driven analysis (quality)')
392
+ .action(async (opts) => {
393
+ if (!opts.model && !opts.scanModel && !opts.analyzeModel && !opts.decideModel && !opts.exitHeartbeatModel && !opts.exitEventModel) {
394
+ console.error(chalk.red(' Error: At least one model flag is required.'));
395
+ console.log(chalk.dim(' Example: trading-boy config set-models --scan-model gpt-5.4-mini --analyze-model gpt-5.4'));
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+ try {
400
+ const result = await apiRequest('/api/v1/llm-config/models', {
401
+ method: 'PATCH',
402
+ body: {
403
+ ...(opts.model ? { model: opts.model } : {}),
404
+ ...(opts.scanModel ? { scanModel: opts.scanModel } : {}),
405
+ ...(opts.analyzeModel ? { analyzeModel: opts.analyzeModel } : {}),
406
+ ...(opts.decideModel ? { decideModel: opts.decideModel } : {}),
407
+ ...(opts.exitHeartbeatModel ? { exitHeartbeatModel: opts.exitHeartbeatModel } : {}),
408
+ ...(opts.exitEventModel ? { exitEventModel: opts.exitEventModel } : {}),
409
+ },
410
+ });
411
+ console.log('');
412
+ console.log(chalk.green(' Stage models updated'));
413
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
414
+ console.log(` ${chalk.gray('Default:')} ${result.model}`);
415
+ if (result.scanModel)
416
+ console.log(` ${chalk.gray('Scan:')} ${result.scanModel}`);
417
+ if (result.analyzeModel)
418
+ console.log(` ${chalk.gray('Analyze:')} ${result.analyzeModel}`);
419
+ if (result.decideModel)
420
+ console.log(` ${chalk.gray('Decide:')} ${result.decideModel}`);
421
+ if (result.exitHeartbeatModel)
422
+ console.log(` ${chalk.gray('Exit heartbeat:')} ${result.exitHeartbeatModel}`);
423
+ if (result.exitEventModel)
424
+ console.log(` ${chalk.gray('Exit event:')} ${result.exitEventModel}`);
425
+ console.log('');
426
+ console.log(chalk.dim(' Agents will use these models on their next tick.'));
427
+ console.log('');
428
+ }
429
+ catch (error) {
430
+ const message = error instanceof Error ? error.message : String(error);
431
+ logger.error({ error: message }, 'Failed to set stage models');
432
+ console.error(chalk.red(`Error: ${message}`));
433
+ process.exitCode = error instanceof ApiError ? 2 : 1;
434
+ }
435
+ });
382
436
  // ─── config get-llm-key ───
383
437
  configCmd
384
438
  .command('get-llm-key')
@@ -405,6 +459,12 @@ export function registerConfigCommand(program) {
405
459
  if (result.decideProvider || result.decideModel) {
406
460
  console.log(` ${chalk.gray('Decide:')} ${result.decideProvider ?? result.provider} / ${result.decideModel ?? result.model}`);
407
461
  }
462
+ if (result.exitHeartbeatModel) {
463
+ console.log(` ${chalk.gray('Exit HB:')} ${result.exitHeartbeatModel}`);
464
+ }
465
+ if (result.exitEventModel) {
466
+ console.log(` ${chalk.gray('Exit Event:')} ${result.exitEventModel}`);
467
+ }
408
468
  if (result.baseUrl) {
409
469
  console.log(` ${chalk.gray('Base URL:')} ${result.baseUrl}`);
410
470
  }
@@ -2,22 +2,25 @@
2
2
  //
3
3
  // Connects a user's ChatGPT subscription via Codex OAuth (PKCE).
4
4
  //
5
- // Flow:
6
- // 1. GET /api/v1/codex/auth-url{ authUrl, state }
7
- // 2. Open browser to authUrl (user authenticates with OpenAI)
8
- // 3. OpenAI redirects back to API server callback
9
- // 4. CLI polls GET /api/v1/llm-config until codexConnected: true
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)
10
9
  //
11
10
  // Also supports disconnect via --disconnect flag.
11
+ import http from 'node:http';
12
+ import { URL } from 'node:url';
12
13
  import chalk from 'chalk';
13
14
  import { createLogger } from '@trading-boy/core';
14
15
  import { apiRequest, ApiError, isRemoteMode } from '../api-client.js';
15
- import { createSpinner, formatConnectionError } from '../utils.js';
16
+ import { formatConnectionError } from '../utils.js';
16
17
  // ─── Logger ───
17
18
  const logger = createLogger('cli-connect-chatgpt');
18
19
  // ─── Constants ───
20
+ const CALLBACK_PORT = 1455;
21
+ const CALLBACK_PATH = '/auth/callback';
22
+ const AUTH_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
19
23
  const POLL_INTERVAL_MS = 2_000;
20
- const POLL_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
21
24
  // ─── Command ───
22
25
  export function registerConnectChatgptCommand(program) {
23
26
  program
@@ -77,43 +80,141 @@ async function handleConnect() {
77
80
  console.log(chalk.dim(' Use your ChatGPT subscription to power your trading agents.'));
78
81
  console.log(chalk.dim(' No API key needed — authenticates via your OpenAI account.'));
79
82
  console.log('');
80
- // Step 1: Get auth URL
81
- const { authUrl } = await apiRequest('/api/v1/codex/auth-url');
82
- // Step 2: Open browser
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)
83
123
  console.log(chalk.white(' Opening OpenAI login in your browser...'));
84
124
  await openBrowser(authUrl);
85
125
  console.log(chalk.dim(' Complete sign-in in your browser to continue.'));
126
+ console.log(chalk.dim(' Waiting for authentication...'));
86
127
  console.log('');
87
- // Step 3: Poll for completion
88
- const spinner = (await createSpinner(' Waiting for authentication...')).start();
89
- const start = Date.now();
90
- while (Date.now() - start < POLL_TIMEOUT_MS) {
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');
91
203
  await sleep(POLL_INTERVAL_MS);
92
- const elapsed = Math.round((Date.now() - start) / 1000);
93
- spinner.text = ` Waiting for authentication... (${elapsed}s)`;
204
+ if (signal.aborted)
205
+ throw new Error('Cancelled');
94
206
  try {
95
207
  const config = await apiRequest('/api/v1/llm-config');
96
208
  if (config.codexConnected) {
97
- spinner.succeed(chalk.green(' ChatGPT connected!'));
98
- console.log('');
99
- console.log(` ${chalk.gray('Account:')} ${config.codexAccountId ?? 'connected'}`);
100
- console.log(` ${chalk.gray('Provider:')} codex`);
101
- console.log(` ${chalk.gray('Model:')} ${config.model}`);
102
- console.log('');
103
- console.log(chalk.dim(' Your agents will now use your ChatGPT subscription for LLM calls.'));
104
- console.log(chalk.dim(' Change model: trading-boy config set-llm-key --provider codex --model <model>'));
105
- return;
209
+ return { via: 'poll', config };
106
210
  }
107
211
  }
108
- catch (error) {
212
+ catch {
109
213
  // Poll failures are expected — API might briefly be unavailable
110
- logger.debug({ error }, 'Poll attempt failed');
214
+ logger.debug('Poll attempt failed, retrying...');
111
215
  }
112
216
  }
113
- spinner.fail(chalk.yellow(' Timed out waiting for authentication.'));
114
- console.log('');
115
- console.log(chalk.dim(' The browser window may still be open — complete sign-in there,'));
116
- console.log(chalk.dim(' then run this command again to check the connection status.'));
217
+ throw new Error('Timed out waiting for OpenAI authentication (3 minutes)');
117
218
  }
118
219
  // ─── Disconnect Flow ───
119
220
  async function handleDisconnect() {
@@ -133,6 +234,24 @@ async function handleDisconnect() {
133
234
  console.log(chalk.dim(' Your agents will need an API key to continue. Set one:'));
134
235
  console.log(chalk.dim(' trading-boy config set-llm-key <your-api-key>'));
135
236
  }
237
+ // ─── HTML for Browser Callback ───
238
+ function callbackHtml(success, errorMsg) {
239
+ const title = success ? 'ChatGPT Connected' : 'Connection Failed';
240
+ const icon = success ? '&#10003;' : '&#10007;';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
254
+ }
136
255
  // ─── Helpers ───
137
256
  function sleep(ms) {
138
257
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerConnectClaudeCommand(program: Command): void;
3
+ declare function handleConnect(): Promise<void>;
4
+ export { handleConnect as connectClaude };
5
+ //# sourceMappingURL=connect-claude.d.ts.map
@@ -0,0 +1,280 @@
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 ? '&#10003;' : '&#10007;';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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
@@ -237,11 +237,15 @@ export async function runOnboarding() {
237
237
  console.log('');
238
238
  console.log(chalk.dim(' Once you create an agent, it runs continuously on our servers:'));
239
239
  console.log('');
240
- console.log(` ${chalk.white('Scan')} ${chalk.dim('Every 5 min (configurable) — scans your watchlist for setups')}`);
240
+ console.log(` ${chalk.white('Scan')} ${chalk.dim('Every 30 min by default — scans your watchlist for setups')}`);
241
241
  console.log(` ${chalk.white('Analyze')} ${chalk.dim('When a setup is found — deep context analysis via your LLM')}`);
242
242
  console.log(` ${chalk.white('Decide')} ${chalk.dim('After analysis — enter/exit/hold based on your strategy')}`);
243
243
  console.log(` ${chalk.white('Learn')} ${chalk.dim('After trades close — updates edge profile from outcomes')}`);
244
244
  console.log('');
245
+ console.log(chalk.dim(' Scan interval controls how often your agent checks for setups.'));
246
+ console.log(chalk.dim(' Lower intervals = more LLM calls = higher cost. Recommended: 30m.'));
247
+ console.log(chalk.dim(' Change it: trading-boy agent update <name> --scan-interval 15m'));
248
+ console.log('');
245
249
  console.log(chalk.dim(' Autonomy levels:'));
246
250
  console.log(` ${chalk.cyan('OBSERVE_ONLY')} ${chalk.dim('Scans and analyzes — takes no action')}`);
247
251
  console.log(` ${chalk.cyan('SUGGEST')} ${chalk.dim('Sends trade ideas to your Telegram')}`);
@@ -1,6 +1,6 @@
1
1
  import { Option } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { createLogger } from '@trading-boy/core';
3
+ import { createLogger, generateRegimeBehavior } from '@trading-boy/core';
4
4
  import { apiRequest } from '../api-client.js';
5
5
  import { padRight, handleApiError, ensureRemote } from '../utils.js';
6
6
  // ─── Logger ───
@@ -155,12 +155,7 @@ export function registerStrategyCommand(program) {
155
155
  agentId: options.agentId,
156
156
  tokens,
157
157
  setupTypes,
158
- regimeBehavior: {
159
- ACCUMULATION: { enabled: true, maxPositionSize: 0.25, confidenceThreshold: 0.6, preferredSetups: setupTypes },
160
- MARKUP: { enabled: true, maxPositionSize: 0.5, confidenceThreshold: 0.5, preferredSetups: setupTypes },
161
- DISTRIBUTION: { enabled: true, maxPositionSize: 0.15, confidenceThreshold: 0.7, preferredSetups: setupTypes },
162
- MARKDOWN: { enabled: false, maxPositionSize: 0.1, confidenceThreshold: 0.8, preferredSetups: setupTypes },
163
- },
158
+ regimeBehavior: generateRegimeBehavior(setupTypes),
164
159
  riskLimits: {
165
160
  maxDrawdown: 0.2,
166
161
  maxConcurrentPositions: 5,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trading-boy/cli",
3
- "version": "1.10.0",
3
+ "version": "1.12.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": {