@okx_ai/okx-trade-cli 1.3.5 → 1.3.6-beta.2

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
@@ -4,6 +4,7 @@
4
4
  import { createRequire as createRequire3 } from "module";
5
5
 
6
6
  // ../core/dist/index.js
7
+ import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
7
8
  import { ProxyAgent } from "undici";
8
9
  import { isIP } from "net";
9
10
  import { lookup as dnsLookup } from "dns";
@@ -33,16 +34,22 @@ import os from "os";
33
34
  import { writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
34
35
  import { join as join4, resolve, basename, sep } from "path";
35
36
  import { randomUUID } from "crypto";
37
+ import { createHash } from "crypto";
38
+ import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
39
+ import { join as join5 } from "path";
40
+ import { createHash as createHash2, createPublicKey, verify as cryptoVerify } from "crypto";
41
+ import { readFileSync as readFileSync3 } from "fs";
42
+ import { join as join6, resolve as resolve2, sep as sep2 } from "path";
36
43
  import yauzl from "yauzl";
37
44
  import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
38
- import { resolve as resolve2, dirname as dirname2 } from "path";
39
- import { readFileSync as readFileSync2, existsSync } from "fs";
40
- import { join as join5 } from "path";
41
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync2 } from "fs";
42
- import { join as join6, dirname as dirname3 } from "path";
45
+ import { resolve as resolve3, dirname as dirname2 } from "path";
46
+ import { readFileSync as readFileSync4, existsSync } from "fs";
47
+ import { join as join7 } from "path";
48
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync2 } from "fs";
49
+ import { join as join8, dirname as dirname3 } from "path";
43
50
  import { homedir as homedir4 } from "os";
44
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync3 } from "fs";
45
- import { join as join7, dirname as dirname4 } from "path";
51
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync3 } from "fs";
52
+ import { join as join9, dirname as dirname4 } from "path";
46
53
  import { homedir as homedir5 } from "os";
47
54
 
48
55
  // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
@@ -120,7 +127,7 @@ function skipVoid(str, ptr, banNewLines, banComments) {
120
127
  ptr++;
121
128
  return banComments || c !== "#" ? ptr : skipVoid(str, skipComment(str, ptr), banNewLines);
122
129
  }
123
- function skipUntil(str, ptr, sep2, end, banNewLines = false) {
130
+ function skipUntil(str, ptr, sep3, end, banNewLines = false) {
124
131
  if (!end) {
125
132
  ptr = indexOfNewline(str, ptr);
126
133
  return ptr < 0 ? str.length : ptr;
@@ -129,7 +136,7 @@ function skipUntil(str, ptr, sep2, end, banNewLines = false) {
129
136
  let c = str[i];
130
137
  if (c === "#") {
131
138
  i = indexOfNewline(str, i);
132
- } else if (c === sep2) {
139
+ } else if (c === sep3) {
133
140
  return i + 1;
134
141
  } else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) {
135
142
  return i;
@@ -872,8 +879,8 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
872
879
  }
873
880
 
874
881
  // ../core/dist/index.js
875
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync4 } from "fs";
876
- import { join as join8 } from "path";
882
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync4 } from "fs";
883
+ import { join as join10 } from "path";
877
884
  import { homedir as homedir6 } from "os";
878
885
  import fs2 from "fs";
879
886
  import path2 from "path";
@@ -890,24 +897,37 @@ import {
890
897
  renameSync as renameSync4
891
898
  } from "fs";
892
899
  import { homedir as homedir9, platform as platform2 } from "os";
893
- import { join as join11, dirname as dirname7 } from "path";
900
+ import { join as join13, dirname as dirname7 } from "path";
894
901
  import { createWriteStream as createWriteStream2, unlinkSync as unlinkSync3 } from "fs";
895
902
  import { get as httpsGet } from "https";
896
903
  import { get as httpGet } from "http";
897
904
  import {
898
- readFileSync as readFileSync7,
905
+ readFileSync as readFileSync9,
899
906
  mkdirSync as mkdirSync8,
900
907
  chmodSync,
901
908
  existsSync as existsSync6,
902
909
  unlinkSync as unlinkSync4,
903
910
  renameSync as renameSync3
904
911
  } from "fs";
905
- import { createHash } from "crypto";
912
+ import { createHash as createHash3 } from "crypto";
906
913
  import { homedir as homedir8, platform, arch } from "os";
907
- import { join as join10, dirname as dirname6 } from "path";
908
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10, unlinkSync as unlinkSync6, existsSync as existsSync8 } from "fs";
909
- import { join as join12 } from "path";
914
+ import { join as join12, dirname as dirname6 } from "path";
915
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync10, unlinkSync as unlinkSync6, existsSync as existsSync8 } from "fs";
916
+ import { join as join14 } from "path";
910
917
  import { homedir as homedir10 } from "os";
918
+ function hasProxyEnv(env = process.env) {
919
+ return Boolean(
920
+ env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy
921
+ );
922
+ }
923
+ function installEnvProxyDispatcher(deps = {}) {
924
+ const env = deps.env ?? process.env;
925
+ if (!hasProxyEnv(env)) return false;
926
+ const register = deps.register ?? (() => setGlobalDispatcher(new EnvHttpProxyAgent()));
927
+ register();
928
+ return true;
929
+ }
930
+ installEnvProxyDispatcher();
911
931
  var EXEC_TIMEOUT_MS = 3e4;
912
932
  var ALLOWED_DOMAIN_RE = /^[\w.-]+\.okx\.com$/;
913
933
  var PILOT_BIN_DIR = join(homedir(), ".okx", "bin");
