@okx_ai/okx-trade-cli 1.2.2 → 1.2.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createRequire } from "module";
4
+ import { createRequire as createRequire2 } from "module";
5
5
 
6
6
  // ../core/dist/index.js
7
7
  import { createHmac } from "crypto";
@@ -933,8 +933,10 @@ function sleep(ms) {
933
933
  var RateLimiter = class {
934
934
  buckets = /* @__PURE__ */ new Map();
935
935
  maxWaitMs;
936
- constructor(maxWaitMs = 3e4) {
936
+ verbose;
937
+ constructor(maxWaitMs = 3e4, verbose = false) {
937
938
  this.maxWaitMs = maxWaitMs;
939
+ this.verbose = verbose;
938
940
  }
939
941
  async consume(config, amount = 1) {
940
942
  const bucket = this.getBucket(config);
@@ -952,6 +954,10 @@ var RateLimiter = class {
952
954
  "Reduce tool call frequency or retry later."
953
955
  );
954
956
  }
957
+ if (this.verbose) {
958
+ process.stderr.write(`[verbose] \u23F3 rate-limit: waiting ${waitMs}ms for "${config.key}"
959
+ `);
960
+ }
955
961
  await sleep(waitMs);
956
962
  this.refill(bucket);
957
963
  if (bucket.tokens < amount) {
@@ -1046,11 +1052,34 @@ function buildQueryString(query) {
1046
1052
  }
1047
1053
  return params.toString();
1048
1054
  }
1055
+ function maskKey(key) {
1056
+ if (key.length <= 8) return "***";
1057
+ return `${key.slice(0, 3)}***${key.slice(-3)}`;
1058
+ }
1059
+ function vlog(message) {
1060
+ process.stderr.write(`[verbose] ${message}
1061
+ `);
1062
+ }
1049
1063
  var OkxRestClient = class {
1050
1064
  config;
1051
- rateLimiter = new RateLimiter();
1065
+ rateLimiter;
1052
1066
  constructor(config) {
1053
1067
  this.config = config;
1068
+ this.rateLimiter = new RateLimiter(3e4, config.verbose);
1069
+ }
1070
+ logRequest(method, url, auth) {
1071
+ if (!this.config.verbose) return;
1072
+ vlog(`\u2192 ${method} ${url}`);
1073
+ const authInfo = auth === "private" && this.config.apiKey ? `auth=\u2713(${maskKey(this.config.apiKey)})` : `auth=${auth}`;
1074
+ vlog(` ${authInfo} demo=${this.config.demo} timeout=${this.config.timeoutMs}ms`);
1075
+ }
1076
+ logResponse(status, rawLen, elapsed, traceId, code, msg) {
1077
+ if (!this.config.verbose) return;
1078
+ if (code && code !== "0" && code !== "1") {
1079
+ vlog(`\u2717 ${status} | code=${code} | msg=${msg ?? "-"} | ${rawLen}B | ${elapsed}ms | trace=${traceId ?? "-"}`);
1080
+ } else {
1081
+ vlog(`\u2190 ${status} | code=${code ?? "0"} | ${rawLen}B | ${elapsed}ms | trace=${traceId ?? "-"}`);
1082
+ }
1054
1083
  }
1055
1084
  async publicGet(path4, query, rateLimit) {
1056
1085
  return this.request({
@@ -1079,126 +1108,146 @@ var OkxRestClient = class {
1079
1108
  rateLimit
1080
1109
  });
1081
1110
  }
1082
- async request(config) {
1083
- const queryString = buildQueryString(config.query);
1084
- const requestPath = queryString.length > 0 ? `${config.path}?${queryString}` : config.path;
1085
- const url = `${this.config.baseUrl}${requestPath}`;
1086
- const bodyJson = config.body ? JSON.stringify(config.body) : "";
1087
- const timestamp = getNow();
1088
- if (config.rateLimit) {
1089
- await this.rateLimiter.consume(config.rateLimit);
1090
- }
1091
- const headers = new Headers({
1092
- "Content-Type": "application/json",
1093
- Accept: "application/json"
1094
- });
1095
- if (this.config.userAgent) {
1096
- headers.set("User-Agent", this.config.userAgent);
1097
- }
1098
- if (config.auth === "private") {
1099
- if (!this.config.hasAuth) {
1100
- throw new ConfigError(
1101
- "Private endpoint requires API credentials.",
1102
- "Configure OKX_API_KEY, OKX_SECRET_KEY and OKX_PASSPHRASE."
1103
- );
1104
- }
1105
- if (!this.config.apiKey || !this.config.secretKey || !this.config.passphrase) {
1106
- throw new ConfigError(
1107
- "Invalid private API credentials state.",
1108
- "Ensure all OKX credentials are set."
1109
- );
1110
- }
1111
- const payload = `${timestamp}${config.method.toUpperCase()}${requestPath}${bodyJson}`;
1112
- const signature = signOkxPayload(payload, this.config.secretKey);
1113
- headers.set("OK-ACCESS-KEY", this.config.apiKey);
1114
- headers.set("OK-ACCESS-SIGN", signature);
1115
- headers.set("OK-ACCESS-PASSPHRASE", this.config.passphrase);
1116
- headers.set("OK-ACCESS-TIMESTAMP", timestamp);
1111
+ setAuthHeaders(headers, method, requestPath, bodyJson, timestamp) {
1112
+ if (!this.config.hasAuth) {
1113
+ throw new ConfigError(
1114
+ "Private endpoint requires API credentials.",
1115
+ "Configure OKX_API_KEY, OKX_SECRET_KEY and OKX_PASSPHRASE."
1116
+ );
1117
1117
  }
1118
- if (this.config.demo) {
1119
- headers.set("x-simulated-trading", "1");
1118
+ if (!this.config.apiKey || !this.config.secretKey || !this.config.passphrase) {
1119
+ throw new ConfigError(
1120
+ "Invalid private API credentials state.",
1121
+ "Ensure all OKX credentials are set."
1122
+ );
1120
1123
  }
1121
- let response;
1122
- try {
1123
- response = await fetch(url, {
1124
- method: config.method,
1125
- headers,
1126
- body: config.method === "POST" ? bodyJson : void 0,
1127
- signal: AbortSignal.timeout(this.config.timeoutMs)
1128
- });
1129
- } catch (error) {
1130
- throw new NetworkError(
1131
- `Failed to call OKX endpoint ${config.method} ${requestPath}.`,
1132
- `${config.method} ${requestPath}`,
1133
- error
1124
+ const payload = `${timestamp}${method.toUpperCase()}${requestPath}${bodyJson}`;
1125
+ const signature = signOkxPayload(payload, this.config.secretKey);
1126
+ headers.set("OK-ACCESS-KEY", this.config.apiKey);
1127
+ headers.set("OK-ACCESS-SIGN", signature);
1128
+ headers.set("OK-ACCESS-PASSPHRASE", this.config.passphrase);
1129
+ headers.set("OK-ACCESS-TIMESTAMP", timestamp);
1130
+ }
1131
+ throwOkxError(code, msg, reqConfig, traceId) {
1132
+ const message = msg || "OKX API request failed.";
1133
+ const endpoint = `${reqConfig.method} ${reqConfig.path}`;
1134
+ if (code === "50111" || code === "50112" || code === "50113") {
1135
+ throw new AuthenticationError(
1136
+ message,
1137
+ "Check API key, secret, passphrase and permissions.",
1138
+ endpoint,
1139
+ traceId
1134
1140
  );
1135
1141
  }
1136
- const rawText = await response.text();
1137
- const traceId = extractTraceId(response.headers);
1142
+ const behavior = OKX_CODE_BEHAVIORS[code];
1143
+ const suggestion = behavior?.suggestion?.replace("{site}", this.config.site);
1144
+ if (code === "50011" || code === "50061") {
1145
+ throw new RateLimitError(message, suggestion, endpoint, traceId);
1146
+ }
1147
+ throw new OkxApiError(message, {
1148
+ code,
1149
+ endpoint,
1150
+ suggestion,
1151
+ traceId
1152
+ });
1153
+ }
1154
+ processResponse(rawText, response, elapsed, traceId, reqConfig, requestPath) {
1138
1155
  let parsed;
1139
1156
  try {
1140
1157
  parsed = rawText ? JSON.parse(rawText) : {};
1141
1158
  } catch (error) {
1159
+ this.logResponse(response.status, rawText.length, elapsed, traceId, "non-JSON");
1142
1160
  if (!response.ok) {
1143
1161
  const messagePreview = rawText.slice(0, 160).replace(/\s+/g, " ").trim();
1144
1162
  throw new OkxApiError(
1145
1163
  `HTTP ${response.status} from OKX: ${messagePreview || "Non-JSON response body"}`,
1146
1164
  {
1147
1165
  code: String(response.status),
1148
- endpoint: `${config.method} ${config.path}`,
1166
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1149
1167
  suggestion: "Verify endpoint path and request parameters.",
1150
1168
  traceId
1151
1169
  }
1152
1170
  );
1153
1171
  }
1154
1172
  throw new NetworkError(
1155
- `OKX returned non-JSON response for ${config.method} ${requestPath}.`,
1156
- `${config.method} ${requestPath}`,
1173
+ `OKX returned non-JSON response for ${reqConfig.method} ${requestPath}.`,
1174
+ `${reqConfig.method} ${requestPath}`,
1157
1175
  error
1158
1176
  );
1159
1177
  }
1160
1178
  if (!response.ok) {
1179
+ this.logResponse(response.status, rawText.length, elapsed, traceId, parsed.code ?? "-", parsed.msg);
1161
1180
  throw new OkxApiError(
1162
1181
  `HTTP ${response.status} from OKX: ${parsed.msg ?? "Unknown error"}`,
1163
1182
  {
1164
1183
  code: String(response.status),
1165
- endpoint: `${config.method} ${config.path}`,
1184
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1166
1185
  suggestion: "Retry later or verify endpoint parameters.",
1167
1186
  traceId
1168
1187
  }
1169
1188
  );
1170
1189
  }
1171
1190
  const responseCode = parsed.code;
1191
+ this.logResponse(response.status, rawText.length, elapsed, traceId, responseCode, parsed.msg);
1172
1192
  if (responseCode && responseCode !== "0" && responseCode !== "1") {
1173
- const message = parsed.msg || "OKX API request failed.";
1174
- const endpoint = `${config.method} ${config.path}`;
1175
- if (responseCode === "50111" || responseCode === "50112" || responseCode === "50113") {
1176
- throw new AuthenticationError(
1177
- message,
1178
- "Check API key, secret, passphrase and permissions.",
1179
- endpoint,
1180
- traceId
1181
- );
1182
- }
1183
- const behavior = OKX_CODE_BEHAVIORS[responseCode];
1184
- const suggestion = behavior?.suggestion?.replace("{site}", this.config.site);
1185
- if (responseCode === "50011" || responseCode === "50061") {
1186
- throw new RateLimitError(message, suggestion, endpoint, traceId);
1187
- }
1188
- throw new OkxApiError(message, {
1189
- code: responseCode,
1190
- endpoint,
1191
- suggestion,
1192
- traceId
1193
- });
1193
+ this.throwOkxError(responseCode, parsed.msg, reqConfig, traceId);
1194
1194
  }
1195
1195
  return {
1196
- endpoint: `${config.method} ${config.path}`,
1196
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1197
1197
  requestTime: (/* @__PURE__ */ new Date()).toISOString(),
1198
1198
  data: parsed.data ?? null,
1199
1199
  raw: parsed
1200
1200
  };
1201
1201
  }
1202
+ async request(reqConfig) {
1203
+ const queryString = buildQueryString(reqConfig.query);
1204
+ const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
1205
+ const url = `${this.config.baseUrl}${requestPath}`;
1206
+ const bodyJson = reqConfig.body ? JSON.stringify(reqConfig.body) : "";
1207
+ const timestamp = getNow();
1208
+ this.logRequest(reqConfig.method, url, reqConfig.auth);
1209
+ if (reqConfig.rateLimit) {
1210
+ await this.rateLimiter.consume(reqConfig.rateLimit);
1211
+ }
1212
+ const headers = new Headers({
1213
+ "Content-Type": "application/json",
1214
+ Accept: "application/json"
1215
+ });
1216
+ if (this.config.userAgent) {
1217
+ headers.set("User-Agent", this.config.userAgent);
1218
+ }
1219
+ if (reqConfig.auth === "private") {
1220
+ this.setAuthHeaders(headers, reqConfig.method, requestPath, bodyJson, timestamp);
1221
+ }
1222
+ if (this.config.demo) {
1223
+ headers.set("x-simulated-trading", "1");
1224
+ }
1225
+ const t0 = Date.now();
1226
+ let response;
1227
+ try {
1228
+ response = await fetch(url, {
1229
+ method: reqConfig.method,
1230
+ headers,
1231
+ body: reqConfig.method === "POST" ? bodyJson : void 0,
1232
+ signal: AbortSignal.timeout(this.config.timeoutMs)
1233
+ });
1234
+ } catch (error) {
1235
+ if (this.config.verbose) {
1236
+ const elapsed2 = Date.now() - t0;
1237
+ const cause = error instanceof Error ? error.message : String(error);
1238
+ vlog(`\u2717 NetworkError after ${elapsed2}ms: ${cause}`);
1239
+ }
1240
+ throw new NetworkError(
1241
+ `Failed to call OKX endpoint ${reqConfig.method} ${requestPath}.`,
1242
+ `${reqConfig.method} ${requestPath}`,
1243
+ error
1244
+ );
1245
+ }
1246
+ const rawText = await response.text();
1247
+ const elapsed = Date.now() - t0;
1248
+ const traceId = extractTraceId(response.headers);
1249
+ return this.processResponse(rawText, response, elapsed, traceId, reqConfig, requestPath);
1250
+ }
1202
1251
  };
1203
1252
  var DEFAULT_SOURCE_TAG = "MCP";
1204
1253
  var OKX_SITES = {
@@ -1224,6 +1273,10 @@ var BOT_SUB_MODULE_IDS = [
1224
1273
  "bot.dca"
1225
1274
  ];
1226
1275
  var BOT_DEFAULT_SUB_MODULES = ["bot.grid"];
1276
+ var EARN_SUB_MODULE_IDS = [
1277
+ "earn.savings",
1278
+ "earn.onchain"
1279
+ ];
1227
1280
  var MODULES = [
1228
1281
  "market",
1229
1282
  "spot",
@@ -1231,6 +1284,7 @@ var MODULES = [
1231
1284
  "futures",
1232
1285
  "option",
1233
1286
  "account",
1287
+ ...EARN_SUB_MODULE_IDS,
1234
1288
  ...BOT_SUB_MODULE_IDS
1235
1289
  ];
1236
1290
  var DEFAULT_MODULES = ["spot", "swap", "option", "account", ...BOT_DEFAULT_SUB_MODULES];
@@ -1296,6 +1350,13 @@ function compactObject(object) {
1296
1350
  }
1297
1351
  return next;
1298
1352
  }
1353
+ function normalizeResponse(response) {
1354
+ return {
1355
+ endpoint: response.endpoint,
1356
+ requestTime: response.requestTime,
1357
+ data: response.data
1358
+ };
1359
+ }
1299
1360
  var OKX_CANDLE_BARS = [
1300
1361
  "1m",
1301
1362
  "3m",
@@ -1335,6 +1396,14 @@ function privateRateLimit(key, rps = 10) {
1335
1396
  refillPerSecond: rps
1336
1397
  };
1337
1398
  }
1399
+ function assertNotDemo(config, endpoint) {
1400
+ if (config.demo) {
1401
+ throw new ConfigError(
1402
+ `"${endpoint}" is not supported in simulated trading mode.`,
1403
+ "Disable demo mode (remove OKX_DEMO=1 or --demo flag) to use this endpoint."
1404
+ );
1405
+ }
1406
+ }
1338
1407
  function normalize(response) {
1339
1408
  return {
1340
1409
  endpoint: response.endpoint,
@@ -2821,6 +2890,246 @@ function registerBotTools() {
2821
2890
  ...registerDcaTools()
2822
2891
  ];
2823
2892
  }
2893
+ function registerEarnTools() {
2894
+ return [
2895
+ {
2896
+ name: "earn_get_savings_balance",
2897
+ module: "earn.savings",
2898
+ description: "Get Simple Earn (savings/flexible earn) balance. Returns current holdings for all currencies or a specific one. Private endpoint. Rate limit: 6 req/s.",
2899
+ isWrite: false,
2900
+ inputSchema: {
2901
+ type: "object",
2902
+ properties: {
2903
+ ccy: {
2904
+ type: "string",
2905
+ description: "e.g. USDT or BTC. Omit for all."
2906
+ }
2907
+ }
2908
+ },
2909
+ handler: async (rawArgs, context) => {
2910
+ const args = asRecord(rawArgs);
2911
+ const response = await context.client.privateGet(
2912
+ "/api/v5/finance/savings/balance",
2913
+ compactObject({ ccy: readString(args, "ccy") }),
2914
+ privateRateLimit("earn_get_savings_balance", 6)
2915
+ );
2916
+ return normalizeResponse(response);
2917
+ }
2918
+ },
2919
+ {
2920
+ name: "earn_savings_purchase",
2921
+ module: "earn.savings",
2922
+ description: "Purchase Simple Earn (savings/flexible earn). [CAUTION] Moves real funds into earn product. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 6 req/s.",
2923
+ isWrite: true,
2924
+ inputSchema: {
2925
+ type: "object",
2926
+ properties: {
2927
+ ccy: {
2928
+ type: "string",
2929
+ description: "Currency to purchase, e.g. USDT"
2930
+ },
2931
+ amt: {
2932
+ type: "string",
2933
+ description: "Purchase amount"
2934
+ },
2935
+ rate: {
2936
+ type: "string",
2937
+ description: "Lending rate. Annual rate in decimal, e.g. 0.01 = 1%. Defaults to 0.01 (1%, minimum rate, easiest to match)."
2938
+ }
2939
+ },
2940
+ required: ["ccy", "amt"]
2941
+ },
2942
+ handler: async (rawArgs, context) => {
2943
+ assertNotDemo(context.config, "earn_savings_purchase");
2944
+ const args = asRecord(rawArgs);
2945
+ const response = await context.client.privatePost(
2946
+ "/api/v5/finance/savings/purchase-redempt",
2947
+ compactObject({
2948
+ ccy: requireString(args, "ccy"),
2949
+ amt: requireString(args, "amt"),
2950
+ side: "purchase",
2951
+ rate: readString(args, "rate") ?? "0.01"
2952
+ }),
2953
+ privateRateLimit("earn_savings_purchase", 6)
2954
+ );
2955
+ return normalizeResponse(response);
2956
+ }
2957
+ },
2958
+ {
2959
+ name: "earn_savings_redeem",
2960
+ module: "earn.savings",
2961
+ description: "Redeem Simple Earn (savings/flexible earn). [CAUTION] Withdraws funds from earn product. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 6 req/s.",
2962
+ isWrite: true,
2963
+ inputSchema: {
2964
+ type: "object",
2965
+ properties: {
2966
+ ccy: {
2967
+ type: "string",
2968
+ description: "Currency to redeem, e.g. USDT"
2969
+ },
2970
+ amt: {
2971
+ type: "string",
2972
+ description: "Redemption amount"
2973
+ }
2974
+ },
2975
+ required: ["ccy", "amt"]
2976
+ },
2977
+ handler: async (rawArgs, context) => {
2978
+ assertNotDemo(context.config, "earn_savings_redeem");
2979
+ const args = asRecord(rawArgs);
2980
+ const response = await context.client.privatePost(
2981
+ "/api/v5/finance/savings/purchase-redempt",
2982
+ compactObject({
2983
+ ccy: requireString(args, "ccy"),
2984
+ amt: requireString(args, "amt"),
2985
+ side: "redempt"
2986
+ }),
2987
+ privateRateLimit("earn_savings_redeem", 6)
2988
+ );
2989
+ return normalizeResponse(response);
2990
+ }
2991
+ },
2992
+ {
2993
+ name: "earn_set_lending_rate",
2994
+ module: "earn.savings",
2995
+ description: "Set lending rate for Simple Earn. [CAUTION] Changes your lending rate preference. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 6 req/s.",
2996
+ isWrite: true,
2997
+ inputSchema: {
2998
+ type: "object",
2999
+ properties: {
3000
+ ccy: {
3001
+ type: "string",
3002
+ description: "Currency, e.g. USDT"
3003
+ },
3004
+ rate: {
3005
+ type: "string",
3006
+ description: "Lending rate. Annual rate in decimal, e.g. 0.01 = 1%"
3007
+ }
3008
+ },
3009
+ required: ["ccy", "rate"]
3010
+ },
3011
+ handler: async (rawArgs, context) => {
3012
+ assertNotDemo(context.config, "earn_set_lending_rate");
3013
+ const args = asRecord(rawArgs);
3014
+ const response = await context.client.privatePost(
3015
+ "/api/v5/finance/savings/set-lending-rate",
3016
+ {
3017
+ ccy: requireString(args, "ccy"),
3018
+ rate: requireString(args, "rate")
3019
+ },
3020
+ privateRateLimit("earn_set_lending_rate", 6)
3021
+ );
3022
+ return normalizeResponse(response);
3023
+ }
3024
+ },
3025
+ {
3026
+ name: "earn_get_lending_history",
3027
+ module: "earn.savings",
3028
+ description: "Get lending history for Simple Earn. Returns lending records with details like amount, rate, and earnings. Private endpoint. Rate limit: 6 req/s.",
3029
+ isWrite: false,
3030
+ inputSchema: {
3031
+ type: "object",
3032
+ properties: {
3033
+ ccy: {
3034
+ type: "string",
3035
+ description: "e.g. USDT. Omit for all."
3036
+ },
3037
+ after: {
3038
+ type: "string",
3039
+ description: "Pagination: before this record ID"
3040
+ },
3041
+ before: {
3042
+ type: "string",
3043
+ description: "Pagination: after this record ID"
3044
+ },
3045
+ limit: {
3046
+ type: "number",
3047
+ description: "Max results (default 100)"
3048
+ }
3049
+ }
3050
+ },
3051
+ handler: async (rawArgs, context) => {
3052
+ const args = asRecord(rawArgs);
3053
+ const response = await context.client.privateGet(
3054
+ "/api/v5/finance/savings/lending-history",
3055
+ compactObject({
3056
+ ccy: readString(args, "ccy"),
3057
+ after: readString(args, "after"),
3058
+ before: readString(args, "before"),
3059
+ limit: readNumber(args, "limit")
3060
+ }),
3061
+ privateRateLimit("earn_get_lending_history", 6)
3062
+ );
3063
+ return normalizeResponse(response);
3064
+ }
3065
+ },
3066
+ {
3067
+ name: "earn_get_lending_rate_summary",
3068
+ module: "earn.savings",
3069
+ description: "Get market lending rate summary for Simple Earn. Public endpoint (no API key required). Returns current lending rates, estimated APY, and available amounts. Rate limit: 6 req/s.",
3070
+ isWrite: false,
3071
+ inputSchema: {
3072
+ type: "object",
3073
+ properties: {
3074
+ ccy: {
3075
+ type: "string",
3076
+ description: "e.g. USDT. Omit for all."
3077
+ }
3078
+ }
3079
+ },
3080
+ handler: async (rawArgs, context) => {
3081
+ const args = asRecord(rawArgs);
3082
+ const response = await context.client.publicGet(
3083
+ "/api/v5/finance/savings/lending-rate-summary",
3084
+ compactObject({ ccy: readString(args, "ccy") }),
3085
+ publicRateLimit("earn_get_lending_rate_summary", 6)
3086
+ );
3087
+ return normalizeResponse(response);
3088
+ }
3089
+ },
3090
+ {
3091
+ name: "earn_get_lending_rate_history",
3092
+ module: "earn.savings",
3093
+ description: "Get historical lending rates for Simple Earn. Public endpoint (no API key required). Returns past lending rate data for trend analysis. Rate limit: 6 req/s.",
3094
+ isWrite: false,
3095
+ inputSchema: {
3096
+ type: "object",
3097
+ properties: {
3098
+ ccy: {
3099
+ type: "string",
3100
+ description: "e.g. USDT. Omit for all."
3101
+ },
3102
+ after: {
3103
+ type: "string",
3104
+ description: "Pagination: before this timestamp (ms)"
3105
+ },
3106
+ before: {
3107
+ type: "string",
3108
+ description: "Pagination: after this timestamp (ms)"
3109
+ },
3110
+ limit: {
3111
+ type: "number",
3112
+ description: "Max results (default 100)"
3113
+ }
3114
+ }
3115
+ },
3116
+ handler: async (rawArgs, context) => {
3117
+ const args = asRecord(rawArgs);
3118
+ const response = await context.client.publicGet(
3119
+ "/api/v5/finance/savings/lending-rate-history",
3120
+ compactObject({
3121
+ ccy: readString(args, "ccy"),
3122
+ after: readString(args, "after"),
3123
+ before: readString(args, "before"),
3124
+ limit: readNumber(args, "limit")
3125
+ }),
3126
+ publicRateLimit("earn_get_lending_rate_history", 6)
3127
+ );
3128
+ return normalizeResponse(response);
3129
+ }
3130
+ }
3131
+ ];
3132
+ }
2824
3133
  var FUTURES_INST_TYPES = ["FUTURES", "SWAP"];
2825
3134
  function normalize5(response) {
2826
3135
  return {
@@ -3086,102 +3395,374 @@ function registerFuturesTools() {
3086
3395
  properties: {
3087
3396
  instType: {
3088
3397
  type: "string",
3089
- enum: [...FUTURES_INST_TYPES],
3090
- description: "FUTURES (default) or SWAP"
3398
+ enum: [...FUTURES_INST_TYPES],
3399
+ description: "FUTURES (default) or SWAP"
3400
+ },
3401
+ instId: {
3402
+ type: "string",
3403
+ description: "e.g. BTC-USDT-240329"
3404
+ },
3405
+ posId: {
3406
+ type: "string"
3407
+ }
3408
+ }
3409
+ },
3410
+ handler: async (rawArgs, context) => {
3411
+ const args = asRecord(rawArgs);
3412
+ const instType = readString(args, "instType") ?? "FUTURES";
3413
+ assertEnum(instType, "instType", FUTURES_INST_TYPES);
3414
+ const response = await context.client.privateGet(
3415
+ "/api/v5/account/positions",
3416
+ compactObject({
3417
+ instType,
3418
+ instId: readString(args, "instId"),
3419
+ posId: readString(args, "posId")
3420
+ }),
3421
+ privateRateLimit("futures_get_positions", 10)
3422
+ );
3423
+ return normalize5(response);
3424
+ }
3425
+ },
3426
+ {
3427
+ name: "futures_get_fills",
3428
+ module: "futures",
3429
+ description: "Get FUTURES fill details. archive=false: last 3 days. archive=true: up to 3 months. Private. Rate limit: 20 req/s.",
3430
+ isWrite: false,
3431
+ inputSchema: {
3432
+ type: "object",
3433
+ properties: {
3434
+ archive: {
3435
+ type: "boolean",
3436
+ description: "true=up to 3 months; false=last 3 days (default)"
3437
+ },
3438
+ instType: {
3439
+ type: "string",
3440
+ enum: [...FUTURES_INST_TYPES],
3441
+ description: "FUTURES (default) or SWAP"
3442
+ },
3443
+ instId: {
3444
+ type: "string",
3445
+ description: "Instrument ID filter"
3446
+ },
3447
+ ordId: {
3448
+ type: "string",
3449
+ description: "Order ID filter"
3450
+ },
3451
+ after: {
3452
+ type: "string",
3453
+ description: "Pagination: before this bill ID"
3454
+ },
3455
+ before: {
3456
+ type: "string",
3457
+ description: "Pagination: after this bill ID"
3458
+ },
3459
+ begin: {
3460
+ type: "string",
3461
+ description: "Start time (ms)"
3462
+ },
3463
+ end: {
3464
+ type: "string",
3465
+ description: "End time (ms)"
3466
+ },
3467
+ limit: {
3468
+ type: "number",
3469
+ description: "Max results (default 100 or 20 for archive)"
3470
+ }
3471
+ }
3472
+ },
3473
+ handler: async (rawArgs, context) => {
3474
+ const args = asRecord(rawArgs);
3475
+ const archive = readBoolean(args, "archive") ?? false;
3476
+ const instType = readString(args, "instType") ?? "FUTURES";
3477
+ assertEnum(instType, "instType", FUTURES_INST_TYPES);
3478
+ const path4 = archive ? "/api/v5/trade/fills-history" : "/api/v5/trade/fills";
3479
+ const response = await context.client.privateGet(
3480
+ path4,
3481
+ compactObject({
3482
+ instType,
3483
+ instId: readString(args, "instId"),
3484
+ ordId: readString(args, "ordId"),
3485
+ after: readString(args, "after"),
3486
+ before: readString(args, "before"),
3487
+ begin: readString(args, "begin"),
3488
+ end: readString(args, "end"),
3489
+ limit: readNumber(args, "limit") ?? (archive ? 20 : void 0)
3490
+ }),
3491
+ privateRateLimit("futures_get_fills", 20)
3492
+ );
3493
+ return normalize5(response);
3494
+ }
3495
+ }
3496
+ ];
3497
+ }
3498
+ function registerOnchainEarnTools() {
3499
+ return [
3500
+ // -------------------------------------------------------------------------
3501
+ // Get Offers
3502
+ // -------------------------------------------------------------------------
3503
+ {
3504
+ name: "onchain_earn_get_offers",
3505
+ module: "earn.onchain",
3506
+ description: "Get available on-chain earn (staking/DeFi) offers. Returns investment products with APY, terms, and limits. Private endpoint. Rate limit: 3 req/s.",
3507
+ isWrite: false,
3508
+ inputSchema: {
3509
+ type: "object",
3510
+ properties: {
3511
+ productId: {
3512
+ type: "string",
3513
+ description: "Specific product ID to query. Omit for all offers."
3514
+ },
3515
+ protocolType: {
3516
+ type: "string",
3517
+ description: "Protocol type filter: staking, defi. Omit for all types."
3518
+ },
3519
+ ccy: {
3520
+ type: "string",
3521
+ description: "Currency filter, e.g. ETH. Omit for all currencies."
3522
+ }
3523
+ }
3524
+ },
3525
+ handler: async (rawArgs, context) => {
3526
+ const args = asRecord(rawArgs);
3527
+ const response = await context.client.privateGet(
3528
+ "/api/v5/finance/staking-defi/offers",
3529
+ compactObject({
3530
+ productId: readString(args, "productId"),
3531
+ protocolType: readString(args, "protocolType"),
3532
+ ccy: readString(args, "ccy")
3533
+ }),
3534
+ privateRateLimit("onchain_earn_get_offers", 3)
3535
+ );
3536
+ return normalizeResponse(response);
3537
+ }
3538
+ },
3539
+ // -------------------------------------------------------------------------
3540
+ // Purchase
3541
+ // -------------------------------------------------------------------------
3542
+ {
3543
+ name: "onchain_earn_purchase",
3544
+ module: "earn.onchain",
3545
+ description: "Purchase on-chain earn (staking/DeFi) product. [CAUTION] Moves real funds into staking/DeFi product. Not supported in demo/simulated trading mode. Private endpoint. Rate limit: 2 req/s.",
3546
+ isWrite: true,
3547
+ inputSchema: {
3548
+ type: "object",
3549
+ properties: {
3550
+ productId: {
3551
+ type: "string",
3552
+ description: "Product ID to purchase"
3553
+ },
3554
+ investData: {
3555
+ type: "array",
3556
+ description: "Investment data array: [{ccy, amt}]. Each item specifies currency and amount.",
3557
+ items: {
3558
+ type: "object",
3559
+ properties: {
3560
+ ccy: { type: "string", description: "Currency, e.g. ETH" },
3561
+ amt: { type: "string", description: "Amount to invest" }
3562
+ },
3563
+ required: ["ccy", "amt"]
3564
+ }
3565
+ },
3566
+ term: {
3567
+ type: "string",
3568
+ description: "Investment term in days. Required for fixed-term products."
3569
+ },
3570
+ tag: {
3571
+ type: "string",
3572
+ description: "Order tag for tracking (optional)."
3573
+ }
3574
+ },
3575
+ required: ["productId", "investData"]
3576
+ },
3577
+ handler: async (rawArgs, context) => {
3578
+ assertNotDemo(context.config, "onchain_earn_purchase");
3579
+ const args = asRecord(rawArgs);
3580
+ const response = await context.client.privatePost(
3581
+ "/api/v5/finance/staking-defi/purchase",
3582
+ compactObject({
3583
+ productId: requireString(args, "productId"),
3584
+ investData: args.investData,
3585
+ term: readString(args, "term"),
3586
+ tag: readString(args, "tag")
3587
+ }),
3588
+ privateRateLimit("onchain_earn_purchase", 2)
3589
+ );
3590
+ return normalizeResponse(response);
3591
+ }
3592
+ },
3593
+ // -------------------------------------------------------------------------
3594
+ // Redeem
3595
+ // -------------------------------------------------------------------------
3596
+ {
3597
+ name: "onchain_earn_redeem",
3598
+ module: "earn.onchain",
3599
+ description: "Redeem on-chain earn (staking/DeFi) investment. [CAUTION] Withdraws funds from staking/DeFi product. Some products may have lock periods. Not supported in demo mode. Private endpoint. Rate limit: 2 req/s.",
3600
+ isWrite: true,
3601
+ inputSchema: {
3602
+ type: "object",
3603
+ properties: {
3604
+ ordId: {
3605
+ type: "string",
3606
+ description: "Order ID to redeem"
3607
+ },
3608
+ protocolType: {
3609
+ type: "string",
3610
+ description: "Protocol type: staking, defi"
3611
+ },
3612
+ allowEarlyRedeem: {
3613
+ type: "boolean",
3614
+ description: "Allow early redemption for fixed-term products (may incur penalties). Default false."
3615
+ }
3616
+ },
3617
+ required: ["ordId", "protocolType"]
3618
+ },
3619
+ handler: async (rawArgs, context) => {
3620
+ assertNotDemo(context.config, "onchain_earn_redeem");
3621
+ const args = asRecord(rawArgs);
3622
+ const response = await context.client.privatePost(
3623
+ "/api/v5/finance/staking-defi/redeem",
3624
+ compactObject({
3625
+ ordId: requireString(args, "ordId"),
3626
+ protocolType: requireString(args, "protocolType"),
3627
+ allowEarlyRedeem: readBoolean(args, "allowEarlyRedeem")
3628
+ }),
3629
+ privateRateLimit("onchain_earn_redeem", 2)
3630
+ );
3631
+ return normalizeResponse(response);
3632
+ }
3633
+ },
3634
+ // -------------------------------------------------------------------------
3635
+ // Cancel
3636
+ // -------------------------------------------------------------------------
3637
+ {
3638
+ name: "onchain_earn_cancel",
3639
+ module: "earn.onchain",
3640
+ description: "Cancel pending on-chain earn purchase. [CAUTION] Cancels a pending investment order. Not supported in demo mode. Private endpoint. Rate limit: 2 req/s.",
3641
+ isWrite: true,
3642
+ inputSchema: {
3643
+ type: "object",
3644
+ properties: {
3645
+ ordId: {
3646
+ type: "string",
3647
+ description: "Order ID to cancel"
3648
+ },
3649
+ protocolType: {
3650
+ type: "string",
3651
+ description: "Protocol type: staking, defi"
3652
+ }
3653
+ },
3654
+ required: ["ordId", "protocolType"]
3655
+ },
3656
+ handler: async (rawArgs, context) => {
3657
+ assertNotDemo(context.config, "onchain_earn_cancel");
3658
+ const args = asRecord(rawArgs);
3659
+ const response = await context.client.privatePost(
3660
+ "/api/v5/finance/staking-defi/cancel",
3661
+ {
3662
+ ordId: requireString(args, "ordId"),
3663
+ protocolType: requireString(args, "protocolType")
3664
+ },
3665
+ privateRateLimit("onchain_earn_cancel", 2)
3666
+ );
3667
+ return normalizeResponse(response);
3668
+ }
3669
+ },
3670
+ // -------------------------------------------------------------------------
3671
+ // Get Active Orders
3672
+ // -------------------------------------------------------------------------
3673
+ {
3674
+ name: "onchain_earn_get_active_orders",
3675
+ module: "earn.onchain",
3676
+ description: "Get active on-chain earn orders. Returns current staking/DeFi investments. Private endpoint. Rate limit: 3 req/s.",
3677
+ isWrite: false,
3678
+ inputSchema: {
3679
+ type: "object",
3680
+ properties: {
3681
+ productId: {
3682
+ type: "string",
3683
+ description: "Filter by product ID. Omit for all."
3091
3684
  },
3092
- instId: {
3685
+ protocolType: {
3093
3686
  type: "string",
3094
- description: "e.g. BTC-USDT-240329"
3687
+ description: "Filter by protocol type: staking, defi. Omit for all."
3095
3688
  },
3096
- posId: {
3097
- type: "string"
3689
+ ccy: {
3690
+ type: "string",
3691
+ description: "Filter by currency, e.g. ETH. Omit for all."
3692
+ },
3693
+ state: {
3694
+ type: "string",
3695
+ description: "Filter by state: 8 (pending), 13 (cancelling), 9 (onchain), 1 (earning), 2 (redeeming). Omit for all."
3098
3696
  }
3099
3697
  }
3100
3698
  },
3101
3699
  handler: async (rawArgs, context) => {
3102
3700
  const args = asRecord(rawArgs);
3103
- const instType = readString(args, "instType") ?? "FUTURES";
3104
- assertEnum(instType, "instType", FUTURES_INST_TYPES);
3105
3701
  const response = await context.client.privateGet(
3106
- "/api/v5/account/positions",
3702
+ "/api/v5/finance/staking-defi/orders-active",
3107
3703
  compactObject({
3108
- instType,
3109
- instId: readString(args, "instId"),
3110
- posId: readString(args, "posId")
3704
+ productId: readString(args, "productId"),
3705
+ protocolType: readString(args, "protocolType"),
3706
+ ccy: readString(args, "ccy"),
3707
+ state: readString(args, "state")
3111
3708
  }),
3112
- privateRateLimit("futures_get_positions", 10)
3709
+ privateRateLimit("onchain_earn_get_active_orders", 3)
3113
3710
  );
3114
- return normalize5(response);
3711
+ return normalizeResponse(response);
3115
3712
  }
3116
3713
  },
3714
+ // -------------------------------------------------------------------------
3715
+ // Get Order History
3716
+ // -------------------------------------------------------------------------
3117
3717
  {
3118
- name: "futures_get_fills",
3119
- module: "futures",
3120
- description: "Get FUTURES fill details. archive=false: last 3 days. archive=true: up to 3 months. Private. Rate limit: 20 req/s.",
3718
+ name: "onchain_earn_get_order_history",
3719
+ module: "earn.onchain",
3720
+ description: "Get on-chain earn order history. Returns past staking/DeFi investments including redeemed orders. Private endpoint. Rate limit: 3 req/s.",
3121
3721
  isWrite: false,
3122
3722
  inputSchema: {
3123
3723
  type: "object",
3124
3724
  properties: {
3125
- archive: {
3126
- type: "boolean",
3127
- description: "true=up to 3 months; false=last 3 days (default)"
3128
- },
3129
- instType: {
3725
+ productId: {
3130
3726
  type: "string",
3131
- enum: [...FUTURES_INST_TYPES],
3132
- description: "FUTURES (default) or SWAP"
3727
+ description: "Filter by product ID. Omit for all."
3133
3728
  },
3134
- instId: {
3729
+ protocolType: {
3135
3730
  type: "string",
3136
- description: "Instrument ID filter"
3731
+ description: "Filter by protocol type: staking, defi. Omit for all."
3137
3732
  },
3138
- ordId: {
3733
+ ccy: {
3139
3734
  type: "string",
3140
- description: "Order ID filter"
3735
+ description: "Filter by currency, e.g. ETH. Omit for all."
3141
3736
  },
3142
3737
  after: {
3143
3738
  type: "string",
3144
- description: "Pagination: before this bill ID"
3739
+ description: "Pagination: return results before this order ID"
3145
3740
  },
3146
3741
  before: {
3147
3742
  type: "string",
3148
- description: "Pagination: after this bill ID"
3149
- },
3150
- begin: {
3151
- type: "string",
3152
- description: "Start time (ms)"
3153
- },
3154
- end: {
3155
- type: "string",
3156
- description: "End time (ms)"
3743
+ description: "Pagination: return results after this order ID"
3157
3744
  },
3158
3745
  limit: {
3159
- type: "number",
3160
- description: "Max results (default 100 or 20 for archive)"
3746
+ type: "string",
3747
+ description: "Max results to return (default 100, max 100)"
3161
3748
  }
3162
3749
  }
3163
3750
  },
3164
3751
  handler: async (rawArgs, context) => {
3165
3752
  const args = asRecord(rawArgs);
3166
- const archive = readBoolean(args, "archive") ?? false;
3167
- const instType = readString(args, "instType") ?? "FUTURES";
3168
- assertEnum(instType, "instType", FUTURES_INST_TYPES);
3169
- const path4 = archive ? "/api/v5/trade/fills-history" : "/api/v5/trade/fills";
3170
3753
  const response = await context.client.privateGet(
3171
- path4,
3754
+ "/api/v5/finance/staking-defi/orders-history",
3172
3755
  compactObject({
3173
- instType,
3174
- instId: readString(args, "instId"),
3175
- ordId: readString(args, "ordId"),
3756
+ productId: readString(args, "productId"),
3757
+ protocolType: readString(args, "protocolType"),
3758
+ ccy: readString(args, "ccy"),
3176
3759
  after: readString(args, "after"),
3177
3760
  before: readString(args, "before"),
3178
- begin: readString(args, "begin"),
3179
- end: readString(args, "end"),
3180
- limit: readNumber(args, "limit") ?? (archive ? 20 : void 0)
3761
+ limit: readString(args, "limit")
3181
3762
  }),
3182
- privateRateLimit("futures_get_fills", 20)
3763
+ privateRateLimit("onchain_earn_get_order_history", 3)
3183
3764
  );
3184
- return normalize5(response);
3765
+ return normalizeResponse(response);
3185
3766
  }
3186
3767
  }
3187
3768
  ];
@@ -5390,6 +5971,8 @@ function allToolSpecs() {
5390
5971
  ...registerAlgoTradeTools(),
5391
5972
  ...registerAccountTools(),
5392
5973
  ...registerBotTools(),
5974
+ ...registerEarnTools(),
5975
+ ...registerOnchainEarnTools(),
5393
5976
  ...registerAuditTools()
5394
5977
  ];
5395
5978
  }
@@ -5427,8 +6010,15 @@ function writeFullConfig(config) {
5427
6010
  writeFileSync(path4, stringify(config), "utf-8");
5428
6011
  }
5429
6012
  var BASE_MODULES = MODULES.filter(
5430
- (m) => !BOT_SUB_MODULE_IDS.includes(m)
6013
+ (m) => !BOT_SUB_MODULE_IDS.includes(m) && !EARN_SUB_MODULE_IDS.includes(m)
5431
6014
  );
6015
+ function expandShorthand(moduleId) {
6016
+ if (moduleId === "all") return [...BASE_MODULES, ...BOT_SUB_MODULE_IDS];
6017
+ if (moduleId === "earn" || moduleId === "earn.all") return [...EARN_SUB_MODULE_IDS];
6018
+ if (moduleId === "bot") return [...BOT_DEFAULT_SUB_MODULES];
6019
+ if (moduleId === "bot.all") return [...BOT_SUB_MODULE_IDS];
6020
+ return null;
6021
+ }
5432
6022
  function parseModuleList(rawModules) {
5433
6023
  if (!rawModules || rawModules.trim().length === 0) {
5434
6024
  return [...DEFAULT_MODULES];
@@ -5437,32 +6027,28 @@ function parseModuleList(rawModules) {
5437
6027
  if (trimmed === "all") {
5438
6028
  return [...BASE_MODULES, ...BOT_SUB_MODULE_IDS];
5439
6029
  }
5440
- const requested = trimmed.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
6030
+ const requested = trimmed.split(",").map((s) => s.trim()).filter(Boolean);
5441
6031
  if (requested.length === 0) {
5442
6032
  return [...DEFAULT_MODULES];
5443
6033
  }
5444
6034
  const deduped = /* @__PURE__ */ new Set();
5445
6035
  for (const moduleId of requested) {
5446
- if (moduleId === "bot") {
5447
- for (const sub of BOT_DEFAULT_SUB_MODULES) deduped.add(sub);
5448
- continue;
5449
- }
5450
- if (moduleId === "bot.all") {
5451
- for (const sub of BOT_SUB_MODULE_IDS) deduped.add(sub);
6036
+ const expanded = expandShorthand(moduleId);
6037
+ if (expanded) {
6038
+ expanded.forEach((sub) => deduped.add(sub));
5452
6039
  continue;
5453
6040
  }
5454
6041
  if (!MODULES.includes(moduleId)) {
5455
6042
  throw new ConfigError(
5456
6043
  `Unknown module "${moduleId}".`,
5457
- `Use one of: ${MODULES.join(", ")}, "bot", "bot.all", or "all".`
6044
+ `Use one of: ${MODULES.join(", ")}, "earn", "earn.all", "bot", "bot.all", or "all".`
5458
6045
  );
5459
6046
  }
5460
6047
  deduped.add(moduleId);
5461
6048
  }
5462
6049
  return Array.from(deduped);
5463
6050
  }
5464
- function loadConfig(cli) {
5465
- const toml = readTomlProfile(cli.profile);
6051
+ function loadCredentials(toml) {
5466
6052
  const apiKey = process.env.OKX_API_KEY?.trim() ?? toml.api_key;
5467
6053
  const secretKey = process.env.OKX_SECRET_KEY?.trim() ?? toml.secret_key;
5468
6054
  const passphrase = process.env.OKX_PASSPHRASE?.trim() ?? toml.passphrase;
@@ -5474,23 +6060,34 @@ function loadConfig(cli) {
5474
6060
  "Set OKX_API_KEY, OKX_SECRET_KEY and OKX_PASSPHRASE together (env vars or config.toml profile)."
5475
6061
  );
5476
6062
  }
5477
- const demo = cli.demo || process.env.OKX_DEMO === "1" || process.env.OKX_DEMO === "true" || (toml.demo ?? false);
5478
- const rawSite = cli.site?.trim() ?? process.env.OKX_SITE?.trim() ?? toml.site ?? "global";
6063
+ return { apiKey, secretKey, passphrase, hasAuth };
6064
+ }
6065
+ function resolveSite(cliSite, tomlSite) {
6066
+ const rawSite = cliSite?.trim() ?? process.env.OKX_SITE?.trim() ?? tomlSite ?? "global";
5479
6067
  if (!SITE_IDS.includes(rawSite)) {
5480
6068
  throw new ConfigError(
5481
6069
  `Unknown site "${rawSite}".`,
5482
6070
  `Use one of: ${SITE_IDS.join(", ")}.`
5483
6071
  );
5484
6072
  }
5485
- const site = rawSite;
5486
- const rawBaseUrl = process.env.OKX_API_BASE_URL?.trim() ?? toml.base_url ?? OKX_SITES[site].apiBaseUrl;
6073
+ return rawSite;
6074
+ }
6075
+ function resolveBaseUrl(site, tomlBaseUrl) {
6076
+ const rawBaseUrl = process.env.OKX_API_BASE_URL?.trim() ?? tomlBaseUrl ?? OKX_SITES[site].apiBaseUrl;
5487
6077
  if (!rawBaseUrl.startsWith("http://") && !rawBaseUrl.startsWith("https://")) {
5488
6078
  throw new ConfigError(
5489
6079
  `Invalid base URL "${rawBaseUrl}".`,
5490
6080
  "OKX_API_BASE_URL must start with http:// or https://"
5491
6081
  );
5492
6082
  }
5493
- const baseUrl = rawBaseUrl.replace(/\/+$/, "");
6083
+ return rawBaseUrl.replace(/\/+$/, "");
6084
+ }
6085
+ function loadConfig(cli) {
6086
+ const toml = readTomlProfile(cli.profile);
6087
+ const creds = loadCredentials(toml);
6088
+ const demo = cli.demo || process.env.OKX_DEMO === "1" || process.env.OKX_DEMO === "true" || (toml.demo ?? false);
6089
+ const site = resolveSite(cli.site, toml.site);
6090
+ const baseUrl = resolveBaseUrl(site, toml.base_url);
5494
6091
  const rawTimeout = process.env.OKX_TIMEOUT_MS ? Number(process.env.OKX_TIMEOUT_MS) : toml.timeout_ms ?? 15e3;
5495
6092
  if (!Number.isFinite(rawTimeout) || rawTimeout <= 0) {
5496
6093
  throw new ConfigError(
@@ -5499,10 +6096,7 @@ function loadConfig(cli) {
5499
6096
  );
5500
6097
  }
5501
6098
  return {
5502
- apiKey,
5503
- secretKey,
5504
- passphrase,
5505
- hasAuth,
6099
+ ...creds,
5506
6100
  baseUrl,
5507
6101
  timeoutMs: Math.floor(rawTimeout),
5508
6102
  modules: parseModuleList(cli.modules),
@@ -5510,7 +6104,8 @@ function loadConfig(cli) {
5510
6104
  demo,
5511
6105
  site,
5512
6106
  userAgent: cli.userAgent,
5513
- sourceTag: cli.sourceTag ?? DEFAULT_SOURCE_TAG
6107
+ sourceTag: cli.sourceTag ?? DEFAULT_SOURCE_TAG,
6108
+ verbose: cli.verbose ?? false
5514
6109
  };
5515
6110
  }
5516
6111
  var CACHE_FILE = join2(homedir2(), ".okx", "update-check.json");
@@ -5726,6 +6321,313 @@ function runSetup(options) {
5726
6321
  }
5727
6322
  }
5728
6323
 
6324
+ // src/commands/diagnose.ts
6325
+ import dns from "dns/promises";
6326
+ import net from "net";
6327
+ import os2 from "os";
6328
+ import tls from "tls";
6329
+ import { createRequire } from "module";
6330
+ var _require = createRequire(import.meta.url);
6331
+ function readCliVersion() {
6332
+ for (const rel of ["../package.json", "../../package.json"]) {
6333
+ try {
6334
+ return _require(rel).version;
6335
+ } catch (_err) {
6336
+ }
6337
+ }
6338
+ return "0.0.0";
6339
+ }
6340
+ var CLI_VERSION = readCliVersion();
6341
+ var GIT_HASH = true ? "7777cc2" : "dev";
6342
+ var Report = class {
6343
+ lines = [];
6344
+ add(key, value) {
6345
+ this.lines.push({ key, value });
6346
+ }
6347
+ print() {
6348
+ const w = process.stdout.write.bind(process.stdout);
6349
+ const sep = "\u2500".repeat(52);
6350
+ w(`
6351
+ \u2500\u2500 Diagnostic Report (copy & share) ${sep.slice(35)}
6352
+ `);
6353
+ for (const { key, value } of this.lines) {
6354
+ w(` ${key.padEnd(14)} ${value}
6355
+ `);
6356
+ }
6357
+ w(` ${sep}
6358
+
6359
+ `);
6360
+ }
6361
+ };
6362
+ function ok(label, detail) {
6363
+ process.stdout.write(` \u2713 ${label.padEnd(14)} ${detail}
6364
+ `);
6365
+ }
6366
+ function fail(label, detail, hints) {
6367
+ process.stdout.write(` \u2717 ${label.padEnd(14)} ${detail}
6368
+ `);
6369
+ for (const hint of hints) {
6370
+ process.stdout.write(` \u2192 ${hint}
6371
+ `);
6372
+ }
6373
+ }
6374
+ function section(title) {
6375
+ process.stdout.write(`
6376
+ ${title}
6377
+ `);
6378
+ }
6379
+ function maskKey2(key) {
6380
+ if (!key) return "(not set)";
6381
+ if (key.length <= 8) return "****";
6382
+ return `${key.slice(0, 2)}****${key.slice(-2)}`;
6383
+ }
6384
+ async function checkDns(hostname) {
6385
+ const t0 = Date.now();
6386
+ try {
6387
+ const addresses = await dns.resolve4(hostname);
6388
+ return { ok: true, ip: addresses[0], ms: Date.now() - t0 };
6389
+ } catch (e) {
6390
+ return { ok: false, ms: Date.now() - t0, error: e instanceof Error ? e.message : String(e) };
6391
+ }
6392
+ }
6393
+ async function checkSocket(createFn, successEvent, timeoutMs) {
6394
+ const t0 = Date.now();
6395
+ return new Promise((resolve) => {
6396
+ const socket = createFn();
6397
+ const cleanup = () => {
6398
+ socket.removeAllListeners();
6399
+ socket.destroy();
6400
+ };
6401
+ socket.once(successEvent, () => {
6402
+ cleanup();
6403
+ resolve({ ok: true, ms: Date.now() - t0 });
6404
+ });
6405
+ socket.once("timeout", () => {
6406
+ cleanup();
6407
+ resolve({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
6408
+ });
6409
+ socket.once("error", (err) => {
6410
+ cleanup();
6411
+ resolve({ ok: false, ms: Date.now() - t0, error: err.message });
6412
+ });
6413
+ });
6414
+ }
6415
+ async function checkTcp(hostname, port, timeoutMs = 5e3) {
6416
+ return checkSocket(
6417
+ () => net.createConnection({ host: hostname, port, timeout: timeoutMs }),
6418
+ "connect",
6419
+ timeoutMs
6420
+ );
6421
+ }
6422
+ async function checkTls(hostname, port, timeoutMs = 5e3) {
6423
+ return checkSocket(
6424
+ () => tls.connect({ host: hostname, port, timeout: timeoutMs, servername: hostname }),
6425
+ "secureConnect",
6426
+ timeoutMs
6427
+ );
6428
+ }
6429
+ function checkProxyEnv(report) {
6430
+ const httpProxy = process.env.HTTP_PROXY ?? process.env.http_proxy;
6431
+ const httpsProxy = process.env.HTTPS_PROXY ?? process.env.https_proxy;
6432
+ const noProxy = process.env.NO_PROXY ?? process.env.no_proxy;
6433
+ if (httpProxy || httpsProxy) {
6434
+ ok("HTTP_PROXY", httpProxy ?? "(not set)");
6435
+ ok("HTTPS_PROXY", httpsProxy ?? "(not set)");
6436
+ if (noProxy) ok("NO_PROXY", noProxy);
6437
+ report.add("http_proxy", httpProxy ?? "-");
6438
+ report.add("https_proxy", httpsProxy ?? "-");
6439
+ if (noProxy) report.add("no_proxy", noProxy);
6440
+ } else {
6441
+ ok("Proxy", "(none)");
6442
+ report.add("proxy", "none");
6443
+ }
6444
+ }
6445
+ function checkEnvironment(report) {
6446
+ let passed = true;
6447
+ section("Environment");
6448
+ const nodeVersion = process.version;
6449
+ const nodeMajor = parseInt(nodeVersion.slice(1), 10);
6450
+ if (nodeMajor >= 18) {
6451
+ ok("Node.js", `${nodeVersion} (>= 18 required)`);
6452
+ } else {
6453
+ fail("Node.js", `${nodeVersion} (>= 18 required)`, ["Upgrade Node.js to v18 or later"]);
6454
+ passed = false;
6455
+ }
6456
+ ok("CLI", `v${CLI_VERSION} (${GIT_HASH})`);
6457
+ ok("OS", `${process.platform} ${process.arch}`);
6458
+ ok("OS release", os2.release());
6459
+ ok("Shell", process.env.SHELL ?? "(unknown)");
6460
+ ok("Locale", `${process.env.LANG ?? process.env.LC_ALL ?? "(unknown)"}`);
6461
+ ok("Timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
6462
+ report.add("cli", `${CLI_VERSION} (${GIT_HASH})`);
6463
+ report.add("node", `${nodeVersion} ${process.platform} ${process.arch}`);
6464
+ const machine = typeof os2.machine === "function" ? os2.machine() : process.arch;
6465
+ report.add("os", `${os2.type()} ${os2.release()} ${machine}`);
6466
+ report.add("shell", process.env.SHELL ?? "-");
6467
+ report.add("locale", process.env.LANG ?? process.env.LC_ALL ?? "-");
6468
+ report.add("tz", Intl.DateTimeFormat().resolvedOptions().timeZone);
6469
+ checkProxyEnv(report);
6470
+ return passed;
6471
+ }
6472
+ function checkConfig(config, profile, report) {
6473
+ let passed = true;
6474
+ section(`Config (profile: ${profile})`);
6475
+ if (config.hasAuth) {
6476
+ ok("API key", maskKey2(config.apiKey));
6477
+ ok("Secret", "****");
6478
+ ok("Passphrase", "****");
6479
+ } else {
6480
+ fail("Credentials", "not configured", [
6481
+ "Set OKX_API_KEY, OKX_SECRET_KEY, OKX_PASSPHRASE env vars",
6482
+ "Or run: okx config init"
6483
+ ]);
6484
+ passed = false;
6485
+ }
6486
+ ok("Demo mode", String(config.demo));
6487
+ ok("Site", config.site);
6488
+ ok("Base URL", config.baseUrl);
6489
+ ok("Timeout", `${config.timeoutMs}ms`);
6490
+ report.add("profile", profile);
6491
+ report.add("site", config.site);
6492
+ report.add("base", config.baseUrl);
6493
+ report.add("auth", config.hasAuth ? `true (key=${maskKey2(config.apiKey)})` : "false");
6494
+ report.add("demo", String(config.demo));
6495
+ report.add("timeout", `${config.timeoutMs}ms`);
6496
+ return passed;
6497
+ }
6498
+ async function checkTcpTls(hostname, port, protocol, report) {
6499
+ let passed = true;
6500
+ const tcpResult = await checkTcp(hostname, port);
6501
+ if (tcpResult.ok) {
6502
+ ok("TCP connect", `port ${port} (${tcpResult.ms}ms)`);
6503
+ report.add("tcp", `${port} OK (${tcpResult.ms}ms)`);
6504
+ } else {
6505
+ fail("TCP connect", `port ${port} \u2014 ${tcpResult.error}`, [
6506
+ "Check firewall/proxy/VPN settings",
6507
+ `Try: nc -zv ${hostname} ${port}`
6508
+ ]);
6509
+ report.add("tcp", `FAIL ${port} ${tcpResult.error} (${tcpResult.ms}ms)`);
6510
+ return false;
6511
+ }
6512
+ if (protocol === "https:") {
6513
+ const tlsResult = await checkTls(hostname, port);
6514
+ if (tlsResult.ok) {
6515
+ ok("TLS handshake", `(${tlsResult.ms}ms)`);
6516
+ report.add("tls", `OK (${tlsResult.ms}ms)`);
6517
+ } else {
6518
+ fail("TLS handshake", tlsResult.error ?? "failed", [
6519
+ "Check system certificates or proxy MITM settings"
6520
+ ]);
6521
+ passed = false;
6522
+ report.add("tls", `FAIL ${tlsResult.error} (${tlsResult.ms}ms)`);
6523
+ }
6524
+ }
6525
+ return passed;
6526
+ }
6527
+ async function checkNetwork(config, client, report) {
6528
+ let passed = true;
6529
+ section("Network");
6530
+ const url = new URL(config.baseUrl);
6531
+ const hostname = url.hostname;
6532
+ const defaultPort = url.protocol === "https:" ? 443 : 80;
6533
+ const port = url.port ? parseInt(url.port, 10) : defaultPort;
6534
+ const dnsResult = await checkDns(hostname);
6535
+ if (dnsResult.ok) {
6536
+ ok("DNS resolve", `${hostname} \u2192 ${dnsResult.ip} (${dnsResult.ms}ms)`);
6537
+ report.add("dns", `${hostname} \u2192 ${dnsResult.ip} (${dnsResult.ms}ms)`);
6538
+ } else {
6539
+ fail("DNS resolve", `${hostname} \u2014 ${dnsResult.error}`, [
6540
+ "Check DNS settings or network connection",
6541
+ `Try: nslookup ${hostname}`
6542
+ ]);
6543
+ passed = false;
6544
+ report.add("dns", `FAIL ${hostname} ${dnsResult.error} (${dnsResult.ms}ms)`);
6545
+ }
6546
+ if (dnsResult.ok) {
6547
+ const tcpTlsPassed = await checkTcpTls(hostname, port, url.protocol, report);
6548
+ if (!tcpTlsPassed) passed = false;
6549
+ }
6550
+ const t0 = Date.now();
6551
+ try {
6552
+ await client.publicGet("/api/v5/public/time");
6553
+ const ms = Date.now() - t0;
6554
+ ok("API /public/time", `200 (${ms}ms)`);
6555
+ report.add("api", `/public/time 200 (${ms}ms)`);
6556
+ } catch (e) {
6557
+ const ms = Date.now() - t0;
6558
+ const msg = e instanceof Error ? e.message : String(e);
6559
+ fail("API /public/time", msg, [
6560
+ "OKX API may be down or blocked in your network",
6561
+ `Try: curl ${config.baseUrl}/api/v5/public/time`
6562
+ ]);
6563
+ passed = false;
6564
+ report.add("api", `FAIL /public/time ${msg} (${ms}ms)`);
6565
+ }
6566
+ return passed;
6567
+ }
6568
+ function getAuthHints(msg) {
6569
+ if (msg.includes("50111") || msg.includes("Invalid OK-ACCESS-KEY")) {
6570
+ return ["API key is invalid or expired", "Regenerate at https://www.okx.com/account/my-api"];
6571
+ }
6572
+ if (msg.includes("50112") || msg.includes("Invalid Sign")) {
6573
+ return ["Secret key or passphrase may be wrong", "Regenerate API key at https://www.okx.com/account/my-api"];
6574
+ }
6575
+ if (msg.includes("50113")) {
6576
+ return ["Passphrase is incorrect"];
6577
+ }
6578
+ if (msg.includes("50100")) {
6579
+ return ["API key lacks required permissions", "Update permissions at https://www.okx.com/account/my-api"];
6580
+ }
6581
+ return ["Check API credentials and permissions"];
6582
+ }
6583
+ async function checkAuth(client, config, report) {
6584
+ if (!config.hasAuth) {
6585
+ report.add("auth_api", "skipped (no credentials)");
6586
+ return true;
6587
+ }
6588
+ let passed = true;
6589
+ section("Authentication");
6590
+ const t1 = Date.now();
6591
+ try {
6592
+ await client.privateGet("/api/v5/account/balance");
6593
+ const ms = Date.now() - t1;
6594
+ ok("Account balance", `200 (${ms}ms)`);
6595
+ if (config.demo) {
6596
+ ok("Demo header", "x-simulated-trading: 1");
6597
+ }
6598
+ report.add("auth_api", `/account/balance 200 (${ms}ms)`);
6599
+ } catch (e) {
6600
+ const ms = Date.now() - t1;
6601
+ const msg = e instanceof Error ? e.message : String(e);
6602
+ const hints = getAuthHints(msg);
6603
+ fail("Account balance", msg, hints);
6604
+ passed = false;
6605
+ report.add("auth_api", `FAIL /account/balance ${msg} (${ms}ms)`);
6606
+ }
6607
+ return passed;
6608
+ }
6609
+ async function cmdDiagnose(config, profile) {
6610
+ process.stdout.write("\n OKX Trade CLI Diagnostics\n");
6611
+ process.stdout.write(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
6612
+ const report = new Report();
6613
+ report.add("ts", (/* @__PURE__ */ new Date()).toISOString());
6614
+ const envPassed = checkEnvironment(report);
6615
+ const cfgPassed = checkConfig(config, profile, report);
6616
+ const client = new OkxRestClient(config);
6617
+ const netPassed = await checkNetwork(config, client, report);
6618
+ const authPassed = await checkAuth(client, config, report);
6619
+ const allPassed = envPassed && cfgPassed && netPassed && authPassed;
6620
+ process.stdout.write("\n");
6621
+ if (allPassed) {
6622
+ process.stdout.write(" Result: All checks passed \u2713\n");
6623
+ } else {
6624
+ process.stdout.write(" Result: Some checks failed \u2717\n");
6625
+ process.exitCode = 1;
6626
+ }
6627
+ report.add("result", allPassed ? "PASS" : "FAIL");
6628
+ report.print();
6629
+ }
6630
+
5729
6631
  // src/config/loader.ts
5730
6632
  function loadProfileConfig(opts) {
5731
6633
  return loadConfig({
@@ -5735,32 +6637,11 @@ function loadProfileConfig(opts) {
5735
6637
  demo: opts.demo ?? false,
5736
6638
  site: opts.site,
5737
6639
  userAgent: opts.userAgent,
5738
- sourceTag: opts.sourceTag
6640
+ sourceTag: opts.sourceTag,
6641
+ verbose: opts.verbose
5739
6642
  });
5740
6643
  }
5741
6644
 
5742
- // src/tips.ts
5743
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
5744
- import { join as join4 } from "path";
5745
- import { homedir as homedir4 } from "os";
5746
- function showFirstRunTips(version) {
5747
- const okxDir = join4(homedir4(), ".okx");
5748
- const marker = join4(okxDir, ".tips-shown");
5749
- if (existsSync4(marker)) return;
5750
- const w = (s) => process.stderr.write(s);
5751
- w("\n");
5752
- w(` @okx_ai/okx-trade-cli v${version}
5753
- `);
5754
- w(" \u26A0\uFE0F Security Tips: NEVER send API keys in agent chat. Create a dedicated sub-account for your agent. Test on demo before going live.\n");
5755
- w(" \u26A0\uFE0F \u5B89\u5168\u63D0\u793A\uFF1A\u5207\u52FF\u5728Agent\u5BF9\u8BDD\u4E2D\u53D1\u9001API Key\u3002\u8BF7\u521B\u5EFAAgent\u4E13\u7528\u5B50\u8D26\u6237\u63A5\u5165\u3002\u5148\u5728\u6A21\u62DF\u76D8\u5145\u5206\u6D4B\u8BD5\uFF0C\u518D\u63A5\u5165\u5B9E\u76D8\u3002\n");
5756
- w("\n");
5757
- try {
5758
- mkdirSync4(okxDir, { recursive: true });
5759
- writeFileSync4(marker, (/* @__PURE__ */ new Date()).toISOString() + "\n");
5760
- } catch {
5761
- }
5762
- }
5763
-
5764
6645
  // src/commands/client-setup.ts
5765
6646
  import * as fs2 from "fs";
5766
6647
  var DETECTABLE_CLIENTS = ["claude-desktop", "cursor", "windsurf"];
@@ -5916,8 +6797,8 @@ var HELP_TREE = {
5916
6797
  description: "Get trade fill history for spot orders"
5917
6798
  },
5918
6799
  place: {
5919
- usage: "okx spot place --instId <id> --side <buy|sell> --ordType <type> --sz <n> [--px <price>] [--tdMode <cash|cross|isolated>]",
5920
- description: "Place a new spot order"
6800
+ usage: "okx spot place --instId <id> --side <buy|sell> --ordType <type> --sz <n> [--px <price>] [--tdMode <cash|cross|isolated>] [--tpTriggerPx <price>] [--tpOrdPx <price|-1>] [--slTriggerPx <price>] [--slOrdPx <price|-1>]",
6801
+ description: "Place a new spot order (supports attached TP/SL)"
5921
6802
  },
5922
6803
  amend: {
5923
6804
  usage: "okx spot amend --instId <id> --ordId <id> [--newSz <n>] [--newPx <price>]",
@@ -5976,8 +6857,8 @@ var HELP_TREE = {
5976
6857
  description: "Get trade fill history for swap orders"
5977
6858
  },
5978
6859
  place: {
5979
- usage: "okx swap place --instId <id> --side <buy|sell> --ordType <type> --sz <n> [--posSide <side>] [--px <price>] [--tdMode <cross|isolated>]",
5980
- description: "Place a new perpetual swap order"
6860
+ usage: "okx swap place --instId <id> --side <buy|sell> --ordType <type> --sz <n> [--posSide <side>] [--px <price>] [--tdMode <cross|isolated>] [--tpTriggerPx <price>] [--tpOrdPx <price|-1>] [--slTriggerPx <price>] [--slOrdPx <price|-1>]",
6861
+ description: "Place a new perpetual swap order (supports attached TP/SL)"
5981
6862
  },
5982
6863
  cancel: {
5983
6864
  usage: "okx swap cancel <instId> --ordId <id>",
@@ -6048,8 +6929,8 @@ var HELP_TREE = {
6048
6929
  description: "Get trade fill history for futures orders"
6049
6930
  },
6050
6931
  place: {
6051
- usage: "okx futures place --instId <id> --side <buy|sell> --ordType <type> --sz <n>\n [--tdMode <cross|isolated>] [--posSide <net|long|short>] [--px <price>] [--reduceOnly]",
6052
- description: "Place a new futures order"
6932
+ usage: "okx futures place --instId <id> --side <buy|sell> --ordType <type> --sz <n>\n [--tdMode <cross|isolated>] [--posSide <net|long|short>] [--px <price>] [--reduceOnly]\n [--tpTriggerPx <price>] [--tpOrdPx <price|-1>] [--slTriggerPx <price>] [--slOrdPx <price|-1>]",
6933
+ description: "Place a new futures order (supports attached TP/SL)"
6053
6934
  },
6054
6935
  cancel: {
6055
6936
  usage: "okx futures cancel <instId> --ordId <id>",
@@ -6106,6 +6987,73 @@ var HELP_TREE = {
6106
6987
  }
6107
6988
  }
6108
6989
  },
6990
+ earn: {
6991
+ description: "Earn products \u2014 Simple Earn (savings/lending) and On-chain Earn (staking/DeFi)",
6992
+ subgroups: {
6993
+ savings: {
6994
+ description: "Simple Earn \u2014 flexible savings and lending",
6995
+ commands: {
6996
+ balance: {
6997
+ usage: "okx earn savings balance [<ccy>]",
6998
+ description: "Get savings balance (optionally filter by currency)"
6999
+ },
7000
+ purchase: {
7001
+ usage: "okx earn savings purchase --ccy <ccy> --amt <n> [--rate <rate>]",
7002
+ description: "Purchase Simple Earn (flexible savings). Rate defaults to 0.01 (1%)"
7003
+ },
7004
+ redeem: {
7005
+ usage: "okx earn savings redeem --ccy <ccy> --amt <n>",
7006
+ description: "Redeem Simple Earn (flexible savings)"
7007
+ },
7008
+ "set-rate": {
7009
+ usage: "okx earn savings set-rate --ccy <ccy> --rate <rate>",
7010
+ description: "Set lending rate for a currency"
7011
+ },
7012
+ "lending-history": {
7013
+ usage: "okx earn savings lending-history [--ccy <ccy>] [--limit <n>]",
7014
+ description: "Get lending history"
7015
+ },
7016
+ "rate-summary": {
7017
+ usage: "okx earn savings rate-summary [<ccy>]",
7018
+ description: "Get market lending rate summary (public, no auth needed)"
7019
+ },
7020
+ "rate-history": {
7021
+ usage: "okx earn savings rate-history [--ccy <ccy>] [--limit <n>]",
7022
+ description: "Get historical lending rates (public, no auth needed)"
7023
+ }
7024
+ }
7025
+ },
7026
+ onchain: {
7027
+ description: "On-chain Earn \u2014 staking and DeFi products",
7028
+ commands: {
7029
+ offers: {
7030
+ usage: "okx earn onchain offers [--productId <id>] [--protocolType <type>] [--ccy <ccy>]",
7031
+ description: "Browse available on-chain earn products (staking, DeFi)"
7032
+ },
7033
+ purchase: {
7034
+ usage: "okx earn onchain purchase --productId <id> --ccy <ccy> --amt <n> [--term <term>] [--tag <tag>]",
7035
+ description: "Purchase an on-chain earn product (stake/deposit)"
7036
+ },
7037
+ redeem: {
7038
+ usage: "okx earn onchain redeem --ordId <id> --protocolType <type> [--allowEarlyRedeem]",
7039
+ description: "Redeem an on-chain earn position"
7040
+ },
7041
+ cancel: {
7042
+ usage: "okx earn onchain cancel --ordId <id> --protocolType <type>",
7043
+ description: "Cancel a pending on-chain earn order"
7044
+ },
7045
+ orders: {
7046
+ usage: "okx earn onchain orders [--productId <id>] [--protocolType <type>] [--ccy <ccy>] [--state <state>]",
7047
+ description: "List active on-chain earn orders"
7048
+ },
7049
+ history: {
7050
+ usage: "okx earn onchain history [--productId <id>] [--protocolType <type>] [--ccy <ccy>]",
7051
+ description: "Get on-chain earn order history"
7052
+ }
7053
+ }
7054
+ }
7055
+ }
7056
+ },
6109
7057
  bot: {
6110
7058
  description: "Trading bot strategies (grid, dca)",
6111
7059
  subgroups: {
@@ -6185,6 +7133,10 @@ var HELP_TREE = {
6185
7133
  setup: {
6186
7134
  description: "Set up client integrations (Cursor, Windsurf, Claude, etc.)",
6187
7135
  usage: `okx setup --client <${SUPPORTED_CLIENTS.join("|")}> [--profile <name>] [--modules <list>]`
7136
+ },
7137
+ diagnose: {
7138
+ description: "Run network diagnostics (DNS, TCP, TLS, API, auth)",
7139
+ usage: "okx diagnose [--profile <name>] [--demo]"
6188
7140
  }
6189
7141
  };
6190
7142
  function printGlobalHelp() {
@@ -6196,6 +7148,7 @@ function printGlobalHelp() {
6196
7148
  ` --profile <name> Use a named profile from ${configFilePath()}`,
6197
7149
  " --demo Use simulated trading (demo) mode",
6198
7150
  " --json Output raw JSON",
7151
+ " --verbose Show detailed network request/response info (stderr)",
6199
7152
  " --version, -v Show version",
6200
7153
  " --help Show this help",
6201
7154
  "",
@@ -6406,11 +7359,22 @@ var CLI_OPTIONS = {
6406
7359
  // batch
6407
7360
  action: { type: "string" },
6408
7361
  orders: { type: "string" },
7362
+ // earn
7363
+ rate: { type: "string" },
6409
7364
  // audit
6410
7365
  since: { type: "string" },
6411
7366
  tool: { type: "string" },
6412
7367
  // config profile
6413
- force: { type: "boolean", default: false }
7368
+ force: { type: "boolean", default: false },
7369
+ // onchain-earn
7370
+ productId: { type: "string" },
7371
+ protocolType: { type: "string" },
7372
+ term: { type: "string" },
7373
+ tag: { type: "string" },
7374
+ allowEarlyRedeem: { type: "boolean", default: false },
7375
+ state: { type: "string" },
7376
+ // diagnostics
7377
+ verbose: { type: "boolean", default: false }
6414
7378
  };
6415
7379
  function parseCli(argv) {
6416
7380
  const negated = /* @__PURE__ */ new Set();
@@ -6672,7 +7636,7 @@ async function cmdMarketCandles(run, instId, opts) {
6672
7636
  // src/commands/account.ts
6673
7637
  import * as fs4 from "fs";
6674
7638
  import * as path2 from "path";
6675
- import * as os2 from "os";
7639
+ import * as os4 from "os";
6676
7640
  function getData2(result) {
6677
7641
  return result.data;
6678
7642
  }
@@ -6878,7 +7842,7 @@ function readAuditLogs(logDir, days = 7) {
6878
7842
  return entries;
6879
7843
  }
6880
7844
  function cmdAccountAudit(opts) {
6881
- const logDir = path2.join(os2.homedir(), ".okx", "logs");
7845
+ const logDir = path2.join(os4.homedir(), ".okx", "logs");
6882
7846
  const limit = Math.min(Number(opts.limit) || 20, 100);
6883
7847
  let entries = readAuditLogs(logDir);
6884
7848
  if (opts.tool) entries = entries.filter((e) => e.tool === opts.tool);
@@ -6932,7 +7896,11 @@ async function cmdSpotPlace(run, opts) {
6932
7896
  side: opts.side,
6933
7897
  ordType: opts.ordType,
6934
7898
  sz: opts.sz,
6935
- px: opts.px
7899
+ px: opts.px,
7900
+ tpTriggerPx: opts.tpTriggerPx,
7901
+ tpOrdPx: opts.tpOrdPx,
7902
+ slTriggerPx: opts.slTriggerPx,
7903
+ slOrdPx: opts.slOrdPx
6936
7904
  });
6937
7905
  const data = getData3(result);
6938
7906
  if (opts.json) return printJson(data);
@@ -7157,7 +8125,11 @@ async function cmdSwapPlace(run, opts) {
7157
8125
  ordType: opts.ordType,
7158
8126
  sz: opts.sz,
7159
8127
  posSide: opts.posSide,
7160
- px: opts.px
8128
+ px: opts.px,
8129
+ tpTriggerPx: opts.tpTriggerPx,
8130
+ tpOrdPx: opts.tpOrdPx,
8131
+ slTriggerPx: opts.slTriggerPx,
8132
+ slOrdPx: opts.slOrdPx
7161
8133
  });
7162
8134
  const data = getData4(result);
7163
8135
  if (opts.json) return printJson(data);
@@ -7459,7 +8431,11 @@ async function cmdFuturesPlace(run, opts) {
7459
8431
  sz: opts.sz,
7460
8432
  posSide: opts.posSide,
7461
8433
  px: opts.px,
7462
- reduceOnly: opts.reduceOnly
8434
+ reduceOnly: opts.reduceOnly,
8435
+ tpTriggerPx: opts.tpTriggerPx,
8436
+ tpOrdPx: opts.tpOrdPx,
8437
+ slTriggerPx: opts.slTriggerPx,
8438
+ slOrdPx: opts.slOrdPx
7463
8439
  });
7464
8440
  const data = getData5(result);
7465
8441
  if (opts.json) return printJson(data);
@@ -8029,6 +9005,101 @@ function cmdConfigUse(profileName) {
8029
9005
  `);
8030
9006
  }
8031
9007
 
9008
+ // src/commands/earn.ts
9009
+ function extractData(result) {
9010
+ if (result && typeof result === "object") {
9011
+ const data = result["data"];
9012
+ if (Array.isArray(data)) return data;
9013
+ }
9014
+ return [];
9015
+ }
9016
+ function printDataList(data, json, emptyMsg, mapper) {
9017
+ if (json) {
9018
+ printJson(data);
9019
+ return;
9020
+ }
9021
+ if (!data.length) {
9022
+ process.stdout.write(emptyMsg + "\n");
9023
+ return;
9024
+ }
9025
+ printTable(data.map(mapper));
9026
+ }
9027
+ async function cmdEarnSavingsBalance(run, ccy, json) {
9028
+ const data = extractData(await run("earn_get_savings_balance", { ccy }));
9029
+ printDataList(data, json, "No savings balance", (r) => ({
9030
+ ccy: r["ccy"],
9031
+ amt: r["amt"],
9032
+ earnings: r["earnings"],
9033
+ rate: r["rate"],
9034
+ loanAmt: r["loanAmt"],
9035
+ pendingAmt: r["pendingAmt"]
9036
+ }));
9037
+ }
9038
+ async function cmdEarnSavingsPurchase(run, opts) {
9039
+ const data = extractData(await run("earn_savings_purchase", { ccy: opts.ccy, amt: opts.amt, rate: opts.rate }));
9040
+ if (opts.json) {
9041
+ printJson(data);
9042
+ return;
9043
+ }
9044
+ const r = data[0];
9045
+ if (!r) {
9046
+ process.stdout.write("No response data\n");
9047
+ return;
9048
+ }
9049
+ printKv({ ccy: r["ccy"], amt: r["amt"], side: r["side"], rate: r["rate"] });
9050
+ }
9051
+ async function cmdEarnSavingsRedeem(run, opts) {
9052
+ const data = extractData(await run("earn_savings_redeem", { ccy: opts.ccy, amt: opts.amt }));
9053
+ if (opts.json) {
9054
+ printJson(data);
9055
+ return;
9056
+ }
9057
+ const r = data[0];
9058
+ if (!r) {
9059
+ process.stdout.write("No response data\n");
9060
+ return;
9061
+ }
9062
+ printKv({ ccy: r["ccy"], amt: r["amt"], side: r["side"] });
9063
+ }
9064
+ async function cmdEarnSetLendingRate(run, opts) {
9065
+ const data = extractData(await run("earn_set_lending_rate", { ccy: opts.ccy, rate: opts.rate }));
9066
+ if (opts.json) {
9067
+ printJson(data);
9068
+ return;
9069
+ }
9070
+ const r = data[0];
9071
+ process.stdout.write(`Lending rate set: ${r?.["ccy"]} \u2192 ${r?.["rate"]}
9072
+ `);
9073
+ }
9074
+ async function cmdEarnLendingHistory(run, opts) {
9075
+ const data = extractData(await run("earn_get_lending_history", { ccy: opts.ccy, limit: opts.limit }));
9076
+ printDataList(data, opts.json, "No lending history", (r) => ({
9077
+ ccy: r["ccy"],
9078
+ amt: r["amt"],
9079
+ earnings: r["earnings"],
9080
+ rate: r["rate"],
9081
+ ts: new Date(Number(r["ts"])).toLocaleString()
9082
+ }));
9083
+ }
9084
+ async function cmdEarnLendingRateSummary(run, ccy, json) {
9085
+ const data = extractData(await run("earn_get_lending_rate_summary", { ccy }));
9086
+ printDataList(data, json, "No rate summary data", (r) => ({
9087
+ ccy: r["ccy"],
9088
+ avgRate: r["avgRate"],
9089
+ estRate: r["estRate"],
9090
+ avgAmt: r["avgAmt"]
9091
+ }));
9092
+ }
9093
+ async function cmdEarnLendingRateHistory(run, opts) {
9094
+ const data = extractData(await run("earn_get_lending_rate_history", { ccy: opts.ccy, limit: opts.limit }));
9095
+ printDataList(data, opts.json, "No rate history data", (r) => ({
9096
+ ccy: r["ccy"],
9097
+ lendingRate: r["lendingRate"],
9098
+ rate: r["rate"],
9099
+ ts: new Date(Number(r["ts"])).toLocaleString()
9100
+ }));
9101
+ }
9102
+
8032
9103
  // src/commands/bot.ts
8033
9104
  function getData7(result) {
8034
9105
  return result.data;
@@ -8260,10 +9331,56 @@ async function cmdDcaSubOrders(run, opts) {
8260
9331
  );
8261
9332
  }
8262
9333
 
9334
+ // src/commands/onchain-earn.ts
9335
+ function cmdOnchainEarnOffers(run, v) {
9336
+ return run("onchain_earn_get_offers", {
9337
+ productId: v.productId,
9338
+ protocolType: v.protocolType,
9339
+ ccy: v.ccy
9340
+ });
9341
+ }
9342
+ function cmdOnchainEarnPurchase(run, v) {
9343
+ const investData = v.ccy && v.amt ? [{ ccy: v.ccy, amt: v.amt }] : void 0;
9344
+ return run("onchain_earn_purchase", {
9345
+ productId: v.productId,
9346
+ investData,
9347
+ term: v.term,
9348
+ tag: v.tag
9349
+ });
9350
+ }
9351
+ function cmdOnchainEarnRedeem(run, v) {
9352
+ return run("onchain_earn_redeem", {
9353
+ ordId: v.ordId,
9354
+ protocolType: v.protocolType,
9355
+ allowEarlyRedeem: v.allowEarlyRedeem
9356
+ });
9357
+ }
9358
+ function cmdOnchainEarnCancel(run, v) {
9359
+ return run("onchain_earn_cancel", {
9360
+ ordId: v.ordId,
9361
+ protocolType: v.protocolType
9362
+ });
9363
+ }
9364
+ function cmdOnchainEarnActiveOrders(run, v) {
9365
+ return run("onchain_earn_get_active_orders", {
9366
+ productId: v.productId,
9367
+ protocolType: v.protocolType,
9368
+ ccy: v.ccy,
9369
+ state: v.state
9370
+ });
9371
+ }
9372
+ function cmdOnchainEarnOrderHistory(run, v) {
9373
+ return run("onchain_earn_get_order_history", {
9374
+ productId: v.productId,
9375
+ protocolType: v.protocolType,
9376
+ ccy: v.ccy
9377
+ });
9378
+ }
9379
+
8263
9380
  // src/index.ts
8264
- var _require = createRequire(import.meta.url);
8265
- var CLI_VERSION = _require("../package.json").version;
8266
- var GIT_HASH = true ? "c49054f" : "dev";
9381
+ var _require2 = createRequire2(import.meta.url);
9382
+ var CLI_VERSION2 = _require2("../package.json").version;
9383
+ var GIT_HASH2 = true ? "7777cc2" : "dev";
8267
9384
  function handleConfigCommand(action, rest, json, lang, force) {
8268
9385
  if (action === "init") return cmdConfigInit(lang === "zh" ? "zh" : "en");
8269
9386
  if (action === "show") return cmdConfigShow(json);
@@ -8435,6 +9552,10 @@ function handleSpotCommand(run, action, rest, v, json) {
8435
9552
  ordType: v.ordType,
8436
9553
  sz: v.sz,
8437
9554
  px: v.px,
9555
+ tpTriggerPx: v.tpTriggerPx,
9556
+ tpOrdPx: v.tpOrdPx,
9557
+ slTriggerPx: v.slTriggerPx,
9558
+ slOrdPx: v.slOrdPx,
8438
9559
  json
8439
9560
  });
8440
9561
  if (action === "cancel")
@@ -8531,6 +9652,10 @@ function handleSwapCommand(run, action, rest, v, json) {
8531
9652
  posSide: v.posSide,
8532
9653
  px: v.px,
8533
9654
  tdMode: v.tdMode ?? "cross",
9655
+ tpTriggerPx: v.tpTriggerPx,
9656
+ tpOrdPx: v.tpOrdPx,
9657
+ slTriggerPx: v.slTriggerPx,
9658
+ slOrdPx: v.slOrdPx,
8534
9659
  json
8535
9660
  });
8536
9661
  if (action === "cancel")
@@ -8625,6 +9750,10 @@ function handleFuturesCommand(run, action, rest, v, json) {
8625
9750
  posSide: v.posSide,
8626
9751
  px: v.px,
8627
9752
  reduceOnly: v.reduceOnly,
9753
+ tpTriggerPx: v.tpTriggerPx,
9754
+ tpOrdPx: v.tpOrdPx,
9755
+ slTriggerPx: v.slTriggerPx,
9756
+ slOrdPx: v.slOrdPx,
8628
9757
  json
8629
9758
  });
8630
9759
  if (action === "cancel")
@@ -8713,24 +9842,75 @@ function handleBotCommand(run, action, rest, v, json) {
8713
9842
  if (action === "grid") return handleBotGridCommand(run, v, rest, json);
8714
9843
  if (action === "dca") return handleBotDcaCommand(run, rest[0], v, json);
8715
9844
  }
9845
+ function handleEarnCommand(run, submodule, rest, v, json) {
9846
+ const action = rest[0];
9847
+ const innerRest = rest.slice(1);
9848
+ if (submodule === "savings") return handleEarnSavingsCommand(run, action, innerRest, v, json);
9849
+ if (submodule === "onchain") return handleEarnOnchainCommand(run, action, v, json);
9850
+ process.stderr.write(`Unknown earn sub-module: ${submodule}
9851
+ Valid: savings, onchain
9852
+ `);
9853
+ process.exitCode = 1;
9854
+ }
9855
+ function handleEarnSavingsCommand(run, action, rest, v, json) {
9856
+ const limit = v.limit !== void 0 ? Number(v.limit) : void 0;
9857
+ if (action === "balance") return cmdEarnSavingsBalance(run, rest[0] ?? v.ccy, json);
9858
+ if (action === "purchase") return cmdEarnSavingsPurchase(run, { ccy: v.ccy, amt: v.amt, rate: v.rate, json });
9859
+ if (action === "redeem") return cmdEarnSavingsRedeem(run, { ccy: v.ccy, amt: v.amt, json });
9860
+ if (action === "set-rate") return cmdEarnSetLendingRate(run, { ccy: v.ccy, rate: v.rate, json });
9861
+ if (action === "lending-history") return cmdEarnLendingHistory(run, { ccy: v.ccy, limit, json });
9862
+ if (action === "rate-summary") return cmdEarnLendingRateSummary(run, rest[0] ?? v.ccy, json);
9863
+ if (action === "rate-history") return cmdEarnLendingRateHistory(run, { ccy: v.ccy, limit, json });
9864
+ process.stderr.write(`Unknown earn savings command: ${action}
9865
+ `);
9866
+ process.exitCode = 1;
9867
+ }
9868
+ function handleEarnOnchainCommand(run, action, v, json) {
9869
+ if (action === "offers") return cmdOnchainEarnOffers(run, v).then((r) => outputResult(r, json));
9870
+ if (action === "purchase") return cmdOnchainEarnPurchase(run, v).then((r) => outputResult(r, json));
9871
+ if (action === "redeem") return cmdOnchainEarnRedeem(run, v).then((r) => outputResult(r, json));
9872
+ if (action === "cancel") return cmdOnchainEarnCancel(run, v).then((r) => outputResult(r, json));
9873
+ if (action === "orders") return cmdOnchainEarnActiveOrders(run, v).then((r) => outputResult(r, json));
9874
+ if (action === "history") return cmdOnchainEarnOrderHistory(run, v).then((r) => outputResult(r, json));
9875
+ process.stderr.write(`Unknown earn onchain command: ${action}
9876
+ `);
9877
+ process.exitCode = 1;
9878
+ }
9879
+ function outputResult(result, json) {
9880
+ if (json) {
9881
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
9882
+ } else {
9883
+ process.stdout.write(JSON.stringify(result.data, null, 2) + "\n");
9884
+ }
9885
+ }
9886
+ function printHelpForLevel(positionals) {
9887
+ const [module, subgroup] = positionals;
9888
+ if (!module) printHelp();
9889
+ else if (!subgroup) printHelp(module);
9890
+ else printHelp(module, subgroup);
9891
+ }
9892
+ function printVerboseConfigSummary(config, profile) {
9893
+ let authLabel = "\u2717";
9894
+ if (config.hasAuth && config.apiKey) {
9895
+ authLabel = `\u2713(${config.apiKey.slice(0, 3)}***${config.apiKey.slice(-3)})`;
9896
+ } else if (config.hasAuth) {
9897
+ authLabel = "\u2713";
9898
+ }
9899
+ process.stderr.write(
9900
+ `[verbose] config: profile=${profile ?? "default"} site=${config.site} base=${config.baseUrl} auth=${authLabel} demo=${config.demo ? "on" : "off"} modules=${config.modules.join(",")}
9901
+ `
9902
+ );
9903
+ }
8716
9904
  async function main() {
8717
- showFirstRunTips(CLI_VERSION);
8718
- checkForUpdates("@okx_ai/okx-trade-cli", CLI_VERSION);
9905
+ checkForUpdates("@okx_ai/okx-trade-cli", CLI_VERSION2);
8719
9906
  const { values, positionals } = parseCli(process.argv.slice(2));
8720
9907
  if (values.version) {
8721
- process.stdout.write(`${CLI_VERSION} (${GIT_HASH})
9908
+ process.stdout.write(`${CLI_VERSION2} (${GIT_HASH2})
8722
9909
  `);
8723
9910
  return;
8724
9911
  }
8725
9912
  if (values.help || positionals.length === 0) {
8726
- const [module2, subgroup] = positionals;
8727
- if (!module2) {
8728
- printHelp();
8729
- } else if (!subgroup) {
8730
- printHelp(module2);
8731
- } else {
8732
- printHelp(module2, subgroup);
8733
- }
9913
+ printHelpForLevel(positionals);
8734
9914
  return;
8735
9915
  }
8736
9916
  const [module, action, ...rest] = positionals;
@@ -8738,16 +9918,23 @@ async function main() {
8738
9918
  const json = v.json ?? false;
8739
9919
  if (module === "config") return handleConfigCommand(action, rest, json, v.lang, v.force);
8740
9920
  if (module === "setup") return handleSetupCommand(v);
8741
- const config = loadProfileConfig({ profile: v.profile, demo: v.demo, userAgent: `okx-trade-cli/${CLI_VERSION}`, sourceTag: "CLI" });
9921
+ const config = loadProfileConfig({ profile: v.profile, demo: v.demo, verbose: v.verbose, userAgent: `okx-trade-cli/${CLI_VERSION2}`, sourceTag: "CLI" });
9922
+ if (config.verbose) printVerboseConfigSummary(config, v.profile);
9923
+ if (module === "diagnose") return cmdDiagnose(config, v.profile ?? "default");
8742
9924
  const client = new OkxRestClient(config);
8743
9925
  const run = createToolRunner(client, config);
8744
- if (module === "market") return handleMarketCommand(run, action, rest, v, json);
8745
- if (module === "account") return handleAccountCommand(run, action, rest, v, json);
8746
- if (module === "spot") return handleSpotCommand(run, action, rest, v, json);
8747
- if (module === "swap") return handleSwapCommand(run, action, rest, v, json);
8748
- if (module === "futures") return handleFuturesCommand(run, action, rest, v, json);
8749
- if (module === "option") return handleOptionCommand(run, action, rest, v, json);
8750
- if (module === "bot") return handleBotCommand(run, action, rest, v, json);
9926
+ const moduleHandlers = {
9927
+ market: () => handleMarketCommand(run, action, rest, v, json),
9928
+ account: () => handleAccountCommand(run, action, rest, v, json),
9929
+ spot: () => handleSpotCommand(run, action, rest, v, json),
9930
+ swap: () => handleSwapCommand(run, action, rest, v, json),
9931
+ futures: () => handleFuturesCommand(run, action, rest, v, json),
9932
+ option: () => handleOptionCommand(run, action, rest, v, json),
9933
+ bot: () => handleBotCommand(run, action, rest, v, json),
9934
+ earn: () => handleEarnCommand(run, action, rest, v, json)
9935
+ };
9936
+ const handler = moduleHandlers[module];
9937
+ if (handler) return handler();
8751
9938
  process.stderr.write(`Unknown command: ${module} ${action ?? ""}
8752
9939
  `);
8753
9940
  process.exitCode = 1;
@@ -8760,7 +9947,7 @@ main().catch((error) => {
8760
9947
  `);
8761
9948
  if (payload.suggestion) process.stderr.write(`Hint: ${payload.suggestion}
8762
9949
  `);
8763
- process.stderr.write(`Version: @okx_ai/okx-trade-cli@${CLI_VERSION}
9950
+ process.stderr.write(`Version: @okx_ai/okx-trade-cli@${CLI_VERSION2}
8764
9951
  `);
8765
9952
  process.exitCode = 1;
8766
9953
  });
@@ -8770,6 +9957,7 @@ export {
8770
9957
  handleBotDcaCommand,
8771
9958
  handleBotGridCommand,
8772
9959
  handleConfigCommand,
9960
+ handleEarnCommand,
8773
9961
  handleMarketCommand,
8774
9962
  handleMarketDataCommand,
8775
9963
  handleMarketPublicCommand,