@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.
- package/dist/cli.bundle.js +241 -48
- package/dist/cli.js +2 -0
- package/dist/commands/agent-cmd.js +4 -9
- package/dist/commands/config-cmd.js +60 -0
- package/dist/commands/connect-chatgpt.js +150 -31
- package/dist/commands/connect-claude.d.ts +5 -0
- package/dist/commands/connect-claude.js +280 -0
- package/dist/commands/onboarding.js +5 -1
- package/dist/commands/strategy-cmd.js +2 -7
- package/package.json +1 -1
package/dist/cli.bundle.js
CHANGED
|
@@ -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
|
-
|
|
57080
|
-
|
|
57081
|
-
|
|
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
|
-
|
|
57084
|
-
|
|
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
|
-
|
|
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
|
|
57099
|
-
logger23.debug(
|
|
57285
|
+
} catch {
|
|
57286
|
+
logger23.debug("Poll attempt failed, retrying...");
|
|
57100
57287
|
}
|
|
57101
57288
|
}
|
|
57102
|
-
|
|
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 ? "✓" : "✗";
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
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
|
|
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
|
|
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 <
|
|
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 >=
|
|
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 <
|
|
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 <
|
|
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
|
|
58989
|
+
var POLL_TIMEOUT_MS2 = 6e4;
|
|
58792
58990
|
async function pollReplayJob(jobId) {
|
|
58793
|
-
const deadline = Date.now() +
|
|
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 ${
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
212
|
+
catch {
|
|
109
213
|
// Poll failures are expected — API might briefly be unavailable
|
|
110
|
-
logger.debug(
|
|
214
|
+
logger.debug('Poll attempt failed, retrying...');
|
|
111
215
|
}
|
|
112
216
|
}
|
|
113
|
-
|
|
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 ? '✓' : '✗';
|
|
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
|
+
}
|
|
136
255
|
// ─── Helpers ───
|
|
137
256
|
function sleep(ms) {
|
|
138
257
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -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 ? '✓' : '✗';
|
|
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
|
|
@@ -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
|
|
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.
|
|
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": {
|