@@ -930,25 +950,25 @@ function execPilotBinary(domain, exclude = [], userAgent) {
930
950
  if (userAgent) {
931
951
  args.push("--user-agent", userAgent);
932
952
  }
933
- return new Promise((resolve3) => {
953
+ return new Promise((resolve4) => {
934
954
  execFile(
935
955
  binPath,
936
956
  args,
937
957
  { timeout: EXEC_TIMEOUT_MS, encoding: "utf-8" },
938
958
  (error, stdout) => {
939
959
  if (error) {
940
- resolve3(null);
960
+ resolve4(null);
941
961
  return;
942
962
  }
943
963
  try {
944
964
  const result = JSON.parse(stdout);
945
965
  if (result.code === 0 && result.data) {
946
- resolve3(result.data);
966
+ resolve4(result.data);
947
967
  } else {
948
- resolve3(null);
968
+ resolve4(null);
949
969
  }
950
970
  } catch {
951
- resolve3(null);
971
+ resolve4(null);
952
972
  }
953
973
  }
954
974
  );
@@ -1282,7 +1302,7 @@ var EXIT_CODES = {
1282
1302
  NOT_LOGGED_IN: 2,
1283
1303
  REFRESH_FAILED: 3
1284
1304
  };
1285
- function finalizeToken(code, token, resolve3, reject) {
1305
+ function finalizeToken(code, token, resolve4, reject) {
1286
1306
  if (code === EXIT_CODES.SUCCESS) {
1287
1307
  if (!token) {
1288
1308
  reject(new AuthenticationError(
@@ -1291,7 +1311,7 @@ function finalizeToken(code, token, resolve3, reject) {
1291
1311
  ));
1292
1312
  return;
1293
1313
  }
1294
- resolve3(token);
1314
+ resolve4(token);
1295
1315
  return;
1296
1316
  }
1297
1317
  if (code === EXIT_CODES.NOT_LOGGED_IN) {
@@ -1328,7 +1348,7 @@ function defaultWindowsPipeName() {
1328
1348
  return WIN_PIPE_PREFIX + randomBytes(32).toString("hex");
1329
1349
  }
1330
1350
  function execAuthTokenWindows(binPath, makePipeName = defaultWindowsPipeName) {
1331
- return new Promise((resolve3, reject) => {
1351
+ return new Promise((resolve4, reject) => {
1332
1352
  const pipeName = makePipeName();
1333
1353
  const server = createServer();
1334
1354
  const chunks = [];
@@ -1348,7 +1368,7 @@ function execAuthTokenWindows(binPath, makePipeName = defaultWindowsPipeName) {
1348
1368
  const tryFinalize = () => {
1349
1369
  if (!pipeClosed || exitCode === void 0) return;
1350
1370
  const token = Buffer.concat(chunks).toString("utf-8").trim();
1351
- settle(() => finalizeToken(exitCode, token, resolve3, reject));
1371
+ settle(() => finalizeToken(exitCode, token, resolve4, reject));
1352
1372
  };
1353
1373
  server.on("connection", (socket) => {
1354
1374
  connectionMade = true;
@@ -1395,7 +1415,7 @@ function execAuthToken() {
1395
1415
  return process.platform === "win32" ? execAuthTokenWindows(binPath) : execAuthTokenUnix(binPath);
1396
1416
  }
1397
1417
  function execAuthTokenUnix(binPath) {
1398
- return new Promise((resolve3, reject) => {
1418
+ return new Promise((resolve4, reject) => {
1399
1419
  const child = spawn2(binPath, ["token"], {
1400
1420
  stdio: ["ignore", "ignore", "inherit", "pipe"]
1401
1421
  // stdin stdout stderr fd3 (pipe)
@@ -1408,35 +1428,35 @@ function execAuthTokenUnix(binPath) {
1408
1428
  });
1409
1429
  child.on("close", (code) => {
1410
1430
  const token = Buffer.concat(chunks).toString("utf-8").trim();
1411
- finalizeToken(code, token, resolve3, reject);
1431
+ finalizeToken(code, token, resolve4, reject);
1412
1432
  });
1413
1433
  });
1414
1434
  }
1415
1435
  function execAuthStatus() {
1416
1436
  const binPath = getAuthBinaryPath();
1417
- return new Promise((resolve3) => {
1437
+ return new Promise((resolve4) => {
1418
1438
  execFile2(
1419
1439
  binPath,
1420
1440
  ["status", "--json"],
1421
1441
  { timeout: EXEC_TIMEOUT_MS2, encoding: "utf-8" },
1422
1442
  (error, stdout) => {
1423
1443
  if (error) {
1424
- resolve3(null);
1444
+ resolve4(null);
1425
1445
  return;
1426
1446
  }
1427
1447
  try {
1428
1448
  const result = JSON.parse(stdout);
1429
- resolve3(result);
1449
+ resolve4(result);
1430
1450
  } catch {
1431
- resolve3(null);
1451
+ resolve4(null);
1432
1452
  }
1433
1453
  }
1434
1454
  );
1435
1455
  });
1436
1456
  }
1437
1457
  function sleep(ms) {
1438
- return new Promise((resolve3) => {
1439
- setTimeout(resolve3, ms);
1458
+ return new Promise((resolve4) => {
1459
+ setTimeout(resolve4, ms);
1440
1460
  });
1441
1461
  }
1442
1462
  var RateLimiter = class {
@@ -2506,6 +2526,7 @@ function registerIndicatorTools() {
2506
2526
  return [
2507
2527
  {
2508
2528
  name: "market_get_indicator",
2529
+ title: "Get Technical Indicator",
2509
2530
  module: "market",
2510
2531
  description: "Get technical indicator values for an instrument. Common indicators: ma, ema, rsi, macd, bb (Bollinger), kdj, supertrend, ahr999. Call market_list_indicators first to see all valid names. No credentials required.",
2511
2532
  isWrite: false,
@@ -2579,6 +2600,7 @@ function registerIndicatorTools() {
2579
2600
  },
2580
2601
  {
2581
2602
  name: "market_list_indicators",
2603
+ title: "List Technical Indicators",
2582
2604
  module: "market",
2583
2605
  description: "List all supported technical indicator names and descriptions. Call this before market_get_indicator to discover valid indicator names. No credentials required.",
2584
2606
  isWrite: false,
@@ -2665,6 +2687,7 @@ function registerAccountTools() {
2665
2687
  return [
2666
2688
  {
2667
2689
  name: "account_get_balance",
2690
+ title: "Get Trading Account Balance",
2668
2691
  module: "account",
2669
2692
  description: "Get account balance for trading account. Returns balances for all currencies or a specific one.",
2670
2693
  isWrite: false,
@@ -2689,9 +2712,11 @@ function registerAccountTools() {
2689
2712
  },
2690
2713
  {
2691
2714
  name: "account_transfer",
2715
+ title: "Transfer Between Accounts",
2692
2716
  module: "account",
2693
2717
  description: "Transfer funds between accounts (trading, funding, etc.). [CAUTION] Moves real funds.",
2694
2718
  isWrite: true,
2719
+ destructiveHint: false,
2695
2720
  inputSchema: {
2696
2721
  type: "object",
2697
2722
  properties: {
@@ -2746,6 +2771,7 @@ function registerAccountTools() {
2746
2771
  },
2747
2772
  {
2748
2773
  name: "account_get_max_size",
2774
+ title: "Get Max Order Size",
2749
2775
  module: "account",
2750
2776
  description: "Get max buy/sell order size for a SWAP/FUTURES instrument given current balance and leverage. Useful before placing orders.",
2751
2777
  isWrite: false,
@@ -2793,6 +2819,7 @@ function registerAccountTools() {
2793
2819
  },
2794
2820
  {
2795
2821
  name: "account_get_asset_balance",
2822
+ title: "Get Funding Account Balance",
2796
2823
  module: "account",
2797
2824
  description: "Get funding account balance (asset account). Different from account_get_balance which queries the trading account. Optionally includes total asset valuation across all account types (trading, funding, earn, etc.).",
2798
2825
  isWrite: false,
@@ -2856,6 +2883,7 @@ function registerAccountTools() {
2856
2883
  },
2857
2884
  {
2858
2885
  name: "account_get_bills",
2886
+ title: "Get Account Bills",
2859
2887
  module: "account",
2860
2888
  description: "Get account ledger: fees paid, funding charges, realized PnL, transfers, etc. Default 20 records (last 7 days), max 100.",
2861
2889
  isWrite: false,
@@ -2922,6 +2950,7 @@ function registerAccountTools() {
2922
2950
  },
2923
2951
  {
2924
2952
  name: "account_get_positions_history",
2953
+ title: "Get Closed Positions History",
2925
2954
  module: "account",
2926
2955
  description: "Get closed position history for SWAP or FUTURES. Default 20 records, max 100.",
2927
2956
  isWrite: false,
@@ -2983,6 +3012,7 @@ function registerAccountTools() {
2983
3012
  },
2984
3013
  {
2985
3014
  name: "account_get_trade_fee",
3015
+ title: "Get Trade Fee Tier",
2986
3016
  module: "account",
2987
3017
  description: "Get maker/taker fee rates for the account. Useful to understand your fee tier before trading.",
2988
3018
  isWrite: false,
@@ -3015,6 +3045,7 @@ function registerAccountTools() {
3015
3045
  },
3016
3046
  {
3017
3047
  name: "account_get_config",
3048
+ title: "Get Account Configuration",
3018
3049
  module: "account",
3019
3050
  description: "Get account configuration: position mode (net vs hedge), account level, auto-loan settings, etc. Note: `settleCcy` is the current settlement currency for USDS-margined contracts. `settleCcyList` is the list of available settlement currencies to choose from. These fields only apply to USDS-margined contracts and can be ignored for standard USDT/coin-margined trading.",
3020
3051
  isWrite: false,
@@ -3033,6 +3064,7 @@ function registerAccountTools() {
3033
3064
  },
3034
3065
  {
3035
3066
  name: "account_get_max_withdrawal",
3067
+ title: "Get Max Withdrawable Amount",
3036
3068
  module: "account",
3037
3069
  description: "Get maximum withdrawable amount for a currency from the trading account. Useful before initiating a transfer or withdrawal.",
3038
3070
  isWrite: false,
@@ -3057,6 +3089,7 @@ function registerAccountTools() {
3057
3089
  },
3058
3090
  {
3059
3091
  name: "account_get_max_avail_size",
3092
+ title: "Get Max Available Position Size",
3060
3093
  module: "account",
3061
3094
  description: "Get maximum available size for opening or reducing a position. Different from account_get_max_size which calculates new order size.",
3062
3095
  isWrite: false,
@@ -3101,6 +3134,7 @@ function registerAccountTools() {
3101
3134
  },
3102
3135
  {
3103
3136
  name: "account_get_positions",
3137
+ title: "Get Current Positions",
3104
3138
  module: "account",
3105
3139
  description: "Get current open positions across all instrument types (MARGIN, SWAP, FUTURES, OPTION, EVENTS). Use swap_get_positions for SWAP/FUTURES-only queries.",
3106
3140
  isWrite: false,
@@ -3136,6 +3170,7 @@ function registerAccountTools() {
3136
3170
  },
3137
3171
  {
3138
3172
  name: "account_get_bills_archive",
3173
+ title: "Get Archived Account Bills",
3139
3174
  module: "account",
3140
3175
  description: "Get archived account ledger (bills older than 7 days, up to 3 months). Use account_get_bills for recent 7-day records. Default 20 records, max 100.",
3141
3176
  isWrite: false,
@@ -3202,9 +3237,11 @@ function registerAccountTools() {
3202
3237
  },
3203
3238
  {
3204
3239
  name: "account_set_position_mode",
3240
+ title: "Set Position Mode",
3205
3241
  module: "account",
3206
3242
  description: "Switch between net position mode and long/short hedge mode. net: one position per instrument (default). long_short_mode: separate long and short positions. [CAUTION] Requires no open positions or pending orders.",
3207
3243
  isWrite: true,
3244
+ idempotentHint: true,
3208
3245
  inputSchema: {
3209
3246
  type: "object",
3210
3247
  properties: {
@@ -3344,6 +3381,8 @@ function registerAlgoTradeTools() {
3344
3381
  return [
3345
3382
  {
3346
3383
  name: "swap_place_algo_order",
3384
+ title: "Perpetual Futures Place Algo Order",
3385
+ destructiveHint: false,
3347
3386
  module: "swap",
3348
3387
  description: "Place a SWAP/FUTURES algo order. [CAUTION] Executes real trades. conditional: single TP, single SL, or both on one order. oco: TP+SL simultaneously - first trigger cancels the other. move_order_stop: trailing stop (callbackRatio or callbackSpread). trigger: pending order activated when triggerPx is hit (provide triggerPx + orderPx). chase: smart-follow best bid/ask. iceberg: split large order into child orders at intervals. twap: time-weighted average price order splitting.",
3349
3388
  isWrite: true,
@@ -3492,6 +3531,8 @@ function registerAlgoTradeTools() {
3492
3531
  },
3493
3532
  {
3494
3533
  name: "swap_place_move_stop_order",
3534
+ title: "Perpetual Futures Place Move-Stop Order",
3535
+ destructiveHint: false,
3495
3536
  module: "swap",
3496
3537
  description: "[DEPRECATED] Use swap_place_algo_order with ordType='move_order_stop' instead. Place a SWAP/FUTURES trailing stop order. [CAUTION] Executes real trades. Specify callbackRatio (e.g. '0.01'=1%) or callbackSpread (fixed price distance), not both. Optionally set activePx so tracking starts once market reaches that price.",
3497
3538
  isWrite: true,
@@ -3569,6 +3610,8 @@ function registerAlgoTradeTools() {
3569
3610
  },
3570
3611
  {
3571
3612
  name: "swap_cancel_algo_orders",
3613
+ title: "Perpetual Futures Cancel Algo Orders",
3614
+ idempotentHint: true,
3572
3615
  module: "swap",
3573
3616
  description: "Cancel one or more pending SWAP/FUTURES algo orders (TP/SL). Accepts a list of {algoId, instId} objects.",
3574
3617
  isWrite: true,
@@ -3612,6 +3655,7 @@ function registerAlgoTradeTools() {
3612
3655
  },
3613
3656
  {
3614
3657
  name: "swap_get_algo_orders",
3658
+ title: "Perpetual Futures Get Algo Orders",
3615
3659
  module: "swap",
3616
3660
  description: "Query pending or completed SWAP/FUTURES algo orders (TP/SL, OCO, trailing stop).",
3617
3661
  isWrite: false,
@@ -3704,6 +3748,8 @@ function registerFuturesAlgoTools() {
3704
3748
  return [
3705
3749
  {
3706
3750
  name: "futures_place_algo_order",
3751
+ title: "Futures Place Algo Order",
3752
+ destructiveHint: false,
3707
3753
  module: "futures",
3708
3754
  description: "Place a FUTURES delivery algo order. [CAUTION] Executes real trades. conditional: single TP, single SL, or both on one order. oco: TP+SL simultaneously - first trigger cancels the other. move_order_stop: trailing stop (callbackRatio or callbackSpread). trigger: pending order activated when triggerPx is hit (provide triggerPx + orderPx). chase: smart-follow best bid/ask. iceberg: split large order into child orders at intervals. twap: time-weighted average price order splitting.",
3709
3755
  isWrite: true,
@@ -3852,6 +3898,8 @@ function registerFuturesAlgoTools() {
3852
3898
  },
3853
3899
  {
3854
3900
  name: "futures_place_move_stop_order",
3901
+ title: "Futures Place Move-Stop Order",
3902
+ destructiveHint: false,
3855
3903
  module: "futures",
3856
3904
  description: "[DEPRECATED] Use futures_place_algo_order with ordType='move_order_stop' instead. Place a FUTURES delivery trailing stop order. [CAUTION] Executes real trades.",
3857
3905
  isWrite: true,
@@ -3929,6 +3977,8 @@ function registerFuturesAlgoTools() {
3929
3977
  },
3930
3978
  {
3931
3979
  name: "futures_amend_algo_order",
3980
+ title: "Futures Amend Algo Order",
3981
+ idempotentHint: true,
3932
3982
  module: "futures",
3933
3983
  description: "Amend a pending FUTURES delivery algo order (modify TP/SL prices or size). Also covers TP/SL orders attached when placing the main order - look up algoId via futures_get_algo_orders first.",
3934
3984
  isWrite: true,
@@ -3965,6 +4015,8 @@ function registerFuturesAlgoTools() {
3965
4015
  },
3966
4016
  {
3967
4017
  name: "futures_cancel_algo_orders",
4018
+ title: "Futures Cancel Algo Orders",
4019
+ idempotentHint: true,
3968
4020
  module: "futures",
3969
4021
  description: "Cancel one or more pending FUTURES delivery algo orders (TP/SL). Accepts a list of {algoId, instId} objects.",
3970
4022
  isWrite: true,
@@ -4002,6 +4054,7 @@ function registerFuturesAlgoTools() {
4002
4054
  },
4003
4055
  {
4004
4056
  name: "futures_get_algo_orders",
4057
+ title: "Futures Get Algo Orders",
4005
4058
  module: "futures",
4006
4059
  description: "Query pending or completed FUTURES delivery algo orders (TP/SL, OCO, trailing stop).",
4007
4060
  isWrite: false,
@@ -4123,6 +4176,7 @@ function registerAuditTools() {
4123
4176
  {
4124
4177
  name: "trade_get_history",
4125
4178
  module: "account",
4179
+ title: "Get Tool-Call Audit Log",
4126
4180
  description: "Query local audit log of tool calls made through this MCP server. Returns recent operations with timestamps, duration, params, and results. Use to review what trades or queries were executed in this session or past sessions.",
4127
4181
  isWrite: false,
4128
4182
  inputSchema: {
@@ -4228,6 +4282,223 @@ async function downloadSkillZip(client, name, targetDir, format = "zip") {
4228
4282
  const filePath = safeWriteFile(targetDir, fileName, result.data);
4229
4283
  return filePath;
4230
4284
  }
4285
+ function listFilesRecursive(dir, base = "") {
4286
+ const results = [];
4287
+ for (const entry of readdirSync(dir)) {
4288
+ const fullPath = join5(dir, entry);
4289
+ const relPath = base ? `${base}/${entry}` : entry;
4290
+ if (statSync(fullPath).isDirectory()) {
4291
+ results.push(...listFilesRecursive(fullPath, relPath));
4292
+ } else {
4293
+ results.push(relPath);
4294
+ }
4295
+ }
4296
+ return results;
4297
+ }
4298
+ function computeFileHashes(contentDir) {
4299
+ const hashes = {};
4300
+ for (const relPath of listFilesRecursive(contentDir)) {
4301
+ if (relPath === "_meta.json") continue;
4302
+ const bytes = readFileSync2(join5(contentDir, relPath));
4303
+ hashes[relPath] = "sha256:" + createHash("sha256").update(bytes).digest("hex");
4304
+ }
4305
+ return hashes;
4306
+ }
4307
+ async function getPublicKey(client, keyId) {
4308
+ try {
4309
+ const query = {};
4310
+ if (keyId !== void 0) query.keyId = keyId;
4311
+ const result = await client.privateGet(
4312
+ "/api/v5/skill/signing-key",
4313
+ query
4314
+ );
4315
+ if (!result.data || result.data.status !== "active") return null;
4316
+ return result.data.publicKey;
4317
+ } catch (e) {
4318
+ if (e instanceof ConfigError) throw e;
4319
+ return null;
4320
+ }
4321
+ }
4322
+ var ED25519_SPKI_HEADER = Buffer.from("302a300506032b6570032100", "hex");
4323
+ function parseSSHPublicKey(base64) {
4324
+ try {
4325
+ const buf = Buffer.from(base64, "base64");
4326
+ let offset = 0;
4327
+ if (buf.length < 4) return null;
4328
+ const algLen = buf.readUInt32BE(offset);
4329
+ offset += 4;
4330
+ if (offset + algLen + 4 > buf.length) return null;
4331
+ if (buf.subarray(offset, offset + algLen).toString("ascii") !== "ssh-ed25519") return null;
4332
+ offset += algLen;
4333
+ const keyLen = buf.readUInt32BE(offset);
4334
+ offset += 4;
4335
+ if (keyLen !== 32 || offset + keyLen > buf.length) return null;
4336
+ return buf.subarray(offset, offset + keyLen);
4337
+ } catch {
4338
+ return null;
4339
+ }
4340
+ }
4341
+ async function verifySkillSignature(contentDir, signing, opts) {
4342
+ if (!signing) {
4343
+ const fallback = await tryServerFallback(contentDir, opts);
4344
+ if (fallback?.status === "verified_by_server") return fallback;
4345
+ return {
4346
+ status: "failed",
4347
+ error: fallback?.error ?? "Skill is not signed",
4348
+ ...fallback?.mismatched?.length && { mismatched: fallback.mismatched }
4349
+ };
4350
+ }
4351
+ const publicKeyBase64 = opts?.fetchPublicKey ? await opts.fetchPublicKey(signing.public_key_id) ?? "" : "";
4352
+ if (!publicKeyBase64) {
4353
+ return localFail(
4354
+ contentDir,
4355
+ opts,
4356
+ `Unknown signing key: ${signing.public_key_id}. Please update CLI.`,
4357
+ "Unknown signing key \u2014 consider updating CLI"
4358
+ );
4359
+ }
4360
+ const rawKey = parseSSHPublicKey(publicKeyBase64);
4361
+ if (!rawKey) {
4362
+ return localFail(
4363
+ contentDir,
4364
+ opts,
4365
+ `Malformed public key for key ID: ${signing.public_key_id}`,
4366
+ "Malformed public key from server \u2014 consider updating CLI",
4367
+ signing.public_key_id
4368
+ );
4369
+ }
4370
+ if (!ed25519Verify(signing, rawKey)) {
4371
+ return localFail(
4372
+ contentDir,
4373
+ opts,
4374
+ "Invalid signature",
4375
+ "Signature mismatch \u2014 key may be outdated or file was modified",
4376
+ signing.public_key_id
4377
+ );
4378
+ }
4379
+ const bindingError = checkNameVersionBinding(signing, opts);
4380
+ if (bindingError) {
4381
+ return { status: "failed", error: bindingError, publicKeyId: signing.public_key_id };
4382
+ }
4383
+ const integrityFailure = await checkFileIntegrity(signing, contentDir, opts);
4384
+ if (integrityFailure) return integrityFailure;
4385
+ const signedFiles = new Set(Object.keys(signing.files));
4386
+ const extraFiles = listFilesRecursive(contentDir).filter(
4387
+ (f) => f !== "_meta.json" && !signedFiles.has(f)
4388
+ );
4389
+ return { status: "verified", publicKeyId: signing.public_key_id, filesChecked: signedFiles.size, extraFiles };
4390
+ }
4391
+ async function localFail(contentDir, opts, localError, serverHint, keyId) {
4392
+ const fallback = await tryServerFallback(contentDir, opts);
4393
+ if (fallback?.status === "verified_by_server") {
4394
+ return { ...fallback, error: serverHint };
4395
+ }
4396
+ return {
4397
+ status: "failed",
4398
+ error: localError,
4399
+ ...keyId && { publicKeyId: keyId },
4400
+ ...fallback?.mismatched?.length && { mismatched: fallback.mismatched }
4401
+ };
4402
+ }
4403
+ function ed25519Verify(signing, rawKey) {
4404
+ const payloadObj = { files: signing.files, public_key_id: signing.public_key_id };
4405
+ if (signing.name !== void 0) payloadObj.name = signing.name;
4406
+ if (signing.version !== void 0) payloadObj.version = signing.version;
4407
+ const message = Buffer.from(canonicalize(payloadObj), "utf-8");
4408
+ const sig = Buffer.from(signing.signature, "base64");
4409
+ const spkiKey = Buffer.concat([ED25519_SPKI_HEADER, rawKey]);
4410
+ const keyObj = createPublicKey({ key: spkiKey, format: "der", type: "spki" });
4411
+ return cryptoVerify(null, message, keyObj, sig);
4412
+ }
4413
+ function checkNameVersionBinding(signing, opts) {
4414
+ if (signing.name !== void 0 && opts?.skillName !== void 0 && signing.name !== opts.skillName) {
4415
+ return `Skill name mismatch: signature binds "${signing.name}" but installing as "${opts.skillName}"`;
4416
+ }
4417
+ if (signing.version !== void 0 && opts?.skillVersion !== void 0 && signing.version !== opts.skillVersion) {
4418
+ return `Skill version mismatch: signature binds "${signing.version}" but package declares "${opts.skillVersion}"`;
4419
+ }
4420
+ return null;
4421
+ }
4422
+ async function checkFileIntegrity(signing, contentDir, opts) {
4423
+ const resolvedContentDir = resolve2(contentDir);
4424
+ for (const [filename, expectedHash] of Object.entries(signing.files)) {
4425
+ const resolvedPath = resolve2(join6(contentDir, filename));
4426
+ if (!resolvedPath.startsWith(resolvedContentDir + sep2)) {
4427
+ return { status: "failed", error: `Path traversal detected in signing manifest: ${filename}`, publicKeyId: signing.public_key_id };
4428
+ }
4429
+ let bytes;
4430
+ try {
4431
+ bytes = readFileSync3(resolvedPath);
4432
+ } catch {
4433
+ return { status: "failed", error: `File missing: ${filename}`, publicKeyId: signing.public_key_id };
4434
+ }
4435
+ const actual = "sha256:" + createHash2("sha256").update(bytes).digest("hex");
4436
+ if (actual !== expectedHash) {
4437
+ return localFail(
4438
+ contentDir,
4439
+ opts,
4440
+ `File integrity check failed: ${filename}`,
4441
+ "_meta.json may be corrupted",
4442
+ signing.public_key_id
4443
+ );
4444
+ }
4445
+ }
4446
+ return null;
4447
+ }
4448
+ async function tryServerFallback(contentDir, opts) {
4449
+ if (!opts?.serverSideVerify || !opts.skillName) return null;
4450
+ const fileHashes = computeFileHashes(contentDir);
4451
+ const result = await opts.serverSideVerify(
4452
+ opts.skillName,
4453
+ opts.skillVersion,
4454
+ fileHashes
4455
+ );
4456
+ if (!result) return null;
4457
+ if (result.verified) {
4458
+ return {
4459
+ status: "verified_by_server",
4460
+ filesChecked: Object.keys(fileHashes).length,
4461
+ serverVersion: result.version
4462
+ };
4463
+ }
4464
+ return {
4465
+ status: "failed",
4466
+ filesChecked: Object.keys(fileHashes).length,
4467
+ mismatched: result.mismatched,
4468
+ ...result.message && { error: result.message }
4469
+ };
4470
+ }
4471
+ function canonicalize(obj) {
4472
+ return JSON.stringify(deepSortKeys(obj));
4473
+ }
4474
+ function compareKeys(a, b) {
4475
+ if (a < b) return -1;
4476
+ if (a > b) return 1;
4477
+ return 0;
4478
+ }
4479
+ function deepSortKeys(val) {
4480
+ if (val === null || typeof val !== "object") return val;
4481
+ if (Array.isArray(val)) return val.map(deepSortKeys);
4482
+ const sorted = {};
4483
+ for (const key of Object.keys(val).sort(compareKeys)) {
4484
+ sorted[key] = deepSortKeys(val[key]);
4485
+ }
4486
+ return sorted;
4487
+ }
4488
+ async function serverSideVerify(client, skillName, version, files) {
4489
+ try {
4490
+ const body = { skillName, files };
4491
+ if (version !== void 0) body.version = version;
4492
+ const result = await client.privatePost(
4493
+ "/api/v5/skill/verify",
4494
+ body
4495
+ );
4496
+ return result.data;
4497
+ } catch (e) {
4498
+ if (e instanceof ConfigError) throw e;
4499
+ return null;
4500
+ }
4501
+ }
4231
4502
  var DEFAULT_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
4232
4503
  var DEFAULT_MAX_FILES = 1e3;
4233
4504
  var DEFAULT_MAX_COMPRESSION_RATIO = 100;
@@ -4266,7 +4537,7 @@ async function extractSkillZip(zipPath, targetDir, limits) {
4266
4537
  const maxTotalBytes = limits?.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
4267
4538
  const maxFiles = limits?.maxFiles ?? DEFAULT_MAX_FILES;
4268
4539
  const maxCompressionRatio = limits?.maxCompressionRatio ?? DEFAULT_MAX_COMPRESSION_RATIO;
4269
- const resolvedTarget = resolve2(targetDir);
4540
+ const resolvedTarget = resolve3(targetDir);
4270
4541
  mkdirSync3(resolvedTarget, { recursive: true });
4271
4542
  return new Promise((resolvePromise, reject) => {
4272
4543
  yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
@@ -4306,11 +4577,11 @@ async function extractSkillZip(zipPath, targetDir, limits) {
4306
4577
  });
4307
4578
  }
4308
4579
  function readMetaJson(contentDir) {
4309
- const metaPath = join5(contentDir, "_meta.json");
4580
+ const metaPath = join7(contentDir, "_meta.json");
4310
4581
  if (!existsSync(metaPath)) {
4311
4582
  throw new Error(`_meta.json not found in ${contentDir}. Invalid skill package.`);
4312
4583
  }
4313
- const raw = readFileSync2(metaPath, "utf-8");
4584
+ const raw = readFileSync4(metaPath, "utf-8");
4314
4585
  let parsed;
4315
4586
  try {
4316
4587
  parsed = JSON.parse(raw);
@@ -4328,22 +4599,49 @@ function readMetaJson(contentDir) {
4328
4599
  name: String(meta.name),
4329
4600
  version: String(meta.version),
4330
4601
  title: typeof meta.title === "string" ? meta.title : "",
4331
- description: typeof meta.description === "string" ? meta.description : ""
4602
+ description: typeof meta.description === "string" ? meta.description : "",
4603
+ signing: parseSigningBlock(meta.signing)
4604
+ };
4605
+ }
4606
+ function tryReadMetaJson(contentDir) {
4607
+ try {
4608
+ return readMetaJson(contentDir);
4609
+ } catch {
4610
+ return null;
4611
+ }
4612
+ }
4613
+ function parseSigningBlock(raw) {
4614
+ if (!raw || typeof raw !== "object") return void 0;
4615
+ const s = raw;
4616
+ if (typeof s.signature !== "string" || typeof s.public_key_id !== "string" || !s.files) {
4617
+ return void 0;
4618
+ }
4619
+ const rawFiles = s.files;
4620
+ const files = {};
4621
+ for (const [key, value] of Object.entries(rawFiles)) {
4622
+ if (typeof value === "string") files[key] = value;
4623
+ }
4624
+ return {
4625
+ signature: s.signature,
4626
+ public_key_id: s.public_key_id,
4627
+ files,
4628
+ ...typeof s.name === "string" && { name: s.name },
4629
+ ...typeof s.version === "string" && { version: s.version }
4332
4630
  };
4333
4631
  }
4334
4632
  function validateSkillMdExists(contentDir) {
4335
- const skillMdPath = join5(contentDir, "SKILL.md");
4633
+ const skillMdPath = join7(contentDir, "SKILL.md");
4336
4634
  if (!existsSync(skillMdPath)) {
4337
4635
  throw new Error(`SKILL.md not found in ${contentDir}. Invalid skill package.`);
4338
4636
  }
4339
4637
  }
4340
- var DEFAULT_REGISTRY_PATH = join6(homedir4(), ".okx", "skills", "registry.json");
4638
+ var DEFAULT_REGISTRY_PATH = join8(homedir4(), ".okx", "skills", "registry.json");
4341
4639
  function readRegistry(registryPath = DEFAULT_REGISTRY_PATH) {
4342
4640
  if (!existsSync2(registryPath)) {
4343
4641
  return { version: 1, skills: {} };
4344
4642
  }
4345
4643
  try {
4346
- const raw = readFileSync3(registryPath, "utf-8");
4644
+ const raw = readFileSync5(registryPath, "utf-8");
4347
4645
  return JSON.parse(raw);
4348
4646
  } catch {
4349
4647
  return { version: 1, skills: {} };
@@ -4353,7 +4651,7 @@ function writeRegistry(registry, registryPath = DEFAULT_REGISTRY_PATH) {
4353
4651
  mkdirSync4(dirname3(registryPath), { recursive: true });
4354
4652
  writeFileSync3(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
4355
4653
  }
4356
- function upsertSkillRecord(meta, registryPath = DEFAULT_REGISTRY_PATH) {
4654
+ function upsertSkillRecord(meta, registryPath = DEFAULT_REGISTRY_PATH, verification) {
4357
4655
  const registry = readRegistry(registryPath);
4358
4656
  const now = (/* @__PURE__ */ new Date()).toISOString();
4359
4657
  const existing = registry.skills[meta.name];
@@ -4363,7 +4661,8 @@ function upsertSkillRecord(meta, registryPath = DEFAULT_REGISTRY_PATH) {
4363
4661
  description: meta.description,
4364
4662
  installedAt: existing?.installedAt ?? now,
4365
4663
  updatedAt: now,
4366
- source: "marketplace"
4664
+ source: "marketplace",
4665
+ ...verification !== void 0 && { verification }
4367
4666
  };
4368
4667
  writeRegistry(registry, registryPath);
4369
4668
  }
@@ -4383,6 +4682,7 @@ function registerSkillsTools() {
4383
4682
  {
4384
4683
  name: "skills_get_categories",
4385
4684
  module: "skills",
4685
+ title: "Skills Marketplace List Categories",
4386
4686
  description: "List all available skill categories in OKX Skills Marketplace. Use the returned categoryId as input to skills_search for category filtering. Do NOT use for searching or downloading skills - use skills_search or skills_download.",
4387
4687
  inputSchema: {
4388
4688
  type: "object",
@@ -4395,6 +4695,7 @@ function registerSkillsTools() {
4395
4695
  {
4396
4696
  name: "skills_search",
4397
4697
  module: "skills",
4698
+ title: "Skills Marketplace Search",
4398
4699
  description: "Search for skills in OKX Skills Marketplace by keyword or category. To get valid category IDs, call skills_get_categories first. Returns skill names for use with skills_download. Do NOT use for downloading - use skills_download.",
4399
4700
  inputSchema: {
4400
4701
  type: "object",
@@ -4424,6 +4725,7 @@ function registerSkillsTools() {
4424
4725
  {
4425
4726
  name: "skills_download",
4426
4727
  module: "skills",
4728
+ title: "Skills Marketplace Download",
4427
4729
  description: "Download a skill package from OKX Skills Marketplace to a local directory. Always call skills_search first to confirm the skill name exists. Downloads the latest approved version. NOTE: Downloads third-party developer content - does NOT install to agents. For full installation use CLI: okx skill add <name>. Use when the user wants to inspect or manually install a skill package.",
4428
4730
  inputSchema: {
4429
4731
  type: "object",
@@ -4446,6 +4748,8 @@ function registerSkillsTools() {
4446
4748
  additionalProperties: false
4447
4749
  },
4448
4750
  isWrite: true,
4751
+ destructiveHint: false,
4752
+ idempotentHint: true,
4449
4753
  handler: handleDownload
4450
4754
  }
4451
4755
  ];
@@ -4509,6 +4813,7 @@ function registerGridTools() {
4509
4813
  return [
4510
4814
  {
4511
4815
  name: "grid_get_orders",
4816
+ title: "Grid Bot List Orders",
4512
4817
  module: "bot.grid",
4513
4818
  description: "List grid bots. status='active' for running; 'history' for stopped.",
4514
4819
  isWrite: false,
@@ -4555,6 +4860,7 @@ function registerGridTools() {
4555
4860
  },
4556
4861
  {
4557
4862
  name: "grid_get_order_details",
4863
+ title: "Grid Bot Get Detail",
4558
4864
  module: "bot.grid",
4559
4865
  description: "Get grid bot detail by algo ID. Returns config, status, PnL, and position.",
4560
4866
  isWrite: false,
@@ -4585,6 +4891,7 @@ function registerGridTools() {
4585
4891
  },
4586
4892
  {
4587
4893
  name: "grid_get_sub_orders",
4894
+ title: "Grid Bot Get Sub-Orders",
4588
4895
  module: "bot.grid",
4589
4896
  description: "Query sub-orders (grid trades) of a bot. type='filled' for executed; 'live' for pending.",
4590
4897
  isWrite: false,
@@ -4629,9 +4936,11 @@ function registerGridTools() {
4629
4936
  },
4630
4937
  {
4631
4938
  name: "grid_create_order",
4939
+ title: "Grid Bot Create",
4632
4940
  module: "bot.grid",
4633
4941
  description: "Create grid bot (spot, USDT-margined, or coin-margined contract). [CAUTION] Locks funds. Spot: quoteSz|baseSz. Contract: direction+lever+sz.",
4634
4942
  isWrite: true,
4943
+ destructiveHint: false,
4635
4944
  inputSchema: {
4636
4945
  type: "object",
4637
4946
  properties: {
@@ -4704,7 +5013,9 @@ function registerGridTools() {
4704
5013
  },
4705
5014
  {
4706
5015
  name: "grid_amend_order",
5016
+ title: "Grid Bot Amend",
4707
5017
  module: "bot.grid",
5018
+ idempotentHint: true,
4708
5019
  description: "Amend a running grid bot. [CAUTION] Modifies a running bot. Use grid_list_orders to confirm the bot is running and obtain the algoId before calling.\nSupports two modes, which can be combined in a single call:\n\u2022 Price-range mode (maxPx+minPx+gridNum): change upper/lower price boundary and grid count. Contract grid: if new range requires more margin, pass topUpAmt; omit to auto-use the minimum required. Spot grid: topUpAmt is not supported.\n\u2022 TP/SL mode (instId + any of tpTriggerPx/slTriggerPx/tpRatio/slRatio): update take-profit and/or stop-loss. Pass '-1' to explicitly clear an existing TP or SL. tpTriggerPx/slTriggerPx are absolute prices; tpRatio/slRatio are profit ratios (e.g. '0.1' = 10%).\nWhen both sets of params are provided, both APIs are called sequentially.\nDo NOT use to create a new grid bot - use grid_create_order instead. Do NOT use to stop a grid bot - use grid_stop_order instead.",
4709
5020
  isWrite: true,
4710
5021
  inputSchema: {
@@ -4827,7 +5138,9 @@ function registerGridTools() {
4827
5138
  },
4828
5139
  {
4829
5140
  name: "grid_stop_order",
5141
+ title: "Grid Bot Stop",
4830
5142
  module: "bot.grid",
5143
+ idempotentHint: true,
4831
5144
  description: "[CAUTION] Stop a grid bot or close its remaining open position \u2014 real trades, irreversible. Workflow: (1) If the user has not specified which bot to stop, call grid_get_orders first and ask the user to confirm which bot before proceeding. (2) Call grid_get_order_details to check the current 'state' field. (3) If state='running' \u2192 call this tool: stopType='1' (default, clean exit) \u2014 spot grid sells all base assets back to quote; contract grid market-closes all positions. stopType='2' (keep assets) \u2014 spot grid keeps base assets as-is; contract grid cancels grid orders but leaves the position open. (4) If state='no_close_position' \u2192 call this tool with stopType='1' to close the remaining open position.",
4832
5145
  isWrite: true,
4833
5146
  inputSchema: {
@@ -4927,9 +5240,11 @@ function registerDcaTools() {
4927
5240
  return [
4928
5241
  {
4929
5242
  name: "dca_create_order",
5243
+ title: "Martingale Bot Create",
4930
5244
  module: "bot.dca",
4931
5245
  description: "Create a DCA (Martingale) bot. [CAUTION] Real trades. contract_dca requires lever; spot_dca must be long. If maxSafetyOrds>0: need safetyOrdAmt, pxSteps.",
4932
5246
  isWrite: true,
5247
+ destructiveHint: false,
4933
5248
  inputSchema: {
4934
5249
  type: "object",
4935
5250
  properties: {
@@ -5005,7 +5320,9 @@ function registerDcaTools() {
5005
5320
  },
5006
5321
  {
5007
5322
  name: "dca_stop_order",
5323
+ title: "Martingale Bot Stop",
5008
5324
  module: "bot.dca",
5325
+ idempotentHint: true,
5009
5326
  description: "[CAUTION] Stop a DCA bot or close its remaining open position \u2014 real trades, irreversible. Workflow: (1) If the user has not specified which bot to stop, call dca_get_orders first and ask the user to confirm which bot before proceeding. (2) Call dca_get_order_details to check the current 'state' field. (3) If state='running' \u2192 call this tool. (4) If state='no_close_position' \u2192 call this tool with stopType='1' to close the remaining open position. spot_dca requires stopType: 1=sell all tokens, 2=keep tokens.",
5010
5327
  isWrite: true,
5011
5328
  inputSchema: {
@@ -5038,6 +5355,7 @@ function registerDcaTools() {
5038
5355
  },
5039
5356
  {
5040
5357
  name: "dca_get_orders",
5358
+ title: "Martingale Bot List Orders",
5041
5359
  module: "bot.dca",
5042
5360
  description: "List DCA bots. Default: active (running). Use status=history for stopped.",
5043
5361
  isWrite: false,
@@ -5076,6 +5394,7 @@ function registerDcaTools() {
5076
5394
  },
5077
5395
  {
5078
5396
  name: "dca_get_order_details",
5397
+ title: "Martingale Bot Get Detail",
5079
5398
  module: "bot.dca",
5080
5399
  description: "Get DCA bot position details (avgPx, upl, liqPx, etc).",
5081
5400
  isWrite: false,
@@ -5101,6 +5420,7 @@ function registerDcaTools() {
5101
5420
  },
5102
5421
  {
5103
5422
  name: "dca_get_sub_orders",
5423
+ title: "Martingale Bot Get Sub-Orders",
5104
5424
  module: "bot.dca",
5105
5425
  description: "Get DCA cycles or orders in a cycle. Omit cycleId=cycle list; with cycleId=orders.",
5106
5426
  isWrite: false,
@@ -5160,8 +5480,9 @@ function registerEarnTools() {
5160
5480
  return [
5161
5481
  {
5162
5482
  name: "earn_get_savings_balance",
5483
+ title: "Get Simple Earn Balance",
5163
5484
  module: "earn.savings",
5164
- description: "Get Simple Earn (savings/flexible earn) balance. Returns current holdings for all currencies or a specific one. To show market rates alongside balance (\u5E02\u573A\u5747\u5229\u7387), call earn_get_lending_rate_history. earn_get_lending_rate_history also returns fixed-term (\u5B9A\u671F) product offers, so one call gives a complete view of both flexible and fixed options. Do NOT use for fixed-term (\u5B9A\u671F) order queries - use earn_get_fixed_order_list instead.",
5485
+ description: "Get Simple Earn (savings/flexible earn) balance. Returns current holdings for all currencies or a specific one. To show market rates alongside balance, call earn_get_lending_rate_history. To browse available fixed-term products with quota info, use earn_get_fixed_earn_products. Do NOT use for fixed-term order queries \u2014 use earn_get_fixed_order_list instead.",
5165
5486
  isWrite: false,
5166
5487
  inputSchema: {
5167
5488
  type: "object",
@@ -5184,6 +5505,7 @@ function registerEarnTools() {
5184
5505
  },
5185
5506
  {
5186
5507
  name: "earn_get_fixed_order_list",
5508
+ title: "Get Fixed-Term Earn Orders",
5187
5509
  module: "earn.savings",
5188
5510
  description: "Get Simple Earn Fixed (\u5B9A\u671F\u8D5A\u5E01) lending order list. Returns orders sorted by creation time descending. Use this to check status of fixed-term lending orders (pending/earning/expired/settled/cancelled). Do NOT use for flexible earn balance - use earn_get_savings_balance instead. If the result is empty, do NOT display any fixed-term section in the output.",
5189
5511
  isWrite: false,
@@ -5221,9 +5543,11 @@ function registerEarnTools() {
5221
5543
  },
5222
5544
  {
5223
5545
  name: "earn_savings_purchase",
5546
+ title: "Subscribe Simple Earn",
5224
5547
  module: "earn.savings",
5225
5548
  description: "Purchase Simple Earn (savings/flexible earn). [CAUTION] Moves real funds into earn product.",
5226
5549
  isWrite: true,
5550
+ destructiveHint: false,
5227
5551
  inputSchema: {
5228
5552
  type: "object",
5229
5553
  properties: {
@@ -5259,9 +5583,11 @@ function registerEarnTools() {
5259
5583
  },
5260
5584
  {
5261
5585
  name: "earn_savings_redeem",
5586
+ title: "Redeem Simple Earn",
5262
5587
  module: "earn.savings",
5263
5588
  description: "Redeem Simple Earn (savings/flexible earn). [CAUTION] Withdraws funds from earn product.",
5264
5589
  isWrite: true,
5590
+ destructiveHint: false,
5265
5591
  inputSchema: {
5266
5592
  type: "object",
5267
5593
  properties: {
@@ -5292,9 +5618,11 @@ function registerEarnTools() {
5292
5618
  },
5293
5619
  {
5294
5620
  name: "earn_set_lending_rate",
5621
+ title: "Set Lending Rate",
5295
5622
  module: "earn.savings",
5296
5623
  description: "Set lending rate for Simple Earn. [CAUTION] Changes your lending rate preference.",
5297
5624
  isWrite: true,
5625
+ idempotentHint: true,
5298
5626
  inputSchema: {
5299
5627
  type: "object",
5300
5628
  properties: {
@@ -5324,6 +5652,7 @@ function registerEarnTools() {
5324
5652
  },
5325
5653
  {
5326
5654
  name: "earn_get_lending_history",
5655
+ title: "Get Lending History",
5327
5656
  module: "earn.savings",
5328
5657
  description: "Get personal lending records for Simple Earn (your own lending history). NOT for market rate queries. Returns your lending records with amount, rate, and earnings data.",
5329
5658
  isWrite: false,
@@ -5365,9 +5694,11 @@ function registerEarnTools() {
5365
5694
  },
5366
5695
  {
5367
5696
  name: "earn_fixed_purchase",
5697
+ title: "Subscribe Fixed-Term Earn",
5368
5698
  module: "earn.savings",
5369
- description: "Purchase Simple Earn Fixed (\u5B9A\u671F) product, two-step flow. First call (confirm omitted or false): returns purchase preview with product details and risk warning. Preview offer fields: lendQuota = remaining quota (\u5269\u4F59\u989D\u5EA6), soldOut = whether product is sold out (lendQuota is 0). YOU MUST display the 'warning' field from the preview response to the user VERBATIM before asking for confirmation - do NOT omit or summarize it. Second call (confirm=true): executes the purchase. Only proceed after the user explicitly confirms. IMPORTANT: Orders in 'pending' (\u5339\u914D\u4E2D) state can still be cancelled via earn_fixed_redeem; once the status changes to 'earning' (\u8D5A\u5E01\u4E2D), funds are LOCKED until maturity - no early redemption allowed.",
5699
+ description: "Purchase Simple Earn Fixed (\u5B9A\u671F) product, two-step flow. First call (confirm omitted or false): returns purchase preview with product details and risk warning. Preview offer fields: lendQuota = remaining quota, soldOut = whether product is sold out (lendQuota is 0). YOU MUST display the 'warning' field from the preview response to the user VERBATIM before asking for confirmation \u2014 do NOT omit or summarize it. Second call (confirm=true): executes the purchase. Only proceed after the user explicitly confirms. IMPORTANT: Orders in 'pending' (\u5339\u914D\u4E2D) state can still be cancelled via earn_fixed_redeem; once the status changes to 'earning' (\u8D5A\u5E01\u4E2D), funds are LOCKED until maturity - no early redemption allowed.",
5370
5700
  isWrite: true,
5701
+ destructiveHint: false,
5371
5702
  inputSchema: {
5372
5703
  type: "object",
5373
5704
  properties: {
@@ -5439,9 +5770,11 @@ function registerEarnTools() {
5439
5770
  },
5440
5771
  {
5441
5772
  name: "earn_fixed_redeem",
5773
+ title: "Redeem Fixed-Term Earn",
5442
5774
  module: "earn.savings",
5443
5775
  description: "Redeem Simple Earn Fixed (\u5B9A\u671F\u8D5A\u5E01) order. [CAUTION] Redeems a fixed-term lending order. Always redeems the full order amount. Only orders in 'pending' (\u5339\u914D\u4E2D) state can be redeemed - orders in 'earning' state are locked until maturity and cannot be redeemed early. Do NOT use for flexible earn redemption - use earn_savings_redeem instead.",
5444
5776
  isWrite: true,
5777
+ destructiveHint: false,
5445
5778
  inputSchema: {
5446
5779
  type: "object",
5447
5780
  properties: {
@@ -5467,8 +5800,9 @@ function registerEarnTools() {
5467
5800
  },
5468
5801
  {
5469
5802
  name: "earn_get_lending_rate_history",
5803
+ title: "Get Lending Rates & Offers",
5470
5804
  module: "earn.savings",
5471
- description: "Query Simple Earn lending rates and fixed-term offers. Use this tool when the user asks about Simple Earn products, current or historical lending rates, or when displaying savings balance with market rate context (\u5E02\u573A\u5747\u5229\u7387). Returns lending rate history (lendingRate field, newest-first) AND available fixed-term (\u5B9A\u671F) offers with APR, term, min amount, and quota - one call gives a complete view of both flexible and fixed options. In fixedOffers: lendQuota = remaining quota (\u5269\u4F59\u989D\u5EA6), soldOut = whether product is sold out (lendQuota is 0). To get current flexible APY: use limit=1 and read lendingRate.",
5805
+ description: "Query Simple Earn lending rates and fixed-term offers. Use this tool when the user asks about Simple Earn products, current or historical lending rates, or when displaying savings balance with market rate context (\u5E02\u573A\u5747\u5229\u7387). Returns lending rate history (lendingRate field, newest-first) AND available fixed-term (\u5B9A\u671F) offers with APR, term, min amount, and quota \u2014 one call gives a complete view of both flexible and fixed options. In fixedOffers: lendQuota = remaining quota, soldOut = whether product is sold out (lendQuota is 0). For dedicated fixed-term product queries, use earn_get_fixed_earn_products. To get current flexible APY: use limit=1 and read lendingRate.",
5472
5806
  isWrite: false,
5473
5807
  inputSchema: {
5474
5808
  type: "object",
@@ -5527,6 +5861,37 @@ function registerEarnTools() {
5527
5861
  fixedOffers
5528
5862
  };
5529
5863
  }
5864
+ },
5865
+ {
5866
+ name: "earn_get_fixed_earn_products",
5867
+ title: "Get Fixed-Term Earn Products",
5868
+ module: "earn.savings",
5869
+ description: "Query available Simple Earn Fixed-term products. Returns fixed-term offers with APR, term, min investment, and remaining quota. Use to check available products and quota before purchasing. Do NOT use for querying your own orders \u2014 use earn_get_fixed_order_list instead. Do NOT use just for current flexible APY -- use earn_get_lending_rate_history with limit=1 instead.",
5870
+ isWrite: false,
5871
+ inputSchema: {
5872
+ type: "object",
5873
+ properties: {
5874
+ ccy: {
5875
+ type: "string",
5876
+ description: "e.g. USDT. Omit for all currencies."
5877
+ }
5878
+ }
5879
+ },
5880
+ handler: async (rawArgs, context) => {
5881
+ const args = asRecord(rawArgs);
5882
+ const response = await context.client.privateGet(
5883
+ "/api/v5/finance/simple-earn-fixed/offers",
5884
+ compactObject({ ccy: readString(args, "ccy") }),
5885
+ privateRateLimit("earn_get_fixed_earn_products", 2)
5886
+ );
5887
+ const result = normalizeResponse(response);
5888
+ const allOffers = Array.isArray(result["data"]) ? result["data"] : [];
5889
+ result["data"] = allOffers.map(({ borrowingOrderQuota: _, ...rest }) => ({
5890
+ ...rest,
5891
+ soldOut: rest["lendQuota"] === "0"
5892
+ }));
5893
+ return result;
5894
+ }
5530
5895
  }
5531
5896
  ];
5532
5897
  }
@@ -5537,6 +5902,7 @@ function registerOnchainEarnTools() {
5537
5902
  // -------------------------------------------------------------------------
5538
5903
  {
5539
5904
  name: "onchain_earn_get_offers",
5905
+ title: "On-chain Earn List Offers",
5540
5906
  module: "earn.onchain",
5541
5907
  description: "List staking/DeFi products with APY, terms, and limits. Always show protocol name (protocol field) and earnings currency (earningData[].ccy) when presenting results.",
5542
5908
  isWrite: false,
@@ -5576,9 +5942,11 @@ function registerOnchainEarnTools() {
5576
5942
  // -------------------------------------------------------------------------
5577
5943
  {
5578
5944
  name: "onchain_earn_purchase",
5945
+ title: "On-chain Earn Subscribe",
5579
5946
  module: "earn.onchain",
5580
5947
  description: "Invest in a staking/DeFi product. [CAUTION] Moves real funds.",
5581
5948
  isWrite: true,
5949
+ destructiveHint: false,
5582
5950
  inputSchema: {
5583
5951
  type: "object",
5584
5952
  properties: {
@@ -5629,9 +5997,11 @@ function registerOnchainEarnTools() {
5629
5997
  // -------------------------------------------------------------------------
5630
5998
  {
5631
5999
  name: "onchain_earn_redeem",
6000
+ title: "On-chain Earn Redeem",
5632
6001
  module: "earn.onchain",
5633
6002
  description: "Redeem a staking/DeFi investment. [CAUTION] Some products have lock periods, early redemption may incur penalties.",
5634
6003
  isWrite: true,
6004
+ destructiveHint: false,
5635
6005
  inputSchema: {
5636
6006
  type: "object",
5637
6007
  properties: {
@@ -5669,9 +6039,11 @@ function registerOnchainEarnTools() {
5669
6039
  // -------------------------------------------------------------------------
5670
6040
  {
5671
6041
  name: "onchain_earn_cancel",
6042
+ title: "On-chain Earn Cancel Order",
5672
6043
  module: "earn.onchain",
5673
6044
  description: "Cancel a pending staking/DeFi purchase order. [CAUTION]",
5674
6045
  isWrite: true,
6046
+ idempotentHint: true,
5675
6047
  inputSchema: {
5676
6048
  type: "object",
5677
6049
  properties: {
@@ -5704,6 +6076,7 @@ function registerOnchainEarnTools() {
5704
6076
  // -------------------------------------------------------------------------
5705
6077
  {
5706
6078
  name: "onchain_earn_get_active_orders",
6079
+ title: "On-chain Earn Active Orders",
5707
6080
  module: "earn.onchain",
5708
6081
  description: "List current active staking/DeFi investments.",
5709
6082
  isWrite: false,
@@ -5748,6 +6121,7 @@ function registerOnchainEarnTools() {
5748
6121
  // -------------------------------------------------------------------------
5749
6122
  {
5750
6123
  name: "onchain_earn_get_order_history",
6124
+ title: "On-chain Earn Order History",
5751
6125
  module: "earn.onchain",
5752
6126
  description: "List past staking/DeFi orders including redeemed ones.",
5753
6127
  isWrite: false,
@@ -5846,6 +6220,7 @@ function registerDcdTools() {
5846
6220
  return [
5847
6221
  {
5848
6222
  name: "dcd_get_currency_pairs",
6223
+ title: "Dual Investment List Currency Pairs",
5849
6224
  module: "earn.dcd",
5850
6225
  description: "Get available DCD currency pairs.",
5851
6226
  isWrite: false,
@@ -5863,6 +6238,7 @@ function registerDcdTools() {
5863
6238
  },
5864
6239
  {
5865
6240
  name: "dcd_get_products",
6241
+ title: "Dual Investment List Products",
5866
6242
  module: "earn.dcd",
5867
6243
  description: "Get DCD products with yield and quota info. Yields in response are decimal fractions, not percentages.",
5868
6244
  isWrite: false,
@@ -5893,6 +6269,7 @@ function registerDcdTools() {
5893
6269
  },
5894
6270
  {
5895
6271
  name: "dcd_get_order_state",
6272
+ title: "Dual Investment Get Order State",
5896
6273
  module: "earn.dcd",
5897
6274
  description: "Check DCD order state after subscription (returns ordId + state only). For full order details (productId, strike, yield, settlement info), use dcd_get_orders instead.",
5898
6275
  isWrite: false,
@@ -5917,6 +6294,7 @@ function registerDcdTools() {
5917
6294
  },
5918
6295
  {
5919
6296
  name: "dcd_get_orders",
6297
+ title: "Dual Investment Get Order History",
5920
6298
  module: "earn.dcd",
5921
6299
  description: "Get DCD order history. Yields in response are decimal fractions, not percentages.",
5922
6300
  isWrite: false,
@@ -5961,9 +6339,11 @@ function registerDcdTools() {
5961
6339
  },
5962
6340
  {
5963
6341
  name: "dcd_subscribe",
6342
+ title: "Dual Investment Subscribe",
5964
6343
  module: "earn.dcd",
5965
6344
  description: "Subscribe to a DCD product: get quote and execute atomically. Confirm product, amount, and currency with user before calling. Optional minAnnualizedYield rejects the order if quote yield falls below threshold. Returns order result with quote snapshot (minAnnualizedYield is in percent; response yields are decimal fractions).",
5966
6345
  isWrite: true,
6346
+ destructiveHint: false,
5967
6347
  inputSchema: {
5968
6348
  type: "object",
5969
6349
  properties: {
@@ -6046,9 +6426,11 @@ function registerDcdTools() {
6046
6426
  },
6047
6427
  {
6048
6428
  name: "dcd_redeem",
6429
+ title: "Dual Investment Redeem",
6049
6430
  module: "earn.dcd",
6050
6431
  description: "Early redemption of a DCD order, two-step flow. First call (no quoteId): returns redemption quote for user confirmation. Second call (with quoteId): executes redemption. If the quote expired, auto-refreshes and executes; response includes autoRefreshedQuote: true.",
6051
6432
  isWrite: true,
6433
+ destructiveHint: false,
6052
6434
  inputSchema: {
6053
6435
  type: "object",
6054
6436
  properties: {
@@ -6123,9 +6505,11 @@ function registerAutoEarnTools() {
6123
6505
  return [
6124
6506
  {
6125
6507
  name: "earn_auto_set",
6508
+ title: "Set Auto-Earn Configuration",
6126
6509
  module: "earn.autoearn",
6127
6510
  description: "Enable or disable auto-earn for a currency. earnType='0' for auto-lend+stake (most currencies); earnType='1' for USDG earn (USDG, BUIDL). Use account_get_balance first: if autoLendStatus or autoStakingStatus != 'unsupported', use earnType='0'; for USDG/BUIDL use earnType='1'. [CAUTION] Cannot disable within 24h of enabling.",
6128
6511
  isWrite: true,
6512
+ idempotentHint: true,
6129
6513
  inputSchema: {
6130
6514
  type: "object",
6131
6515
  properties: {
@@ -6175,6 +6559,7 @@ function registerFlashEarnTools() {
6175
6559
  return [
6176
6560
  {
6177
6561
  name: "earn_get_flash_earn_projects",
6562
+ title: "Flash Earn List Projects",
6178
6563
  module: "earn.flash",
6179
6564
  description: "Get Flash Earn projects. Use this to browse upcoming or in-progress Flash Earn opportunities. Do NOT use for purchase or redeem actions - Flash Earn is query-only in this module.",
6180
6565
  isWrite: false,
@@ -6597,6 +6982,7 @@ function registerEventContractTools() {
6597
6982
  // -----------------------------------------------------------------------
6598
6983
  {
6599
6984
  name: "event_browse",
6985
+ title: "Event Contracts Browse Active",
6600
6986
  module: "event",
6601
6987
  description: "Browse currently active (in-progress) event contracts. Call when user asks what event contracts are available to trade. Returns only in-progress contracts (floorStrike set). If a live quote field px is present, it is the event contract price (0.01-0.99), not the underlying asset price; it reflects the market-implied probability when actively trading. Grouped by settlement type and underlying. Do NOT use for querying contracts within a specific series - use event_get_markets with seriesId instead.",
6602
6988
  isWrite: false,
@@ -6637,6 +7023,7 @@ function registerEventContractTools() {
6637
7023
  },
6638
7024
  {
6639
7025
  name: "event_get_series",
7026
+ title: "Event Contracts List Series",
6640
7027
  module: "event",
6641
7028
  description: "List event contract series. Returns all available series with settlement type and underlying. Use event_browse to see currently active contracts.",
6642
7029
  isWrite: false,
@@ -6661,6 +7048,7 @@ function registerEventContractTools() {
6661
7048
  },
6662
7049
  {
6663
7050
  name: "event_get_events",
7051
+ title: "Event Contracts List Events",
6664
7052
  module: "event",
6665
7053
  description: "List expiry periods within a series. state: preopen|live|settling|expired. expTime is pre-formatted UTC+8.",
6666
7054
  isWrite: false,
@@ -6716,6 +7104,7 @@ function registerEventContractTools() {
6716
7104
  },
6717
7105
  {
6718
7106
  name: "event_get_markets",
7107
+ title: "Event Contracts List Markets",
6719
7108
  module: "event",
6720
7109
  description: "List tradeable contracts within a series. state=live for active contracts, state=expired for settlement results. floorStrike=strike price; px (when present) is the event contract price (0.01-0.99), not the underlying asset price - reflects the market-implied probability when actively trading; outcome pre-translated (pending/YES/NO/UP/DOWN); timestamps UTC+8. Do NOT use for discovering what series are available across all underlyings - use event_browse instead.",
6721
7110
  isWrite: false,
@@ -6797,6 +7186,7 @@ function registerEventContractTools() {
6797
7186
  },
6798
7187
  {
6799
7188
  name: "event_get_orders",
7189
+ title: "Event Contracts Get Orders",
6800
7190
  module: "event",
6801
7191
  description: "Query event contract orders (open, 7d history, or 3-month archive). outcome pre-translated (YES/NO/UP/DOWN). Do NOT use for trade executions - use event_get_fills for fill records and settlement outcomes.",
6802
7192
  isWrite: false,
@@ -6855,6 +7245,7 @@ function registerEventContractTools() {
6855
7245
  },
6856
7246
  {
6857
7247
  name: "event_get_fills",
7248
+ title: "Event Contracts Get Fills",
6858
7249
  module: "event",
6859
7250
  description: "Get event contract fill history (trade executions and settlement payouts). archive=true for up to 3mo, false (default) for last 3d. outcome pre-translated (YES/NO/UP/DOWN). Each record includes a 'type' field: 'fill' (opening trade) or 'settlement' (expiry payout with settlementResult win/loss and pnl). Do NOT use for order status - use event_get_orders instead.",
6860
7251
  isWrite: false,
@@ -6902,6 +7293,8 @@ function registerEventContractTools() {
6902
7293
  // -----------------------------------------------------------------------
6903
7294
  {
6904
7295
  name: "event_place_order",
7296
+ title: "Event Contracts Place Order",
7297
+ destructiveHint: false,
6905
7298
  module: "event",
6906
7299
  description: `Place an event contract order. [CAUTION] Places a real order. Before placing, call event_get_markets(seriesId, state=live) to obtain the instId of the target contract.
6907
7300
  - outcome: UP/YES (bet price goes up/condition met) or DOWN/NO (bet price goes down/condition not met)
@@ -6977,6 +7370,8 @@ function registerEventContractTools() {
6977
7370
  },
6978
7371
  {
6979
7372
  name: "event_amend_order",
7373
+ title: "Event Contracts Amend Order",
7374
+ idempotentHint: true,
6980
7375
  module: "event",
6981
7376
  description: "Amend a pending event contract order (change price or size). [CAUTION] Modifies a real order. Before amending, call event_get_orders(status=open) to obtain the ordId and confirm the order is still pending. Only limit/post_only orders can be amended.",
6982
7377
  isWrite: true,
@@ -7008,6 +7403,8 @@ function registerEventContractTools() {
7008
7403
  },
7009
7404
  {
7010
7405
  name: "event_cancel_order",
7406
+ title: "Event Contracts Cancel Order",
7407
+ idempotentHint: true,
7011
7408
  module: "event",
7012
7409
  description: "Cancel a pending event contract order. [CAUTION] Cancels a real order. Before cancelling, call event_get_orders(status=open) to obtain the ordId and confirm the order is still pending. instId must be the full event contract instrument ID (e.g. BTC-ABOVE-DAILY-260224-1600-69700), NOT a spot trading pair.",
7013
7410
  isWrite: true,
@@ -7416,6 +7813,7 @@ function registerSmartmoneyTools() {
7416
7813
  /* ---------- T1. Top traders (leaderboard rank) ---------- */
7417
7814
  {
7418
7815
  name: "smartmoney_get_traders_by_filter",
7816
+ title: "Smart Money Leaderboard",
7419
7817
  module: "smartmoney",
7420
7818
  description: "Leaderboard ranking of OKX smart-money traders, filtered by pool conditions and ranked by `sortBy`. Use when: discovering top performers by criteria (PnL / win-rate / drawdown / AUM). See also: `smartmoney_get_performance_by_trader` (lookup by ID), `smartmoney_search_trader` (lookup by nickname). Note: `updateTime` is 12-digit `yyyyMMddHHmm` UTC+8, different from signal tools' 10-digit UTC `asOfTime`/`dataVersion` - do not cross-pass.",
7421
7819
  isWrite: false,
@@ -7482,6 +7880,7 @@ function registerSmartmoneyTools() {
7482
7880
  /* ---------- T2. Trader performance (by authorIds) ---------- */
7483
7881
  {
7484
7882
  name: "smartmoney_get_performance_by_trader",
7883
+ title: "Smart Money Trader Performance",
7485
7884
  module: "smartmoney",
7486
7885
  description: "PnL / win-rate / drawdown profile for one or more traders looked up by `authorIds`. Use when: caller already has trader IDs and needs their performance metrics. See also: `smartmoney_search_trader` (resolve nickname -> authorId), `smartmoney_get_traders_by_filter` (criteria-based discovery). Note: response `updateTime` is 12-digit `yyyyMMddHHmm` UTC+8 - do not pass to signal-side tools' `asOfTime` (10-digit UTC).",
7487
7886
  isWrite: false,
@@ -7552,6 +7951,7 @@ function registerSmartmoneyTools() {
7552
7951
  /* ---------- T3. Trader current positions ---------- */
7553
7952
  {
7554
7953
  name: "smartmoney_get_trader_positions",
7954
+ title: "Smart Money Trader Current Positions",
7555
7955
  module: "smartmoney",
7556
7956
  description: "Currently-open positions held by a single trader (direction, size, leverage, entry, conviction). Use when: inspecting what a top trader is holding RIGHT NOW. See also: `smartmoney_get_trader_positions_history` (closed positions), `smartmoney_search_trader` (nickname -> authorId), `smartmoney_get_traders_by_filter` (discover trader).",
7557
7957
  isWrite: false,
@@ -7633,6 +8033,7 @@ function registerSmartmoneyTools() {
7633
8033
  /* ---------- T4. Trader closed-position history ---------- */
7634
8034
  {
7635
8035
  name: "smartmoney_get_trader_positions_history",
8036
+ title: "Smart Money Trader Position History",
7636
8037
  module: "smartmoney",
7637
8038
  description: "Closed-position history of a single trader, paginated by `posId` cursor. Use when: studying realized PnL pattern, holding duration, win/loss streaks, or how positions ended (closed vs liquidated). See also: `smartmoney_get_trader_positions` (currently-open), `smartmoney_search_trader` (nickname -> authorId), `smartmoney_get_traders_by_filter` (discover trader).",
7638
8039
  isWrite: false,
@@ -7770,6 +8171,7 @@ function registerSmartmoneyTools() {
7770
8171
  /* ---------- T5. Trader order history ---------- */
7771
8172
  {
7772
8173
  name: "smartmoney_get_trader_orders_history",
8174
+ title: "Smart Money Trader Order History",
7773
8175
  module: "smartmoney",
7774
8176
  description: "Recent orders/fills placed by a single trader (direction, size, price, leverage), paginated by `ordId` cursor. Aligned with the cross-module `*_get_orders` family. Use when: tracking a top trader's latest trade activity. See also: `smartmoney_search_trader` (nickname -> authorId), `smartmoney_get_traders_by_filter` (discover trader).",
7775
8177
  isWrite: false,
@@ -7888,6 +8290,7 @@ function registerSmartmoneyTools() {
7888
8290
  /* ---------- T6. Search top traders by nickname keyword ---------- */
7889
8291
  {
7890
8292
  name: "smartmoney_search_trader",
8293
+ title: "Search Smart Money Top Traders",
7891
8294
  module: "smartmoney",
7892
8295
  description: "Search Top Traders by nickname keyword, ranked by OKX-platform follower count DESC. Returns up to 10 matches; intersects KOL full-text recall with the Top Trader set. Use when: resolving a nickname or partial name to `authorId`(s) before calling other `smartmoney_get_trader_*` tools. See also: `smartmoney_get_traders_by_filter` (discover top performers by criteria), `smartmoney_get_performance_by_trader` (lookup by known authorId).",
7893
8296
  isWrite: false,
@@ -7938,6 +8341,7 @@ function registerSmartmoneyTools() {
7938
8341
  /* ---------- S1. Signal overview by filter (multi-asset, tier-filtered pool) ---------- */
7939
8342
  {
7940
8343
  name: "smartmoney_get_signal_overview_by_filter",
8344
+ title: "Smart Money Consensus Signals by Filter",
7941
8345
  module: "smartmoney",
7942
8346
  description: "Multi-asset smart-money consensus signals (long/short ratio, weighted entry, capital flow, deltas vs 1h/24h/7d), aggregated over a tier-filtered trader pool (PnL / win-rate / drawdown / AUM). Pick instruments via `topInstruments` OR `instCcyList` - exactly one. Snapshot time auto-resolved to current hour. **Linear (USDT/USDS-margined) contracts only - coin-margined (`-USD-SWAP` / `-USD-DELIVERY`) positions are excluded by upstream and silently omitted from the aggregation.** Use when: latest cross-asset consensus from a criteria-defined pool. See also: `smartmoney_get_signal_overview_by_trader` (restrict pool to specific traders), `smartmoney_get_signal_trend_by_filter` (time-series instead of latest snapshot).",
7943
8347
  isWrite: false,
@@ -7998,6 +8402,7 @@ function registerSmartmoneyTools() {
7998
8402
  /* ---------- S2. Signal overview by trader (multi-asset, authorIds-restricted) ---------- */
7999
8403
  {
8000
8404
  name: "smartmoney_get_signal_overview_by_trader",
8405
+ title: "Smart Money Consensus Signals by Trader",
8001
8406
  module: "smartmoney",
8002
8407
  description: "Multi-asset smart-money signals aggregated over a hand-picked set of traders (`authorIds`). Pick instruments via `topInstruments` OR `instCcyList`. Capability tier filters (pnlTier / winRateTier / etc.) not exposed - backend uses defaults for direct-lookup scenarios. **Linear (USDT/USDS-margined) contracts only - a trader's coin-margined (`-USD-SWAP` / `-USD-DELIVERY`) positions are silently excluded from the aggregation, even when those positions are large.** Use `smartmoney_get_trader_positions` if the full position book is needed. Use when: caller already knows which traders to follow and wants their cross-asset consensus at the latest hour. See also: `smartmoney_get_signal_overview_by_filter` (criteria-defined pool), `smartmoney_get_signal_trend_by_trader` (time-series), `smartmoney_get_traders_by_filter` / `smartmoney_search_trader` (discover authorIds).",
8003
8408
  isWrite: false,
@@ -8080,6 +8485,7 @@ function registerSmartmoneyTools() {
8080
8485
  /* ---------- S3. Signal trend by filter (single-asset, tier-filtered pool, asOfTime anchor) ---------- */
8081
8486
  {
8082
8487
  name: "smartmoney_get_signal_trend_by_filter",
8488
+ title: "Smart Money Signal Trend by Filter",
8083
8489
  module: "smartmoney",
8084
8490
  description: "Time-series of single-asset smart-money signal across hourly/daily buckets, aggregated over a tier-filtered trader pool. Returns the latest `limit` buckets ending at `asOfTime` (defaults to current UTC hour). **Linear (USDT/USDS-margined) contracts only - coin-margined (`-USD-SWAP` / `-USD-DELIVERY`) positions are excluded by upstream and silently omitted.** Use when: tracking how long/short conviction and capital evolve over time (smart money adding exposure or retreating). See also: `smartmoney_get_signal_overview_by_filter` (latest snapshot only), `smartmoney_get_signal_trend_by_trader` (restrict to specific traders). Note: `asOfTime` is 10-digit `yyyyMMddHH` UTC, different from leaderboard tools' 12-digit UTC+8 `updateTime` - do not cross-pass.",
8085
8491
  isWrite: false,
@@ -8152,6 +8558,7 @@ function registerSmartmoneyTools() {
8152
8558
  /* ---------- S4. Signal trend by trader (single-asset, authorIds-restricted) ---------- */
8153
8559
  {
8154
8560
  name: "smartmoney_get_signal_trend_by_trader",
8561
+ title: "Smart Money Signal Trend by Trader",
8155
8562
  module: "smartmoney",
8156
8563
  description: "Time-series of single-asset smart-money signal aggregated over a hand-picked set of traders (`authorIds`). Returns the latest `limit` buckets ending at `asOfTime` (defaults to current UTC hour). Capability tier filters (pnlTier / winRateTier / etc.) not exposed - backend uses defaults for direct-lookup scenarios. **Linear (USDT/USDS-margined) contracts only - a trader's coin-margined (`-USD-SWAP` / `-USD-DELIVERY`) positions on the requested base ccy are silently excluded from each bucket.** Use `smartmoney_get_trader_positions` to inspect the full position book. Use when: tracking how a specific group of traders has evolved their long/short consensus over time on one coin. See also: `smartmoney_get_signal_trend_by_filter` (criteria-defined pool), `smartmoney_get_signal_overview_by_trader` (latest snapshot only), `smartmoney_get_traders_by_filter` / `smartmoney_search_trader` (discover authorIds). Note: `asOfTime` is 10-digit `yyyyMMddHH` UTC, different from leaderboard tools' 12-digit UTC+8 `updateTime` - do not cross-pass.",
8157
8564
  isWrite: false,
@@ -8246,7 +8653,7 @@ function registerSmartmoneyTools() {
8246
8653
  return tools;
8247
8654
  }
8248
8655
  function buildContractTradeTools(cfg) {
8249
- const { prefix, module, label, instTypes, instIdExample } = cfg;
8656
+ const { prefix, module, label, instTypes, instIdExample, titleLabel } = cfg;
8250
8657
  const [defaultType, otherType] = instTypes;
8251
8658
  const instTypeDesc = `${defaultType} (default) or ${otherType}`;
8252
8659
  const n = (suffix) => `${prefix}_${suffix}`;
@@ -8254,6 +8661,8 @@ function buildContractTradeTools(cfg) {
8254
8661
  // ── place_order ──────────────────────────────────────────────────────────
8255
8662
  {
8256
8663
  name: n("place_order"),
8664
+ title: `${titleLabel} Place Order`,
8665
+ destructiveHint: false,
8257
8666
  module,
8258
8667
  description: `Place ${label} order. Attach TP/SL via tpTriggerPx/slTriggerPx. Before placing, use market_get_instruments to get ctVal (contract face value) - do NOT assume contract sizes. [CAUTION] Executes real trades.`,
8259
8668
  isWrite: true,
@@ -8350,6 +8759,8 @@ function buildContractTradeTools(cfg) {
8350
8759
  // ── cancel_order ─────────────────────────────────────────────────────────
8351
8760
  {
8352
8761
  name: n("cancel_order"),
8762
+ title: `${titleLabel} Cancel Order`,
8763
+ idempotentHint: true,
8353
8764
  module,
8354
8765
  description: `Cancel an unfilled ${label} order.`,
8355
8766
  isWrite: true,
@@ -8379,6 +8790,7 @@ function buildContractTradeTools(cfg) {
8379
8790
  // ── get_order ─────────────────────────────────────────────────────────────
8380
8791
  {
8381
8792
  name: n("get_order"),
8793
+ title: `${titleLabel} Get Order`,
8382
8794
  module,
8383
8795
  description: `Get details of a single ${label} order by ordId or clOrdId.`,
8384
8796
  isWrite: false,
@@ -8408,6 +8820,7 @@ function buildContractTradeTools(cfg) {
8408
8820
  // ── get_orders ───────────────────────────────────────────────────────────
8409
8821
  {
8410
8822
  name: n("get_orders"),
8823
+ title: `${titleLabel} Get Orders`,
8411
8824
  module,
8412
8825
  description: `Query ${label} open orders, history (last 7 days), or archive (up to 3 months).`,
8413
8826
  isWrite: false,
@@ -8466,6 +8879,7 @@ function buildContractTradeTools(cfg) {
8466
8879
  // ── get_positions ────────────────────────────────────────────────────────
8467
8880
  {
8468
8881
  name: n("get_positions"),
8882
+ title: `${titleLabel} Get Positions`,
8469
8883
  module,
8470
8884
  description: `Get current ${label} positions.`,
8471
8885
  isWrite: false,
@@ -8500,6 +8914,7 @@ function buildContractTradeTools(cfg) {
8500
8914
  // ── get_fills ────────────────────────────────────────────────────────────
8501
8915
  {
8502
8916
  name: n("get_fills"),
8917
+ title: `${titleLabel} Get Fills`,
8503
8918
  module,
8504
8919
  description: `Get ${label} fill details. archive=false (default): last 3 days; archive=true: up to 3 months.`,
8505
8920
  isWrite: false,
@@ -8550,6 +8965,8 @@ function buildContractTradeTools(cfg) {
8550
8965
  // ── close_position ───────────────────────────────────────────────────────
8551
8966
  {
8552
8967
  name: n("close_position"),
8968
+ title: `${titleLabel} Close Position`,
8969
+ idempotentHint: true,
8553
8970
  module,
8554
8971
  description: `[CAUTION] Close entire ${label} position at market.`,
8555
8972
  isWrite: true,
@@ -8592,6 +9009,8 @@ function buildContractTradeTools(cfg) {
8592
9009
  // ── set_leverage ─────────────────────────────────────────────────────────
8593
9010
  {
8594
9011
  name: n("set_leverage"),
9012
+ title: `${titleLabel} Set Leverage`,
9013
+ idempotentHint: true,
8595
9014
  module,
8596
9015
  description: `Set leverage for a ${label} instrument or position. [CAUTION] Changes risk parameters.
8597
9016
  Scenarios (SWAP/FUTURES only):
@@ -8654,6 +9073,7 @@ Not supported: PORTFOLIO MARGIN accounts cannot adjust cross leverage for SWAP/F
8654
9073
  // ── get_leverage ─────────────────────────────────────────────────────────
8655
9074
  {
8656
9075
  name: n("get_leverage"),
9076
+ title: `${titleLabel} Get Leverage`,
8657
9077
  module,
8658
9078
  description: `Get current leverage for a ${label} instrument.`,
8659
9079
  isWrite: false,
@@ -8681,6 +9101,8 @@ Not supported: PORTFOLIO MARGIN accounts cannot adjust cross leverage for SWAP/F
8681
9101
  // ── batch_amend ──────────────────────────────────────────────────────────
8682
9102
  {
8683
9103
  name: n("batch_amend"),
9104
+ title: `${titleLabel} Batch Amend Orders`,
9105
+ idempotentHint: true,
8684
9106
  module,
8685
9107
  description: `[CAUTION] Batch amend up to 20 unfilled ${label} orders.`,
8686
9108
  isWrite: true,
@@ -8712,6 +9134,8 @@ Not supported: PORTFOLIO MARGIN accounts cannot adjust cross leverage for SWAP/F
8712
9134
  // ── batch_cancel ─────────────────────────────────────────────────────────
8713
9135
  {
8714
9136
  name: n("batch_cancel"),
9137
+ title: `${titleLabel} Batch Cancel Orders`,
9138
+ idempotentHint: true,
8715
9139
  module,
8716
9140
  description: `[CAUTION] Batch cancel up to 20 ${label} orders.`,
8717
9141
  isWrite: true,
@@ -8747,6 +9171,7 @@ function registerFuturesTools() {
8747
9171
  prefix: "futures",
8748
9172
  module: "futures",
8749
9173
  label: "FUTURES delivery",
9174
+ titleLabel: "Futures",
8750
9175
  instTypes: ["FUTURES", "SWAP"],
8751
9176
  instIdExample: "e.g. BTC-USDT-240329"
8752
9177
  });
@@ -8756,6 +9181,8 @@ function registerFuturesTools() {
8756
9181
  // Unique to futures: amend a regular (non-algo) unfilled order.
8757
9182
  {
8758
9183
  name: "futures_amend_order",
9184
+ title: "Futures Amend Order",
9185
+ idempotentHint: true,
8759
9186
  module: "futures",
8760
9187
  description: "Amend an unfilled FUTURES delivery order (modify price and/or size). To modify attached TP/SL, use futures_amend_algo_order with the algoId from futures_get_algo_orders.",
8761
9188
  isWrite: true,
@@ -8790,6 +9217,8 @@ function registerFuturesTools() {
8790
9217
  // Unique to futures: batch place only (no cancel/amend action dispatch).
8791
9218
  {
8792
9219
  name: "futures_batch_orders",
9220
+ title: "Futures Batch Place Orders",
9221
+ destructiveHint: false,
8793
9222
  module: "futures",
8794
9223
  description: "[CAUTION] Batch place up to 20 FUTURES delivery orders.",
8795
9224
  isWrite: true,
@@ -8849,6 +9278,7 @@ function registerMarketTools() {
8849
9278
  return [
8850
9279
  {
8851
9280
  name: "market_get_ticker",
9281
+ title: "Get Ticker",
8852
9282
  module: "market",
8853
9283
  description: "Get ticker data for a single instrument.",
8854
9284
  isWrite: false,
@@ -8876,6 +9306,7 @@ function registerMarketTools() {
8876
9306
  },
8877
9307
  {
8878
9308
  name: "market_get_tickers",
9309
+ title: "Get All Tickers",
8879
9310
  module: "market",
8880
9311
  description: "Get ticker data for all instruments of a given type.",
8881
9312
  isWrite: false,
@@ -8915,6 +9346,7 @@ function registerMarketTools() {
8915
9346
  },
8916
9347
  {
8917
9348
  name: "market_get_orderbook",
9349
+ title: "Get Order Book",
8918
9350
  module: "market",
8919
9351
  description: "Get the order book (bids/asks) for an instrument.",
8920
9352
  isWrite: false,
@@ -8949,6 +9381,7 @@ function registerMarketTools() {
8949
9381
  },
8950
9382
  {
8951
9383
  name: "market_get_candles",
9384
+ title: "Get Candlesticks",
8952
9385
  module: "market",
8953
9386
  description: "Get candlestick (OHLCV) data for an instrument. Automatically retrieves historical data (back to 2021) when requesting older time ranges. Use the `after` parameter to paginate back in time (the old `history` parameter has been removed). IMPORTANT: Before fetching with `after`/`before`, estimate the number of candles: time_range_ms / bar_interval_ms. If the estimate exceeds ~500 candles, inform the user of the estimated count and ask for confirmation before proceeding.",
8954
9387
  isWrite: false,
@@ -9005,6 +9438,7 @@ function registerMarketTools() {
9005
9438
  },
9006
9439
  {
9007
9440
  name: "market_get_instruments",
9441
+ title: "List Instruments",
9008
9442
  module: "market",
9009
9443
  description: "Get tradable instruments for a given type. Returns contract specs: min order size, lot size, tick size, contract value, settlement currency, listing/expiry time. Essential before placing orders.",
9010
9444
  isWrite: false,
@@ -9049,6 +9483,7 @@ function registerMarketTools() {
9049
9483
  },
9050
9484
  {
9051
9485
  name: "market_get_funding_rate",
9486
+ title: "Get Funding Rate",
9052
9487
  module: "market",
9053
9488
  description: "Get funding rate for a perpetual SWAP instrument. IMPORTANT: instId must end with -SWAP (e.g. BTC-USDT-SWAP). Spot IDs like BTC-USDT are NOT valid. history=false (default): current rate + next estimated rate; history=true: historical rates.",
9054
9489
  isWrite: false,
@@ -9110,6 +9545,7 @@ function registerMarketTools() {
9110
9545
  },
9111
9546
  {
9112
9547
  name: "market_get_mark_price",
9548
+ title: "Get Mark Price",
9113
9549
  module: "market",
9114
9550
  description: "Get mark price for SWAP, FUTURES, or MARGIN instruments. Used for liquidation calculations and unrealized PnL.",
9115
9551
  isWrite: false,
@@ -9153,6 +9589,7 @@ function registerMarketTools() {
9153
9589
  },
9154
9590
  {
9155
9591
  name: "market_get_trades",
9592
+ title: "Get Recent Trades",
9156
9593
  module: "market",
9157
9594
  description: "Get recent trades for an instrument. Default 20 records, max 500.",
9158
9595
  isWrite: false,
@@ -9187,6 +9624,7 @@ function registerMarketTools() {
9187
9624
  },
9188
9625
  {
9189
9626
  name: "market_get_index_ticker",
9627
+ title: "Get Index Ticker",
9190
9628
  module: "market",
9191
9629
  description: "Get index ticker data (e.g. BTC-USD, ETH-USD index prices). Independent of any single exchange.",
9192
9630
  isWrite: false,
@@ -9220,6 +9658,7 @@ function registerMarketTools() {
9220
9658
  },
9221
9659
  {
9222
9660
  name: "market_get_index_candles",
9661
+ title: "Get Index Candlesticks",
9223
9662
  module: "market",
9224
9663
  description: "Get candlestick data for an index (e.g. BTC-USD index). history=false: recent up to 1440 bars; history=true: older data.",
9225
9664
  isWrite: false,
@@ -9276,6 +9715,7 @@ function registerMarketTools() {
9276
9715
  },
9277
9716
  {
9278
9717
  name: "market_get_price_limit",
9718
+ title: "Get Price Limit",
9279
9719
  module: "market",
9280
9720
  description: "Get the current price limit (upper and lower bands) for a SWAP or FUTURES instrument. Orders outside these limits will be rejected.",
9281
9721
  isWrite: false,
@@ -9303,6 +9743,7 @@ function registerMarketTools() {
9303
9743
  },
9304
9744
  {
9305
9745
  name: "market_get_open_interest",
9746
+ title: "Get Open Interest",
9306
9747
  module: "market",
9307
9748
  description: "Get open interest for SWAP, FUTURES, or OPTION instruments. Useful for gauging market sentiment and positioning.",
9308
9749
  isWrite: false,
@@ -9346,6 +9787,7 @@ function registerMarketTools() {
9346
9787
  },
9347
9788
  {
9348
9789
  name: "market_get_stock_tokens",
9790
+ title: "List Stock Tokens",
9349
9791
  module: "market",
9350
9792
  description: '[Deprecated: use market_get_instruments_by_category with instCategory="3" instead] Get all stock token instruments (instCategory=3). Stock tokens track real-world stock prices on OKX (e.g. AAPL-USDT-SWAP).',
9351
9793
  isWrite: false,
@@ -9382,6 +9824,7 @@ function registerMarketTools() {
9382
9824
  },
9383
9825
  {
9384
9826
  name: "market_get_instruments_by_category",
9827
+ title: "List Instruments by Category",
9385
9828
  module: "market",
9386
9829
  description: "Discover tradeable instruments by asset category. Stock tokens (instCategory=3, e.g. AAPL-USDT-SWAP, TSLA-USDT-SWAP), Metals (4, e.g. XAUUSDT-USDT-SWAP for gold), Commodities (5, e.g. OIL-USDT-SWAP for crude oil), Forex (6, e.g. EURUSDT-USDT-SWAP for EUR/USD), Bonds (7, e.g. US30Y-USDT-SWAP for crude oil). Use this to find instIds before querying prices or placing orders. Filters client-side by instCategory.",
9387
9830
  isWrite: false,
@@ -9432,6 +9875,7 @@ function registerMarketFilterTools() {
9432
9875
  // ─────────────────────────────────────────────────────────────────────────
9433
9876
  {
9434
9877
  name: "market_filter",
9878
+ title: "Screen Instruments",
9435
9879
  module: "market",
9436
9880
  description: "Screen / rank instruments across SPOT, SWAP, or FUTURES by multi-dimensional criteria: price range, 24h change %, market cap, 24h volume (USD), funding rate (SWAP), open interest (USD), listing time. Returns ranked rows with full ticker snapshot. Use to find top movers, high-OI contracts, newly listed tokens, etc. No credentials required. Do NOT use to get OI change rankings across contracts - use market_filter_oi_change instead. Do NOT use to get OI time series for a single instrument - use market_get_oi_history instead.",
9437
9881
  isWrite: false,
@@ -9569,6 +10013,7 @@ function registerMarketFilterTools() {
9569
10013
  // ─────────────────────────────────────────────────────────────────────────
9570
10014
  {
9571
10015
  name: "market_get_oi_history",
10016
+ title: "Get Open Interest History",
9572
10017
  module: "market",
9573
10018
  description: "Get open interest (OI) history time series for a single SWAP or FUTURES instrument. Returns per-bar OI in contracts, base currency and USD, plus bar-over-bar delta and delta %. Useful for tracking how OI evolves around price moves. No credentials required. Do NOT use to compare OI changes across multiple contracts - use market_filter_oi_change instead. Do NOT use to screen instruments by current OI level - use market_filter instead.",
9574
10019
  isWrite: false,
@@ -9616,6 +10061,7 @@ function registerMarketFilterTools() {
9616
10061
  // ─────────────────────────────────────────────────────────────────────────
9617
10062
  {
9618
10063
  name: "market_filter_oi_change",
10064
+ title: "Find Open Interest Change Instruments",
9619
10065
  module: "market",
9620
10066
  description: "Find SWAP or FUTURES instruments with significant open interest changes over a given bar window. Returns ranked rows with current OI (USD), previous OI (USD), OI delta (USD and %), price change %, 24h volume and funding rate. Ideal for spotting unusual accumulation/distribution or confirming trend momentum. No credentials required. Do NOT use to get OI time series for a single instrument - use market_get_oi_history instead. Do NOT use to screen by current OI absolute level or other non-OI metrics - use market_filter instead.",
9621
10067
  isWrite: false,
@@ -9688,6 +10134,7 @@ function registerMarketFilterTools() {
9688
10134
  // ─────────────────────────────────────────────────────────────────────────
9689
10135
  {
9690
10136
  name: "market_get_pair_spread",
10137
+ title: "Get Pair Spread",
9691
10138
  module: "market",
9692
10139
  description: "Compute spread statistics between two instruments over a lookback window. Returns absolute and ratio spread (mean / stdDev / median / min / max) plus an optional realtime spread snapshot. Use to size pairs trades, detect mean-reversion setups, or compare cross-listed contracts. Results are cached ~60s per (pair, bar, window) tuple. Read-only, no credentials required.\nDo NOT use to fetch raw candles (use market_get_candles) or single-instrument OI/funding (use market_get_oi_history / market_filter_oi_change).",
9693
10140
  isWrite: false,
@@ -9980,6 +10427,7 @@ function registerNewsTools() {
9980
10427
  // -----------------------------------------------------------------------
9981
10428
  {
9982
10429
  name: "news_get_latest",
10430
+ title: "Get Latest Crypto News",
9983
10431
  module: "news",
9984
10432
  description: "Get crypto news sorted by time. For broad browsing ('what happened recently', 'latest news', 'any big news today'), pass importance='low' to include both high and low importance. Server default (when importance omitted) returns only high-importance news. For coin-specific news, use news_get_by_coin instead.",
9985
10433
  isWrite: false,
@@ -10025,6 +10473,7 @@ function registerNewsTools() {
10025
10473
  },
10026
10474
  {
10027
10475
  name: "news_get_by_coin",
10476
+ title: "Get News by Coin",
10028
10477
  module: "news",
10029
10478
  description: "Get news for specific coins or tokens. Use when user mentions a coin: 'BTC news', 'any SOL updates'. Supports multiple coins (comma-separated). For general browsing without a coin filter, use news_get_latest.",
10030
10479
  isWrite: false,
@@ -10068,6 +10517,7 @@ function registerNewsTools() {
10068
10517
  },
10069
10518
  {
10070
10519
  name: "news_search",
10520
+ title: "Search News",
10071
10521
  module: "news",
10072
10522
  description: "Search crypto news by keyword with optional filters. Use when user provides specific search terms: 'SEC ETF news', 'stablecoin regulation'. Keyword is optional - pass sentiment alone to browse by sentiment direction. For coin-only queries prefer news_get_by_coin.",
10073
10523
  isWrite: false,
@@ -10125,6 +10575,7 @@ function registerNewsTools() {
10125
10575
  },
10126
10576
  {
10127
10577
  name: "news_get_detail",
10578
+ title: "Get News Article",
10128
10579
  module: "news",
10129
10580
  description: "Get full article content by news ID (returns title + summary + full original text). Use when user says 'show full article', 'read more', or provides a specific news ID from a previous result.",
10130
10581
  isWrite: false,
@@ -10156,6 +10607,7 @@ function registerNewsTools() {
10156
10607
  },
10157
10608
  {
10158
10609
  name: "news_get_domains",
10610
+ title: "List News Sources",
10159
10611
  module: "news",
10160
10612
  description: "List available news source domains (e.g. coindesk, cointelegraph). Use when user asks what news sources are available or which platforms are covered.",
10161
10613
  isWrite: false,
@@ -10178,6 +10630,7 @@ function registerNewsTools() {
10178
10630
  // -----------------------------------------------------------------------
10179
10631
  {
10180
10632
  name: "news_get_coin_sentiment",
10633
+ title: "Get Coin Sentiment",
10181
10634
  module: "news",
10182
10635
  description: "Get sentiment snapshot or time-series trend for coins. Returns bullish/bearish ratios and mention counts. Pass trendPoints for trend data (1h->24 points, 4h->6, 24h->7). Use when user asks about coin sentiment, sentiment trend, or how bullish/bearish a coin is. For ranking all coins by sentiment, use news_get_sentiment_ranking instead.",
10183
10636
  isWrite: false,
@@ -10219,6 +10672,7 @@ function registerNewsTools() {
10219
10672
  },
10220
10673
  {
10221
10674
  name: "news_get_sentiment_ranking",
10675
+ title: "Get Sentiment Ranking",
10222
10676
  module: "news",
10223
10677
  description: "Get coin ranking by social hotness or sentiment direction. Use when user asks which coins are trending, most bullish/bearish coins. Sort by hot (mention count), bullish, or bearish. For sentiment data on a specific coin, use news_get_coin_sentiment instead.",
10224
10678
  isWrite: false,
@@ -10258,6 +10712,7 @@ function registerNewsTools() {
10258
10712
  // -----------------------------------------------------------------------
10259
10713
  {
10260
10714
  name: "news_list_calendar_regions",
10715
+ title: "List Calendar Regions",
10261
10716
  module: "news",
10262
10717
  description: "List all valid region values for the economic calendar. Returns a string array of snake_case region codes. Call this when economic-calendar returns empty results to verify the region value, or to help the user pick a valid region. Do NOT use to list news source platforms - use news_get_domains instead.",
10263
10718
  isWrite: false,
@@ -10266,6 +10721,7 @@ function registerNewsTools() {
10266
10721
  },
10267
10722
  {
10268
10723
  name: "news_get_economic_calendar",
10724
+ title: "Get Economic Calendar",
10269
10725
  module: "news",
10270
10726
  description: "Get macro-economic calendar data (GDP, CPI, NFP, interest rate decisions, PMI, etc.). Returns scheduled and released economic events with forecast, previous, and actual values. Use when user asks about economic calendar, macro data, or specific indicators like NFP/CPI/GDP/FOMC. Do NOT use for news articles or sentiment - use news_get_latest or news_search instead.",
10271
10727
  isWrite: false,
@@ -10326,6 +10782,8 @@ function registerOptionAlgoTools() {
10326
10782
  return [
10327
10783
  {
10328
10784
  name: "option_place_algo_order",
10785
+ title: "Option Place Algo Order",
10786
+ destructiveHint: false,
10329
10787
  module: "option",
10330
10788
  description: "Place OPTION TP/SL algo order (conditional/oco). [CAUTION] Executes real trades. conditional=single TP/SL; oco=TP+SL pair. -1=market.",
10331
10789
  isWrite: true,
@@ -10432,6 +10890,8 @@ function registerOptionAlgoTools() {
10432
10890
  },
10433
10891
  {
10434
10892
  name: "option_amend_algo_order",
10893
+ title: "Option Amend Algo Order",
10894
+ idempotentHint: true,
10435
10895
  module: "option",
10436
10896
  description: "Amend a pending OPTION algo order (modify TP/SL prices or size). Also covers TP/SL orders attached when placing the main order - look up algoId via option_get_algo_orders first.",
10437
10897
  isWrite: true,
@@ -10468,6 +10928,8 @@ function registerOptionAlgoTools() {
10468
10928
  },
10469
10929
  {
10470
10930
  name: "option_cancel_algo_orders",
10931
+ title: "Option Cancel Algo Orders",
10932
+ idempotentHint: true,
10471
10933
  module: "option",
10472
10934
  description: "Cancel OPTION algo orders (TP/SL). Each item: {algoId, instId}.",
10473
10935
  isWrite: true,
@@ -10511,6 +10973,7 @@ function registerOptionAlgoTools() {
10511
10973
  },
10512
10974
  {
10513
10975
  name: "option_get_algo_orders",
10976
+ title: "Option Get Algo Orders",
10514
10977
  module: "option",
10515
10978
  description: "Query pending or completed OPTION algo orders (TP/SL, OCO).",
10516
10979
  isWrite: false,
@@ -10595,6 +11058,8 @@ function registerOptionTools() {
10595
11058
  return [
10596
11059
  {
10597
11060
  name: "option_place_order",
11061
+ title: "Option Place Order",
11062
+ destructiveHint: false,
10598
11063
  module: "option",
10599
11064
  description: "Place OPTION order. instId: {uly}-{expiry}-{strike}-C/P, e.g. BTC-USD-241227-50000-C. Before placing, use market_get_instruments to get ctVal (contract face value) - do NOT assume contract sizes. [CAUTION] Executes real trades.",
10600
11065
  isWrite: true,
@@ -10698,6 +11163,8 @@ function registerOptionTools() {
10698
11163
  },
10699
11164
  {
10700
11165
  name: "option_cancel_order",
11166
+ title: "Option Cancel Order",
11167
+ idempotentHint: true,
10701
11168
  module: "option",
10702
11169
  description: "Cancel an unfilled OPTION order. Provide ordId or clOrdId.",
10703
11170
  isWrite: true,
@@ -10726,6 +11193,8 @@ function registerOptionTools() {
10726
11193
  },
10727
11194
  {
10728
11195
  name: "option_batch_cancel",
11196
+ title: "Option Batch Cancel Orders",
11197
+ idempotentHint: true,
10729
11198
  module: "option",
10730
11199
  description: "[CAUTION] Batch cancel up to 20 OPTION orders.",
10731
11200
  isWrite: true,
@@ -10756,6 +11225,8 @@ function registerOptionTools() {
10756
11225
  },
10757
11226
  {
10758
11227
  name: "option_amend_order",
11228
+ title: "Option Amend Order",
11229
+ idempotentHint: true,
10759
11230
  module: "option",
10760
11231
  description: "Amend an unfilled OPTION order (price and/or size). Provide ordId or clOrdId. To modify attached TP/SL, use option_amend_algo_order with the algoId from option_get_algo_orders.",
10761
11232
  isWrite: true,
@@ -10788,6 +11259,7 @@ function registerOptionTools() {
10788
11259
  },
10789
11260
  {
10790
11261
  name: "option_get_order",
11262
+ title: "Option Get Order",
10791
11263
  module: "option",
10792
11264
  description: "Get details of a single OPTION order by ordId or clOrdId.",
10793
11265
  isWrite: false,
@@ -10816,6 +11288,7 @@ function registerOptionTools() {
10816
11288
  },
10817
11289
  {
10818
11290
  name: "option_get_orders",
11291
+ title: "Option Get Orders",
10819
11292
  module: "option",
10820
11293
  description: "List OPTION orders. status: live=pending (default), history=7d, archive=3mo.",
10821
11294
  isWrite: false,
@@ -10870,6 +11343,7 @@ function registerOptionTools() {
10870
11343
  },
10871
11344
  {
10872
11345
  name: "option_get_positions",
11346
+ title: "Option Get Positions with Greeks",
10873
11347
  module: "option",
10874
11348
  description: "Get current OPTION positions including Greeks (delta, gamma, theta, vega).",
10875
11349
  isWrite: false,
@@ -10896,6 +11370,7 @@ function registerOptionTools() {
10896
11370
  },
10897
11371
  {
10898
11372
  name: "option_get_fills",
11373
+ title: "Option Get Fills",
10899
11374
  module: "option",
10900
11375
  description: "Get OPTION fill history. archive=false: last 3 days (default); archive=true: up to 3 months.",
10901
11376
  isWrite: false,
@@ -10938,6 +11413,7 @@ function registerOptionTools() {
10938
11413
  },
10939
11414
  {
10940
11415
  name: "option_get_instruments",
11416
+ title: "Option List Instruments (Chain)",
10941
11417
  module: "option",
10942
11418
  description: "List available OPTION contracts for a given underlying (option chain). Use to find valid instIds before placing orders.",
10943
11419
  isWrite: false,
@@ -10971,6 +11447,7 @@ function registerOptionTools() {
10971
11447
  },
10972
11448
  {
10973
11449
  name: "option_get_greeks",
11450
+ title: "Option Get Greeks",
10974
11451
  module: "option",
10975
11452
  description: "Get implied volatility and Greeks (delta, gamma, theta, vega) for OPTION contracts by underlying.",
10976
11453
  isWrite: false,
@@ -11007,6 +11484,8 @@ function registerSpotTradeTools() {
11007
11484
  return [
11008
11485
  {
11009
11486
  name: "spot_place_order",
11487
+ title: "Spot Place Order",
11488
+ destructiveHint: false,
11010
11489
  module: "spot",
11011
11490
  description: "Place a spot order. Attach TP/SL via tpTriggerPx/slTriggerPx. [CAUTION] Executes real trades.",
11012
11491
  isWrite: true,
@@ -11101,6 +11580,8 @@ function registerSpotTradeTools() {
11101
11580
  },
11102
11581
  {
11103
11582
  name: "spot_cancel_order",
11583
+ title: "Spot Cancel Order",
11584
+ idempotentHint: true,
11104
11585
  module: "spot",
11105
11586
  description: "Cancel an unfilled spot order.",
11106
11587
  isWrite: true,
@@ -11136,6 +11617,8 @@ function registerSpotTradeTools() {
11136
11617
  },
11137
11618
  {
11138
11619
  name: "spot_amend_order",
11620
+ title: "Spot Amend Order",
11621
+ idempotentHint: true,
11139
11622
  module: "spot",
11140
11623
  description: "Amend an unfilled spot order (modify price or size). To modify attached TP/SL, use the corresponding algo amend tool: spot_amend_algo_order (spot), swap_amend_algo_order (swap), futures_amend_algo_order (futures), option_amend_algo_order (option). Use spot_get_algo_orders to find the algoId for spot orders.",
11141
11624
  isWrite: true,
@@ -11186,6 +11669,7 @@ function registerSpotTradeTools() {
11186
11669
  },
11187
11670
  {
11188
11671
  name: "spot_get_orders",
11672
+ title: "Spot Get Orders",
11189
11673
  module: "spot",
11190
11674
  description: "Query spot orders. status: open(active)|history(7d)|archive(3mo).",
11191
11675
  isWrite: false,
@@ -11254,6 +11738,8 @@ function registerSpotTradeTools() {
11254
11738
  },
11255
11739
  {
11256
11740
  name: "spot_place_algo_order",
11741
+ title: "Spot Place Algo Order",
11742
+ destructiveHint: false,
11257
11743
  module: "spot",
11258
11744
  description: "Place a spot algo order. [CAUTION] Executes real trades. conditional: single TP/SL. oco: TP+SL pair. move_order_stop: trailing stop. trigger: pending order at triggerPx. chase: follow best bid/ask. iceberg: split large order into child orders. twap: time-weighted split.",
11259
11745
  isWrite: true,
@@ -11369,6 +11855,8 @@ function registerSpotTradeTools() {
11369
11855
  },
11370
11856
  {
11371
11857
  name: "spot_amend_algo_order",
11858
+ title: "Spot Amend Algo Order",
11859
+ idempotentHint: true,
11372
11860
  module: "spot",
11373
11861
  description: "Amend a pending spot algo order (modify TP/SL prices or size). Also covers TP/SL orders attached when placing the main order - look up algoId via spot_get_algo_orders first.",
11374
11862
  isWrite: true,
@@ -11405,6 +11893,8 @@ function registerSpotTradeTools() {
11405
11893
  },
11406
11894
  {
11407
11895
  name: "spot_cancel_algo_order",
11896
+ title: "Spot Cancel Algo Order",
11897
+ idempotentHint: true,
11408
11898
  module: "spot",
11409
11899
  description: "Cancel a spot algo order (TP/SL).",
11410
11900
  isWrite: true,
@@ -11438,6 +11928,7 @@ function registerSpotTradeTools() {
11438
11928
  },
11439
11929
  {
11440
11930
  name: "spot_get_algo_orders",
11931
+ title: "Spot Get Algo Orders",
11441
11932
  module: "spot",
11442
11933
  description: "Query spot algo orders (TP/SL) - pending or history.",
11443
11934
  isWrite: false,
@@ -11515,6 +12006,7 @@ function registerSpotTradeTools() {
11515
12006
  },
11516
12007
  {
11517
12008
  name: "spot_get_fills",
12009
+ title: "Spot Get Fills",
11518
12010
  module: "spot",
11519
12011
  description: "Get spot transaction fills. archive=false(3d, default)|true(up to 3mo).",
11520
12012
  isWrite: false,
@@ -11578,6 +12070,7 @@ function registerSpotTradeTools() {
11578
12070
  },
11579
12071
  {
11580
12072
  name: "spot_batch_orders",
12073
+ title: "Spot Batch Orders",
11581
12074
  module: "spot",
11582
12075
  description: "[CAUTION] Batch place/cancel/amend up to 20 spot orders. action: place|cancel|amend.",
11583
12076
  isWrite: true,
@@ -11636,6 +12129,7 @@ function registerSpotTradeTools() {
11636
12129
  },
11637
12130
  {
11638
12131
  name: "spot_get_order",
12132
+ title: "Spot Get Order",
11639
12133
  module: "spot",
11640
12134
  description: "Get details of a single spot order.",
11641
12135
  isWrite: false,
@@ -11673,6 +12167,8 @@ function registerSpotTradeTools() {
11673
12167
  },
11674
12168
  {
11675
12169
  name: "spot_batch_amend",
12170
+ title: "Spot Batch Amend Orders",
12171
+ idempotentHint: true,
11676
12172
  module: "spot",
11677
12173
  description: "[CAUTION] Batch amend up to 20 unfilled spot orders.",
11678
12174
  isWrite: true,
@@ -11703,6 +12199,8 @@ function registerSpotTradeTools() {
11703
12199
  },
11704
12200
  {
11705
12201
  name: "spot_batch_cancel",
12202
+ title: "Spot Batch Cancel Orders",
12203
+ idempotentHint: true,
11706
12204
  module: "spot",
11707
12205
  description: "[CAUTION] Batch cancel up to 20 spot orders.",
11708
12206
  isWrite: true,
@@ -11740,6 +12238,8 @@ function registerSpotTradeTools() {
11740
12238
  // Not applicable: posSide (spot has no long/short hedge).
11741
12239
  {
11742
12240
  name: "spot_set_leverage",
12241
+ title: "Spot Set Leverage",
12242
+ idempotentHint: true,
11743
12243
  module: "spot",
11744
12244
  description: "Set leverage for SPOT margin trading. Provide exactly ONE of instId (pair-level) or ccy (currency-level cross, requires borrow-enabled account / multi-ccy / portfolio margin). [CAUTION] Changes risk parameters.\nScenarios:\n \u2022 instId + mgnMode=isolated -> pair-level isolated margin\n \u2022 instId + mgnMode=cross -> pair-level cross margin (contract-mode account)\n \u2022 ccy + mgnMode=cross -> currency-level cross margin (spot-with-borrow / multi-ccy / portfolio margin)\nWhen ccy is supplied, mgnMode MUST be cross. posSide is never applicable to spot margin.",
11745
12245
  isWrite: true,
@@ -11814,6 +12314,7 @@ function registerSwapTradeTools() {
11814
12314
  prefix: "swap",
11815
12315
  module: "swap",
11816
12316
  label: "SWAP/FUTURES",
12317
+ titleLabel: "Perpetual Futures",
11817
12318
  instTypes: ["SWAP", "FUTURES"],
11818
12319
  instIdExample: "e.g. BTC-USDT-SWAP"
11819
12320
  });
@@ -11823,6 +12324,8 @@ function registerSwapTradeTools() {
11823
12324
  // Unique to swap: amend a pending TP/SL algo order attached to a position.
11824
12325
  {
11825
12326
  name: "swap_amend_algo_order",
12327
+ title: "Perpetual Futures Amend Algo Order",
12328
+ idempotentHint: true,
11826
12329
  module: "swap",
11827
12330
  description: "Amend a pending SWAP/FUTURES algo order (modify TP/SL prices or size). Also covers TP/SL orders attached when placing the main order - look up algoId via swap_get_algo_orders first.",
11828
12331
  isWrite: true,
@@ -11861,6 +12364,7 @@ function registerSwapTradeTools() {
11861
12364
  // Unique to swap: 3-in-1 batch tool (place / cancel / amend via action param).
11862
12365
  {
11863
12366
  name: "swap_batch_orders",
12367
+ title: "Perpetual Futures Batch Orders",
11864
12368
  module: "swap",
11865
12369
  description: "[CAUTION] Batch place/cancel/amend SWAP/FUTURES orders (max 20). action=place|cancel|amend.",
11866
12370
  isWrite: true,
@@ -11954,12 +12458,12 @@ function createToolRunner(client, config) {
11954
12458
  };
11955
12459
  }
11956
12460
  function configFilePath() {
11957
- return join7(homedir5(), ".okx", "config.toml");
12461
+ return join9(homedir5(), ".okx", "config.toml");
11958
12462
  }
11959
12463
  function readFullConfig() {
11960
12464
  const path42 = configFilePath();
11961
12465
  if (!existsSync3(path42)) return { profiles: {} };
11962
- const raw = readFileSync4(path42, "utf-8");
12466
+ const raw = readFileSync6(path42, "utf-8");
11963
12467
  try {
11964
12468
  return parse(raw);
11965
12469
  } catch (err) {
@@ -12109,7 +12613,7 @@ async function loadConfig(cli) {
12109
12613
  verbose: cli.verbose ?? false
12110
12614
  };
12111
12615
  }
12112
- var CACHE_FILE = join8(homedir6(), ".okx", "update-check.json");
12616
+ var CACHE_FILE = join10(homedir6(), ".okx", "update-check.json");
12113
12617
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
12114
12618
  var NEGATIVE_CHECK_INTERVAL_MS = 60 * 60 * 1e3;
12115
12619
  var DEFAULT_REGISTRY = "https://registry.npmjs.org/";
@@ -12117,7 +12621,7 @@ var FETCH_TIMEOUT_MS = 3e3;
12117
12621
  function readCache2() {
12118
12622
  try {
12119
12623
  if (existsSync4(CACHE_FILE)) {
12120
- return JSON.parse(readFileSync5(CACHE_FILE, "utf-8"));
12624
+ return JSON.parse(readFileSync7(CACHE_FILE, "utf-8"));
12121
12625
  }
12122
12626
  } catch {
12123
12627
  }
@@ -12125,7 +12629,7 @@ function readCache2() {
12125
12629
  }
12126
12630
  function writeCache2(cache) {
12127
12631
  try {
12128
- mkdirSync6(join8(homedir6(), ".okx"), { recursive: true });
12632
+ mkdirSync6(join10(homedir6(), ".okx"), { recursive: true });
12129
12633
  writeFileSync5(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
12130
12634
  } catch {
12131
12635
  }
@@ -12155,13 +12659,13 @@ function buildNpmrcCandidates() {
12155
12659
  let dir = process.cwd();
12156
12660
  const root = dir.startsWith("/") ? "/" : dir.slice(0, 3);
12157
12661
  while (true) {
12158
- add(join8(dir, ".npmrc"));
12662
+ add(join10(dir, ".npmrc"));
12159
12663
  if (dir === root) break;
12160
- const parent = join8(dir, "..");
12664
+ const parent = join10(dir, "..");
12161
12665
  if (parent === dir) break;
12162
12666
  dir = parent;
12163
12667
  }
12164
- add(join8(homedir6(), ".npmrc"));
12668
+ add(join10(homedir6(), ".npmrc"));
12165
12669
  if (process.platform !== "win32") {
12166
12670
  add("/etc/npmrc");
12167
12671
  }
@@ -12170,7 +12674,7 @@ function buildNpmrcCandidates() {
12170
12674
  function readNpmrcRegistry(filePath) {
12171
12675
  try {
12172
12676
  if (!existsSync4(filePath)) return null;
12173
- const lines = readFileSync5(filePath, "utf-8").split(/\r?\n/);
12677
+ const lines = readFileSync7(filePath, "utf-8").split(/\r?\n/);
12174
12678
  for (const line of lines) {
12175
12679
  const trimmed = line.trim();
12176
12680
  if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -12497,6 +13001,14 @@ function runSetup(options) {
12497
13001
  `);
12498
13002
  }
12499
13003
  }
13004
+ var HttpStatusError = class extends Error {
13005
+ statusCode;
13006
+ constructor(statusCode) {
13007
+ super(`HTTP ${statusCode}`);
13008
+ this.name = "HttpStatusError";
13009
+ this.statusCode = statusCode;
13010
+ }
13011
+ };
12500
13012
  function isRedirect(statusCode) {
12501
13013
  return statusCode !== void 0 && statusCode >= 300 && statusCode < 400;
12502
13014
  }
@@ -12511,7 +13023,7 @@ function validateRedirect(res, requestUrl, redirectCount, maxRedirects) {
12511
13023
  return location;
12512
13024
  }
12513
13025
  function fetchResponse(url, timeoutMs) {
12514
- return new Promise((resolve3, reject) => {
13026
+ return new Promise((resolve4, reject) => {
12515
13027
  let redirects = 0;
12516
13028
  const maxRedirects = 5;
12517
13029
  function doRequest(requestUrl) {
@@ -12529,10 +13041,14 @@ function fetchResponse(url, timeoutMs) {
12529
13041
  return;
12530
13042
  }
12531
13043
  if (res.statusCode !== 200) {
12532
- reject(new Error(`HTTP ${res.statusCode ?? "unknown"}`));
13044
+ if (res.statusCode === void 0) {
13045
+ reject(new Error("HTTP unknown"));
13046
+ } else {
13047
+ reject(new HttpStatusError(res.statusCode));
13048
+ }
12533
13049
  return;
12534
13050
  }
12535
- resolve3(res);
13051
+ resolve4(res);
12536
13052
  });
12537
13053
  req.on("error", reject);
12538
13054
  req.on("timeout", () => {
@@ -12545,10 +13061,10 @@ function fetchResponse(url, timeoutMs) {
12545
13061
  }
12546
13062
  function download(url, destPath, timeoutMs) {
12547
13063
  return fetchResponse(url, timeoutMs).then(
12548
- (res) => new Promise((resolve3, reject) => {
13064
+ (res) => new Promise((resolve4, reject) => {
12549
13065
  const file = createWriteStream2(destPath);
12550
13066
  res.pipe(file);
12551
- file.on("finish", () => file.close(() => resolve3()));
13067
+ file.on("finish", () => file.close(() => resolve4()));
12552
13068
  file.on("error", (err) => {
12553
13069
  try {
12554
13070
  unlinkSync3(destPath);
@@ -12561,10 +13077,10 @@ function download(url, destPath, timeoutMs) {
12561
13077
  }
12562
13078
  function downloadText(url, timeoutMs) {
12563
13079
  return fetchResponse(url, timeoutMs).then(
12564
- (res) => new Promise((resolve3, reject) => {
13080
+ (res) => new Promise((resolve4, reject) => {
12565
13081
  const chunks = [];
12566
13082
  res.on("data", (chunk) => chunks.push(chunk));
12567
- res.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
13083
+ res.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
12568
13084
  res.on("error", reject);
12569
13085
  })
12570
13086
  );
@@ -12593,10 +13109,10 @@ function getBinaryName() {
12593
13109
  return platform() === "win32" ? "okx-pilot.exe" : "okx-pilot";
12594
13110
  }
12595
13111
  function hashFile(filePath) {
12596
- const buf = readFileSync7(filePath);
13112
+ const buf = readFileSync9(filePath);
12597
13113
  return {
12598
13114
  size: buf.byteLength,
12599
- sha256: createHash("sha256").update(buf).digest("hex")
13115
+ sha256: createHash3("sha256").update(buf).digest("hex")
12600
13116
  };
12601
13117
  }
12602
13118
  function getPilotStatus(binaryPath, opts) {
@@ -12711,7 +13227,7 @@ async function installPilotBinary(destPath, sources = CDN_SOURCES, onProgress) {
12711
13227
  if (earlyResult) return earlyResult;
12712
13228
  const platformDir = getPlatformDir();
12713
13229
  const binaryName = getBinaryName();
12714
- const resolvedDest = destPath ?? join10(homedir8(), ".okx", "bin", binaryName);
13230
+ const resolvedDest = destPath ?? join12(homedir8(), ".okx", "bin", binaryName);
12715
13231
  const tmpPath = resolvedDest + ".tmp";
12716
13232
  mkdirSync8(dirname6(resolvedDest), { recursive: true });
12717
13233
  const localHash = existsSync6(resolvedDest) ? hashFile(resolvedDest) : null;
@@ -12763,9 +13279,41 @@ function removePilotBinary(binaryPath) {
12763
13279
  }
12764
13280
  }
12765
13281
  var AUTH_CDN_PATH_PREFIX = "/upgradeapp/tools/oauth";
13282
+ var LINUX_ARM64_FALLBACK_DIR = "linux-x64";
12766
13283
  function getAuthBinaryName() {
12767
13284
  return platform2() === "win32" ? "okx-auth.exe" : "okx-auth";
12768
13285
  }
13286
+ async function _resolveAuthPlatformFromNative(native, sources, timeoutMs) {
13287
+ if (native !== "linux-arm64") {
13288
+ return native;
13289
+ }
13290
+ const checksumPath = `${AUTH_CDN_PATH_PREFIX}/linux-arm64/checksum.json`;
13291
+ let got404 = false;
13292
+ for (const { host, protocol } of sources) {
13293
+ const url = `${protocol}://${host}${checksumPath}`;
13294
+ try {
13295
+ await downloadText(url, timeoutMs);
13296
+ return "linux-arm64";
13297
+ } catch (err) {
13298
+ if (err instanceof HttpStatusError && err.statusCode === 404) {
13299
+ got404 = true;
13300
+ break;
13301
+ }
13302
+ }
13303
+ }
13304
+ if (got404) {
13305
+ console.warn(
13306
+ "[okx-auth] Native linux-arm64 binary not yet on CDN. Falling back to linux-x64 binary \u2014 x86_64 emulation (binfmt_misc/Rosetta) required."
13307
+ );
13308
+ return LINUX_ARM64_FALLBACK_DIR;
13309
+ }
13310
+ return "linux-arm64";
13311
+ }
13312
+ async function resolveAuthPlatformDir(sources = CDN_SOURCES, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
13313
+ const native = getPlatformDir();
13314
+ if (!native) return null;
13315
+ return _resolveAuthPlatformFromNative(native, sources, timeoutMs);
13316
+ }
12769
13317
  function getAuthStatus(binaryPath, opts) {
12770
13318
  const resolvedPath = binaryPath ?? getAuthBinaryPath();
12771
13319
  const platformDir = getPlatformDir();
@@ -12779,7 +13327,7 @@ function getAuthStatus(binaryPath, opts) {
12779
13327
  return { binaryPath: resolvedPath, exists: true, platform: platformDir, fileSize: size, sha256 };
12780
13328
  }
12781
13329
  async function fetchAuthCdnChecksum(sources = CDN_SOURCES, timeoutMs = DOWNLOAD_TIMEOUT_MS) {
12782
- const platformDir = getPlatformDir();
13330
+ const platformDir = await resolveAuthPlatformDir(sources, timeoutMs);
12783
13331
  if (!platformDir) return null;
12784
13332
  const checksumPath = `${AUTH_CDN_PATH_PREFIX}/${platformDir}/checksum.json`;
12785
13333
  for (const { host, protocol } of sources) {
@@ -12848,12 +13396,51 @@ function installPreChecks2(destPath, sources) {
12848
13396
  function isLocalUpToDate2(localHash, checksum) {
12849
13397
  return localHash !== null && localHash.size === checksum.size && localHash.sha256 === checksum.sha256;
12850
13398
  }
13399
+ async function tryInstallFromOneSource(ctx) {
13400
+ const { host, protocol, platformDir, checksumPath, binaryPath, tmpPath, resolvedDest, localHash, onProgress } = ctx;
13401
+ try {
13402
+ const checksum = await fetchAndValidateChecksum2(
13403
+ host,
13404
+ protocol,
13405
+ checksumPath,
13406
+ platformDir,
13407
+ DOWNLOAD_TIMEOUT_MS,
13408
+ onProgress
13409
+ );
13410
+ if (isLocalUpToDate2(localHash, checksum)) {
13411
+ onProgress?.("Already up to date (checksum match)");
13412
+ return { kind: "done", result: { status: "up-to-date", source: host } };
13413
+ }
13414
+ await downloadAndVerify2(host, protocol, binaryPath, tmpPath, checksum, DOWNLOAD_TIMEOUT_MS, onProgress);
13415
+ atomicReplace2(tmpPath, resolvedDest);
13416
+ onProgress?.(`Downloaded and verified from ${host}`);
13417
+ return { kind: "done", result: { status: "installed", source: host } };
13418
+ } catch (err) {
13419
+ try {
13420
+ unlinkSync5(tmpPath);
13421
+ } catch {
13422
+ }
13423
+ const errObj = err instanceof Error ? err : new Error(String(err));
13424
+ onProgress?.(`${host} failed: ${errObj.message}`);
13425
+ return { kind: "next", err: errObj };
13426
+ }
13427
+ }
13428
+ function buildInstallFailureResult(errors) {
13429
+ const formatted = errors.map(({ host, err }) => `${host}: ${err.message}`).join("\n");
13430
+ const allHttpErrors = errors.length > 0 && errors.every(({ err }) => err instanceof HttpStatusError);
13431
+ const prefix = allHttpErrors ? "okx-auth binary not available on CDN for this platform" : "All CDN sources failed";
13432
+ return { status: "failed", error: `${prefix}:
13433
+ ${formatted}` };
13434
+ }
12851
13435
  async function installAuthBinary(destPath, sources = CDN_SOURCES, onProgress) {
12852
13436
  const earlyResult = installPreChecks2(destPath, sources);
12853
13437
  if (earlyResult) return earlyResult;
12854
- const platformDir = getPlatformDir();
13438
+ const platformDir = await resolveAuthPlatformDir(sources);
13439
+ if (!platformDir) {
13440
+ return { status: "failed", error: "Unsupported platform" };
13441
+ }
12855
13442
  const binaryName = getAuthBinaryName();
12856
- const resolvedDest = destPath ?? join11(homedir9(), ".okx", "bin", binaryName);
13443
+ const resolvedDest = destPath ?? join13(homedir9(), ".okx", "bin", binaryName);
12857
13444
  const tmpPath = resolvedDest + ".tmp";
12858
13445
  mkdirSync9(dirname7(resolvedDest), { recursive: true });
12859
13446
  const localHash = existsSync7(resolvedDest) ? hashFile(resolvedDest) : null;
@@ -12861,35 +13448,21 @@ async function installAuthBinary(destPath, sources = CDN_SOURCES, onProgress) {
12861
13448
  const binaryPath = `${AUTH_CDN_PATH_PREFIX}/${platformDir}/${binaryName}`;
12862
13449
  const errors = [];
12863
13450
  for (const { host, protocol } of sources) {
12864
- try {
12865
- const checksum = await fetchAndValidateChecksum2(
12866
- host,
12867
- protocol,
12868
- checksumPath,
12869
- platformDir,
12870
- DOWNLOAD_TIMEOUT_MS,
12871
- onProgress
12872
- );
12873
- if (isLocalUpToDate2(localHash, checksum)) {
12874
- onProgress?.("Already up to date (checksum match)");
12875
- return { status: "up-to-date", source: host };
12876
- }
12877
- await downloadAndVerify2(host, protocol, binaryPath, tmpPath, checksum, DOWNLOAD_TIMEOUT_MS, onProgress);
12878
- atomicReplace2(tmpPath, resolvedDest);
12879
- onProgress?.(`Downloaded and verified from ${host}`);
12880
- return { status: "installed", source: host };
12881
- } catch (err) {
12882
- try {
12883
- unlinkSync5(tmpPath);
12884
- } catch {
12885
- }
12886
- const msg = err instanceof Error ? err.message : String(err);
12887
- errors.push(`${host}: ${msg}`);
12888
- onProgress?.(`${host} failed: ${msg}`);
12889
- }
13451
+ const attempt = await tryInstallFromOneSource({
13452
+ host,
13453
+ protocol,
13454
+ platformDir,
13455
+ checksumPath,
13456
+ binaryPath,
13457
+ tmpPath,
13458
+ resolvedDest,
13459
+ localHash,
13460
+ onProgress
13461
+ });
13462
+ if (attempt.kind === "done") return attempt.result;
13463
+ errors.push({ host, err: attempt.err });
12890
13464
  }
12891
- return { status: "failed", error: `All CDN sources failed:
12892
- ${errors.join("\n")}` };
13465
+ return buildInstallFailureResult(errors);
12893
13466
  }
12894
13467
  function removeAuthBinary(binaryPath) {
12895
13468
  const resolvedPath = binaryPath ?? getAuthBinaryPath();
@@ -12904,12 +13477,12 @@ function removeAuthBinary(binaryPath) {
12904
13477
  throw new Error(`Failed to remove ${resolvedPath}: ${msg}`);
12905
13478
  }
12906
13479
  }
12907
- var CACHE_PATH = join12(homedir10(), ".okx", "auth-binary-check.json");
13480
+ var CACHE_PATH = join14(homedir10(), ".okx", "auth-binary-check.json");
12908
13481
  var CHECK_INTERVAL_MS2 = 2 * 60 * 60 * 1e3;
12909
13482
  function readCache3() {
12910
13483
  try {
12911
13484
  if (!existsSync8(CACHE_PATH)) return null;
12912
- const data = JSON.parse(readFileSync8(CACHE_PATH, "utf-8"));
13485
+ const data = JSON.parse(readFileSync10(CACHE_PATH, "utf-8"));
12913
13486
  if (typeof data.cdnSha256 !== "string" || typeof data.checkedAt !== "number") return null;
12914
13487
  return { cdnSha256: data.cdnSha256, checkedAt: data.checkedAt };
12915
13488
  } catch {
@@ -12918,7 +13491,7 @@ function readCache3() {
12918
13491
  }
12919
13492
  function writeCache3(cdnSha256) {
12920
13493
  try {
12921
- mkdirSync10(join12(homedir10(), ".okx"), { recursive: true });
13494
+ mkdirSync10(join14(homedir10(), ".okx"), { recursive: true });
12922
13495
  writeFileSync7(CACHE_PATH, JSON.stringify({ cdnSha256, checkedAt: Date.now() }, null, 2), "utf-8");
12923
13496
  } catch {
12924
13497
  }
@@ -13053,7 +13626,7 @@ function markFailedIfSCodeError(data) {
13053
13626
  // src/commands/auth.ts
13054
13627
  function runOkxAuth(args) {
13055
13628
  const binPath = getAuthBinaryPath();
13056
- return new Promise((resolve3, reject) => {
13629
+ return new Promise((resolve4, reject) => {
13057
13630
  const child = spawn3(binPath, args, {
13058
13631
  stdio: "inherit"
13059
13632
  });
@@ -13061,13 +13634,13 @@ function runOkxAuth(args) {
13061
13634
  reject(new Error(`Failed to spawn okx-auth: ${err.message}`));
13062
13635
  });
13063
13636
  child.on("close", (code) => {
13064
- resolve3(code ?? 1);
13637
+ resolve4(code ?? 1);
13065
13638
  });
13066
13639
  });
13067
13640
  }
13068
13641
  function runOkxAuthCapture(args) {
13069
13642
  const binPath = getAuthBinaryPath();
13070
- return new Promise((resolve3, reject) => {
13643
+ return new Promise((resolve4, reject) => {
13071
13644
  const child = spawn3(binPath, args, {
13072
13645
  stdio: ["inherit", "pipe", "inherit"]
13073
13646
  });
@@ -13077,7 +13650,7 @@ function runOkxAuthCapture(args) {
13077
13650
  reject(new Error(`Failed to spawn okx-auth: ${err.message}`));
13078
13651
  });
13079
13652
  child.on("close", (code) => {
13080
- resolve3({
13653
+ resolve4({
13081
13654
  code: code ?? 1,
13082
13655
  stdout: Buffer.concat(chunks).toString("utf-8")
13083
13656
  });
@@ -13282,14 +13855,14 @@ async function confirmRemoval(force, binaryPath) {
13282
13855
  return true;
13283
13856
  }
13284
13857
  function askConfirmation(prompt2) {
13285
- return new Promise((resolve3) => {
13858
+ return new Promise((resolve4) => {
13286
13859
  const rl = readline.createInterface({
13287
13860
  input: process.stdin,
13288
13861
  output: process.stdout
13289
13862
  });
13290
13863
  rl.question(prompt2, (answer) => {
13291
13864
  rl.close();
13292
- resolve3(answer.trim().toLowerCase() === "y");
13865
+ resolve4(answer.trim().toLowerCase() === "y");
13293
13866
  });
13294
13867
  });
13295
13868
  }
@@ -13346,26 +13919,26 @@ var Report = class {
13346
13919
  this.lines.push({ key, value });
13347
13920
  }
13348
13921
  print() {
13349
- const sep2 = "\u2500".repeat(52);
13922
+ const sep3 = "\u2500".repeat(52);
13350
13923
  outputLine("");
13351
- outputLine(` \u2500\u2500 Diagnostic Report (copy & share) ${sep2.slice(35)}`);
13924
+ outputLine(` \u2500\u2500 Diagnostic Report (copy & share) ${sep3.slice(35)}`);
13352
13925
  for (const { key, value } of this.lines) {
13353
13926
  outputLine(` ${key.padEnd(14)} ${value}`);
13354
13927
  }
13355
- outputLine(` ${sep2}`);
13928
+ outputLine(` ${sep3}`);
13356
13929
  outputLine("");
13357
13930
  }
13358
13931
  /** Write report to a file path, returns true on success. */
13359
13932
  writeToFile(filePath) {
13360
13933
  try {
13361
- const sep2 = "\u2500".repeat(52);
13934
+ const sep3 = "\u2500".repeat(52);
13362
13935
  const lines = [
13363
- `\u2500\u2500 Diagnostic Report (copy & share) ${sep2.slice(35)}`
13936
+ `\u2500\u2500 Diagnostic Report (copy & share) ${sep3.slice(35)}`
13364
13937
  ];
13365
13938
  for (const { key, value } of this.lines) {
13366
13939
  lines.push(`${key.padEnd(14)} ${value}`);
13367
13940
  }
13368
- lines.push(sep2, "");
13941
+ lines.push(sep3, "");
13369
13942
  fs4.writeFileSync(filePath, lines.join("\n"), "utf8");
13370
13943
  return true;
13371
13944
  } catch (_e) {
@@ -13741,13 +14314,13 @@ async function checkStdioHandshake(entryPath, report) {
13741
14314
  clientInfo: { name: "okx-diagnose", version: "1.0" }
13742
14315
  }
13743
14316
  });
13744
- return new Promise((resolve3) => {
14317
+ return new Promise((resolve4) => {
13745
14318
  let settled = false;
13746
14319
  const settle = (passed) => {
13747
14320
  if (settled) return;
13748
14321
  settled = true;
13749
14322
  clearTimeout(timer);
13750
- resolve3(passed);
14323
+ resolve4(passed);
13751
14324
  };
13752
14325
  const child = spawn4(process.execPath, [entryPath], {
13753
14326
  stdio: ["pipe", "pipe", "pipe"],
@@ -13868,7 +14441,7 @@ async function cmdDiagnoseMcp(options = {}) {
13868
14441
 
13869
14442
  // src/commands/diagnose.ts
13870
14443
  var CLI_VERSION = readCliVersion();
13871
- var GIT_HASH = true ? "c34b282c" : "dev";
14444
+ var GIT_HASH = true ? "e5abc21f" : "dev";
13872
14445
  function maskKey2(key) {
13873
14446
  if (!key) return "(not set)";
13874
14447
  if (key.length <= 8) return "****";
@@ -13885,7 +14458,7 @@ async function checkDns(hostname) {
13885
14458
  }
13886
14459
  async function checkSocket(createFn, successEvent, timeoutMs) {
13887
14460
  const t0 = Date.now();
13888
- return new Promise((resolve3) => {
14461
+ return new Promise((resolve4) => {
13889
14462
  const socket = createFn();
13890
14463
  const cleanup = () => {
13891
14464
  socket.removeAllListeners();
@@ -13893,15 +14466,15 @@ async function checkSocket(createFn, successEvent, timeoutMs) {
13893
14466
  };
13894
14467
  socket.once(successEvent, () => {
13895
14468
  cleanup();
13896
- resolve3({ ok: true, ms: Date.now() - t0 });
14469
+ resolve4({ ok: true, ms: Date.now() - t0 });
13897
14470
  });
13898
14471
  socket.once("timeout", () => {
13899
14472
  cleanup();
13900
- resolve3({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
14473
+ resolve4({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
13901
14474
  });
13902
14475
  socket.once("error", (err) => {
13903
14476
  cleanup();
13904
- resolve3({ ok: false, ms: Date.now() - t0, error: err.message });
14477
+ resolve4({ ok: false, ms: Date.now() - t0, error: err.message });
13905
14478
  });
13906
14479
  });
13907
14480
  }
@@ -14229,23 +14802,23 @@ function suggestSubcommand(action, knownActions, knownPaths = []) {
14229
14802
 
14230
14803
  // src/commands/upgrade.ts
14231
14804
  import { spawnSync as spawnSync2 } from "child_process";
14232
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync11 } from "fs";
14233
- import { dirname as dirname8, join as join13 } from "path";
14805
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync11 } from "fs";
14806
+ import { dirname as dirname8, join as join15 } from "path";
14234
14807
  import { homedir as homedir11 } from "os";
14235
14808
  var PACKAGES = ["@okx_ai/okx-trade-mcp", "@okx_ai/okx-trade-cli"];
14236
- var CACHE_FILE2 = join13(homedir11(), ".okx", "last_check");
14809
+ var CACHE_FILE2 = join15(homedir11(), ".okx", "last_check");
14237
14810
  var THROTTLE_MS = 12 * 60 * 60 * 1e3;
14238
- var NPM_BIN = join13(dirname8(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
14811
+ var NPM_BIN = join15(dirname8(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
14239
14812
  function readLastCheck() {
14240
14813
  try {
14241
- return parseInt(readFileSync9(CACHE_FILE2, "utf-8").trim(), 10) || 0;
14814
+ return parseInt(readFileSync11(CACHE_FILE2, "utf-8").trim(), 10) || 0;
14242
14815
  } catch {
14243
14816
  return 0;
14244
14817
  }
14245
14818
  }
14246
14819
  function writeLastCheck() {
14247
14820
  try {
14248
- mkdirSync11(join13(homedir11(), ".okx"), { recursive: true });
14821
+ mkdirSync11(join15(homedir11(), ".okx"), { recursive: true });
14249
14822
  writeFileSync8(CACHE_FILE2, String(Math.floor(Date.now() / 1e3)), "utf-8");
14250
14823
  } catch {
14251
14824
  }
@@ -14932,6 +15505,11 @@ var CLI_REGISTRY = {
14932
15505
  usage: "okx earn savings rate-history [--ccy <ccy>] [--limit <n>]",
14933
15506
  description: "Query Simple Earn lending rates and fixed-term offers (requires auth)"
14934
15507
  },
15508
+ "fixed-products": {
15509
+ toolName: "earn_get_fixed_earn_products",
15510
+ usage: "okx earn savings fixed-products [--ccy <ccy>]",
15511
+ description: "List available fixed-term earn products with APR, term, and remaining quota"
15512
+ },
14935
15513
  "fixed-orders": {
14936
15514
  toolName: "earn_get_fixed_order_list",
14937
15515
  usage: "okx earn savings fixed-orders [--ccy <ccy>] [--state <pending|earning|expired|settled|cancelled>]",
@@ -14944,7 +15522,7 @@ var CLI_REGISTRY = {
14944
15522
  },
14945
15523
  "fixed-redeem": {
14946
15524
  toolName: "earn_fixed_redeem",
14947
- usage: "okx earn savings fixed-redeem <reqId>",
15525
+ usage: "okx earn savings fixed-redeem --reqId <reqId>",
14948
15526
  description: "Redeem a fixed-term earn order (full amount)"
14949
15527
  }
14950
15528
  }
@@ -18247,7 +18825,7 @@ Config saved to ${p}
18247
18825
  }
18248
18826
  };
18249
18827
  function prompt(rl, question) {
18250
- return new Promise((resolve3) => rl.question(question, resolve3));
18828
+ return new Promise((resolve4) => rl.question(question, resolve4));
18251
18829
  }
18252
18830
  function cmdConfigShow(json) {
18253
18831
  const config = readFullConfig();
@@ -18659,13 +19237,24 @@ async function cmdEarnLendingRateHistory(run, opts) {
18659
19237
  printTable(fixedOffers.map((r) => ({
18660
19238
  ccy: r["ccy"],
18661
19239
  term: r["term"],
18662
- rate: r["rate"],
19240
+ apr: r["apr"],
18663
19241
  minLend: r["minLend"],
18664
19242
  remainingQuota: r["lendQuota"],
18665
19243
  soldOut: r["soldOut"] ? "Yes" : "No"
18666
19244
  })));
18667
19245
  }
18668
19246
  }
19247
+ async function cmdEarnFixedProducts(run, opts) {
19248
+ const data = extractData(await run("earn_get_fixed_earn_products", { ccy: opts.ccy }));
19249
+ printDataList(data, opts.json, "No fixed earn products available", (r) => ({
19250
+ ccy: r["ccy"],
19251
+ term: r["term"],
19252
+ apr: r["apr"],
19253
+ minLend: r["minLend"],
19254
+ remainingQuota: r["lendQuota"],
19255
+ soldOut: r["soldOut"] ? "Yes" : "No"
19256
+ }));
19257
+ }
18669
19258
  function extractFixedOffers(result) {
18670
19259
  if (result && typeof result === "object") {
18671
19260
  const offers = result["fixedOffers"];
@@ -19659,18 +20248,21 @@ async function cmdDcdQuoteAndBuy(run, opts) {
19659
20248
 
19660
20249
  // src/commands/skill.ts
19661
20250
  import { tmpdir, homedir as homedir13 } from "os";
19662
- import { join as join15, dirname as dirname9 } from "path";
20251
+ import { join as join17, dirname as dirname9 } from "path";
19663
20252
  import { mkdirSync as mkdirSync12, rmSync, existsSync as existsSync10, copyFileSync as copyFileSync2 } from "fs";
19664
20253
  import { execFileSync as execFileSync2 } from "child_process";
19665
20254
  import { randomUUID as randomUUID2 } from "crypto";
19666
20255
  function resolveNpx() {
19667
- const sibling = join15(dirname9(process.execPath), "npx");
20256
+ const sibling = join17(dirname9(process.execPath), "npx");
19668
20257
  if (existsSync10(sibling)) return sibling;
19669
20258
  return "npx";
19670
20259
  }
19671
20260
  function npxEnv() {
19672
20261
  return { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" };
19673
20262
  }
20263
+ function getSkillContentDir(name) {
20264
+ return join17(homedir13(), ".agents", "skills", name);
20265
+ }
19674
20266
  var THIRD_PARTY_INSTALL_NOTICE = "Note: This skill was created by a third-party developer, not by OKX. Review SKILL.md before use.";
19675
20267
  async function cmdSkillSearch(run, opts) {
19676
20268
  const args = {};
@@ -19720,16 +20312,56 @@ async function cmdSkillCategories(run, json) {
19720
20312
  }
19721
20313
  outputLine("");
19722
20314
  }
19723
- async function cmdSkillAdd(name, config, json, exec = execFileSync2) {
19724
- const tmpBase = join15(tmpdir(), `okx-skill-${randomUUID2()}`);
20315
+ async function wrapVerify(fn) {
20316
+ try {
20317
+ return await fn();
20318
+ } catch (e) {
20319
+ if (e instanceof ConfigError) {
20320
+ throw new Error(
20321
+ `Signature verification requires authentication \u2014 run \`okx auth login\` first, or use --force to bypass.`
20322
+ );
20323
+ }
20324
+ throw e;
20325
+ }
20326
+ }
20327
+ async function cmdSkillAdd(name, config, json, force = false, exec = execFileSync2, _deps) {
20328
+ const _download = _deps?.download ?? downloadSkillZip;
20329
+ const _extract = _deps?.extract ?? extractSkillZip;
20330
+ const tmpBase = join17(tmpdir(), `okx-skill-${randomUUID2()}`);
19725
20331
  mkdirSync12(tmpBase, { recursive: true });
19726
20332
  try {
19727
20333
  outputLine(`Downloading ${name}...`);
19728
20334
  const client = new OkxRestClient(config);
19729
- const zipPath = await downloadSkillZip(client, name, tmpBase);
19730
- const contentDir = await extractSkillZip(zipPath, join15(tmpBase, "content"));
20335
+ const zipPath = await _download(client, name, tmpBase);
20336
+ const contentDir = await _extract(zipPath, join17(tmpBase, "content"));
19731
20337
  const meta = readMetaJson(contentDir);
19732
20338
  validateSkillMdExists(contentDir);
20339
+ outputLine("Verifying signature...");
20340
+ const verifyResult = await wrapVerify(
20341
+ () => verifySkillSignature(contentDir, meta.signing, {
20342
+ fetchPublicKey: (keyId) => getPublicKey(client, keyId),
20343
+ serverSideVerify: (sName, version, files) => serverSideVerify(client, sName, version, files),
20344
+ skillName: meta.name,
20345
+ skillVersion: meta.version
20346
+ })
20347
+ );
20348
+ if (verifyResult.status === "failed") {
20349
+ if (!force) {
20350
+ throw new Error(`Signature verification failed: ${verifyResult.error ?? "unknown error"}. Use --force to install anyway.`);
20351
+ }
20352
+ process.stderr.write(`WARNING: Signature verification failed \u2014 ${verifyResult.error ?? "unknown"}. Installing anyway (--force).
20353
+ `);
20354
+ } else if (verifyResult.status === "verified_by_server") {
20355
+ if (!json) {
20356
+ outputLine(` Verified by server (v${verifyResult.serverVersion ?? "?"})`);
20357
+ if (verifyResult.error) outputLine(` Note: ${verifyResult.error}`);
20358
+ }
20359
+ } else if (!json) {
20360
+ outputLine(` Signature verified (key: ${verifyResult.publicKeyId}, files: ${verifyResult.filesChecked})`);
20361
+ if (verifyResult.extraFiles?.length) {
20362
+ outputLine(` Note: ${verifyResult.extraFiles.length} extra unsigned file(s) present`);
20363
+ }
20364
+ }
19733
20365
  outputLine("Installing to detected agents...");
19734
20366
  try {
19735
20367
  exec(resolveNpx(), ["skills", "add", contentDir, "-y", "-g"], {
@@ -19738,7 +20370,7 @@ async function cmdSkillAdd(name, config, json, exec = execFileSync2) {
19738
20370
  env: npxEnv()
19739
20371
  });
19740
20372
  } catch (e) {
19741
- const savedZip = join15(process.cwd(), `${name}.zip`);
20373
+ const savedZip = join17(process.cwd(), `${name}.zip`);
19742
20374
  try {
19743
20375
  copyFileSync2(zipPath, savedZip);
19744
20376
  } catch {
@@ -19747,7 +20379,8 @@ async function cmdSkillAdd(name, config, json, exec = execFileSync2) {
19747
20379
  errorLine(`You can manually install from: ${savedZip}`);
19748
20380
  throw e;
19749
20381
  }
19750
- upsertSkillRecord(meta);
20382
+ const registryStatus = verifyResult.status === "failed" && force ? "bypassed" : verifyResult.status;
20383
+ upsertSkillRecord(meta, void 0, registryStatus);
19751
20384
  printSkillInstallResult(meta, json);
19752
20385
  } finally {
19753
20386
  rmSync(tmpBase, { recursive: true, force: true });
@@ -19778,7 +20411,7 @@ function cmdSkillRemove(name, json, exec = execFileSync2) {
19778
20411
  env: npxEnv()
19779
20412
  });
19780
20413
  } catch {
19781
- const agentsPath = join15(homedir13(), ".agents", "skills", name);
20414
+ const agentsPath = getSkillContentDir(name);
19782
20415
  try {
19783
20416
  rmSync(agentsPath, { recursive: true, force: true });
19784
20417
  } catch {
@@ -19842,6 +20475,59 @@ function cmdSkillList(json) {
19842
20475
  outputLine("");
19843
20476
  outputLine(`${skills.length} skills installed.`);
19844
20477
  }
20478
+ async function cmdSkillVerify(name, config, json) {
20479
+ const record = getSkillRecord(name);
20480
+ if (!record) {
20481
+ errorLine(`Skill "${name}" is not installed.`);
20482
+ process.exitCode = 1;
20483
+ return;
20484
+ }
20485
+ const contentDir = getSkillContentDir(name);
20486
+ if (!existsSync10(contentDir)) {
20487
+ errorLine(`Skill content directory not found: ${contentDir}`);
20488
+ errorLine(`Try reinstalling with: okx skill add ${name}`);
20489
+ process.exitCode = 1;
20490
+ return;
20491
+ }
20492
+ const meta = tryReadMetaJson(contentDir);
20493
+ const client = new OkxRestClient(config);
20494
+ let result;
20495
+ try {
20496
+ result = await wrapVerify(
20497
+ () => verifySkillSignature(contentDir, meta?.signing, {
20498
+ fetchPublicKey: (keyId) => getPublicKey(client, keyId),
20499
+ serverSideVerify: (sName, version, files) => serverSideVerify(client, sName, version, files),
20500
+ skillName: name,
20501
+ skillVersion: meta?.version
20502
+ })
20503
+ );
20504
+ } catch (e) {
20505
+ errorLine(e instanceof Error ? e.message : String(e));
20506
+ process.exitCode = 1;
20507
+ return;
20508
+ }
20509
+ if (meta) {
20510
+ upsertSkillRecord(meta, void 0, result.status);
20511
+ }
20512
+ if (result.status === "failed") {
20513
+ process.exitCode = 1;
20514
+ }
20515
+ if (json) {
20516
+ outputLine(JSON.stringify(result, null, 2));
20517
+ return;
20518
+ }
20519
+ if (result.status === "verified") {
20520
+ outputLine(`\u2713 ${name}: signature verified (key: ${result.publicKeyId}, files: ${result.filesChecked})`);
20521
+ if (result.extraFiles?.length) {
20522
+ outputLine(` Note: ${result.extraFiles.length} extra unsigned file(s) present`);
20523
+ }
20524
+ } else if (result.status === "verified_by_server") {
20525
+ outputLine(`\u2713 ${name}: verified by server (v${result.serverVersion ?? "?"})`);
20526
+ if (result.error) outputLine(` Note: ${result.error}`);
20527
+ } else if (result.status === "failed") {
20528
+ errorLine(`\u2717 ${name}: verification failed \u2014 ${result.error ?? "unknown"}`);
20529
+ }
20530
+ }
19845
20531
  function printSkillInstallResult(meta, json) {
19846
20532
  if (json) {
19847
20533
  outputLine(JSON.stringify({ name: meta.name, version: meta.version, status: "installed" }, null, 2));
@@ -19992,14 +20678,14 @@ function formatBytes2(bytes) {
19992
20678
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
19993
20679
  }
19994
20680
  function askConfirmation2(prompt2) {
19995
- return new Promise((resolve3) => {
20681
+ return new Promise((resolve4) => {
19996
20682
  const rl = readline2.createInterface({
19997
20683
  input: process.stdin,
19998
20684
  output: process.stdout
19999
20685
  });
20000
20686
  rl.question(prompt2, (answer) => {
20001
20687
  rl.close();
20002
- resolve3(answer.trim().toLowerCase() === "y");
20688
+ resolve4(answer.trim().toLowerCase() === "y");
20003
20689
  });
20004
20690
  });
20005
20691
  }
@@ -20462,7 +21148,7 @@ async function cmdEventCancel(run, opts) {
20462
21148
  // src/index.ts
20463
21149
  var _require3 = createRequire3(import.meta.url);
20464
21150
  var CLI_VERSION2 = _require3("../package.json").version;
20465
- var GIT_HASH2 = true ? "c34b282c" : "dev";
21151
+ var GIT_HASH2 = true ? "e5abc21f" : "dev";
20466
21152
  function handlePilotCommand(action, json, force, binaryPath) {
20467
21153
  if (action === "status") return cmdPilotStatus(json, binaryPath);
20468
21154
  if (action === "install") return cmdPilotInstall(json, binaryPath);
@@ -21656,6 +22342,7 @@ function handleEarnSavingsCommand(run, action, rest, v, json) {
21656
22342
  if (action === "set-rate") return cmdEarnSetLendingRate(run, { ccy: v.ccy, rate: v.rate, json });
21657
22343
  if (action === "lending-history") return cmdEarnLendingHistory(run, { ccy: v.ccy, limit, json });
21658
22344
  if (action === "rate-history") return cmdEarnLendingRateHistory(run, { ccy: v.ccy, limit, json });
22345
+ if (action === "fixed-products") return cmdEarnFixedProducts(run, { ccy: v.ccy, json });
21659
22346
  if (action === "fixed-orders") return cmdEarnFixedOrderList(run, { ccy: v.ccy, state: v.state, json });
21660
22347
  if (action === "fixed-purchase") return cmdEarnFixedPurchase(run, { ccy: v.ccy, amt: v.amt, term: v.term, confirm: v.confirm ?? false, json });
21661
22348
  if (action === "fixed-redeem") return cmdEarnFixedRedeem(run, { reqId: v.reqId, json });
@@ -21773,9 +22460,13 @@ function requireSkillName(rest, usage) {
21773
22460
  }
21774
22461
  return name;
21775
22462
  }
21776
- function handleSkillAdd(rest, config, json) {
22463
+ function handleSkillAdd(rest, v, config, json) {
21777
22464
  const n = requireSkillName(rest, "Usage: okx skill add <name>");
21778
- if (n) return cmdSkillAdd(n, config, json);
22465
+ if (n) return cmdSkillAdd(n, config, json, v.force ?? false);
22466
+ }
22467
+ function handleSkillVerify(rest, config, json) {
22468
+ const n = requireSkillName(rest, "Usage: okx skill verify <name>");
22469
+ if (n) return cmdSkillVerify(n, config, json);
21779
22470
  }
21780
22471
  function handleSkillDownload(rest, v, config, json) {
21781
22472
  const n = requireSkillName(rest, "Usage: okx skill download <name> [--dir <path>] [--format zip|skill]");
@@ -21794,12 +22485,13 @@ function handleSkillCommand(run, action, rest, v, json, config) {
21794
22485
  if (action === "search") return cmdSkillSearch(run, { keyword: rest[0] ?? v.keyword, categories: v.categories, page: v.page, limit: v.limit, json });
21795
22486
  if (action === "categories") return cmdSkillCategories(run, json);
21796
22487
  if (action === "list") return cmdSkillList(json);
21797
- if (action === "add") return handleSkillAdd(rest, config, json);
22488
+ if (action === "add") return handleSkillAdd(rest, v, config, json);
21798
22489
  if (action === "download") return handleSkillDownload(rest, v, config, json);
21799
22490
  if (action === "remove") return handleSkillRemove(rest, json);
21800
22491
  if (action === "check") return handleSkillCheck(run, rest, json);
22492
+ if (action === "verify") return handleSkillVerify(rest, config, json);
21801
22493
  errorLine(`Unknown skill command: ${action}`);
21802
- errorLine("Valid: search, categories, add, download, remove, check, list");
22494
+ errorLine("Valid: search, categories, add, download, remove, check, list, verify");
21803
22495
  process.exitCode = 1;
21804
22496
  }
21805
22497
  function handleEventCommand(run, action, rest, v, json) {