@okx_ai/okx-trade-cli 1.2.8-beta.4 → 1.2.8-beta.6

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
@@ -9,9 +9,20 @@ import { createHmac } from "crypto";
9
9
  import fs from "fs";
10
10
  import path from "path";
11
11
  import os from "os";
12
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
13
- import { join, dirname } from "path";
12
+ import { writeFileSync, renameSync, unlinkSync, mkdirSync } from "fs";
13
+ import { join, resolve, basename, sep } from "path";
14
+ import { randomUUID } from "crypto";
15
+ import yauzl from "yauzl";
16
+ import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
17
+ import { resolve as resolve2, dirname } from "path";
18
+ import { readFileSync, existsSync } from "fs";
19
+ import { join as join2 } from "path";
20
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
21
+ import { join as join3, dirname as dirname2 } from "path";
14
22
  import { homedir } from "os";
23
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3 } from "fs";
24
+ import { join as join4, dirname as dirname3 } from "path";
25
+ import { homedir as homedir2 } from "os";
15
26
 
16
27
  // ../../node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
17
28
  function getLineColFromPtr(string, ptr) {
@@ -88,7 +99,7 @@ function skipVoid(str, ptr, banNewLines, banComments) {
88
99
  ptr++;
89
100
  return banComments || c !== "#" ? ptr : skipVoid(str, skipComment(str, ptr), banNewLines);
90
101
  }
91
- function skipUntil(str, ptr, sep, end, banNewLines = false) {
102
+ function skipUntil(str, ptr, sep2, end, banNewLines = false) {
92
103
  if (!end) {
93
104
  ptr = indexOfNewline(str, ptr);
94
105
  return ptr < 0 ? str.length : ptr;
@@ -97,7 +108,7 @@ function skipUntil(str, ptr, sep, end, banNewLines = false) {
97
108
  let c = str[i];
98
109
  if (c === "#") {
99
110
  i = indexOfNewline(str, i);
100
- } else if (c === sep) {
111
+ } else if (c === sep2) {
101
112
  return i + 1;
102
113
  } else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) {
103
114
  return i;
@@ -840,9 +851,9 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
840
851
  }
841
852
 
842
853
  // ../core/dist/index.js
843
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
844
- import { join as join2 } from "path";
845
- import { homedir as homedir2 } from "os";
854
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
855
+ import { join as join5 } from "path";
856
+ import { homedir as homedir3 } from "os";
846
857
  import * as fs3 from "fs";
847
858
  import * as path3 from "path";
848
859
  import * as os3 from "os";
@@ -927,8 +938,8 @@ function toToolErrorPayload(error, fallbackEndpoint) {
927
938
  };
928
939
  }
929
940
  function sleep(ms) {
930
- return new Promise((resolve) => {
931
- setTimeout(resolve, ms);
941
+ return new Promise((resolve3) => {
942
+ setTimeout(resolve3, ms);
932
943
  });
933
944
  }
934
945
  var RateLimiter = class {
@@ -1061,7 +1072,7 @@ function vlog(message) {
1061
1072
  process.stderr.write(`[verbose] ${message}
1062
1073
  `);
1063
1074
  }
1064
- var OkxRestClient = class {
1075
+ var OkxRestClient = class _OkxRestClient {
1065
1076
  config;
1066
1077
  rateLimiter;
1067
1078
  dispatcher;
@@ -1214,6 +1225,88 @@ var OkxRestClient = class {
1214
1225
  raw: parsed
1215
1226
  };
1216
1227
  }
1228
+ // ---------------------------------------------------------------------------
1229
+ // Binary (non-JSON) download — reuses auth, proxy, rate-limit, verbose
1230
+ // ---------------------------------------------------------------------------
1231
+ static DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
1232
+ // 50 MB
1233
+ /**
1234
+ * Try to parse a text body as OKX JSON error and throw the appropriate error.
1235
+ * Re-throws OkxApiError / AuthenticationError / RateLimitError if matched.
1236
+ * Returns the parsed message string (or fallback) if no specific error was thrown.
1237
+ */
1238
+ tryThrowJsonError(text, path42, traceId) {
1239
+ try {
1240
+ const parsed = JSON.parse(text);
1241
+ if (parsed.code && parsed.code !== "0") {
1242
+ this.throwOkxError(parsed.code, parsed.msg, { method: "POST", path: path42, auth: "private" }, traceId);
1243
+ }
1244
+ return parsed.msg ?? "";
1245
+ } catch (e) {
1246
+ if (e instanceof OkxApiError || e instanceof AuthenticationError || e instanceof RateLimitError) throw e;
1247
+ return "";
1248
+ }
1249
+ }
1250
+ /**
1251
+ * Send a signed POST request and return the raw binary response.
1252
+ * Inherits all client capabilities: auth, proxy, rate-limit, verbose, user-agent.
1253
+ * Security: validates Content-Type and enforces maxBytes limit.
1254
+ */
1255
+ async privatePostBinary(path42, body, opts) {
1256
+ const maxBytes = opts?.maxBytes ?? _OkxRestClient.DEFAULT_MAX_BYTES;
1257
+ const expectedCT = opts?.expectedContentType ?? "application/octet-stream";
1258
+ const bodyJson = body ? JSON.stringify(body) : "";
1259
+ const endpoint = `POST ${path42}`;
1260
+ this.logRequest("POST", `${this.config.baseUrl}${path42}`, "private");
1261
+ const reqConfig = { method: "POST", path: path42, auth: "private" };
1262
+ const headers = this.buildHeaders(reqConfig, path42, bodyJson, getNow());
1263
+ const t0 = Date.now();
1264
+ const response = await this.fetchBinary(path42, endpoint, headers, bodyJson, t0);
1265
+ const elapsed = Date.now() - t0;
1266
+ const traceId = extractTraceId(response.headers);
1267
+ if (!response.ok) {
1268
+ const text = await response.text();
1269
+ this.logResponse(response.status, text.length, elapsed, traceId, String(response.status));
1270
+ const msg = this.tryThrowJsonError(text, path42, traceId) || `HTTP ${response.status}`;
1271
+ throw new OkxApiError(msg, { code: String(response.status), endpoint, traceId });
1272
+ }
1273
+ const ct = response.headers.get("content-type") ?? "";
1274
+ if (!ct.includes(expectedCT)) {
1275
+ const text = await response.text();
1276
+ this.logResponse(response.status, text.length, elapsed, traceId, "unexpected-ct");
1277
+ this.tryThrowJsonError(text, path42, traceId);
1278
+ throw new OkxApiError(`Expected binary response (${expectedCT}) but got: ${ct}`, { code: "UNEXPECTED_CONTENT_TYPE", endpoint, traceId });
1279
+ }
1280
+ const buffer = Buffer.from(await response.arrayBuffer());
1281
+ if (buffer.length > maxBytes) {
1282
+ throw new OkxApiError(`Response size ${buffer.length} bytes exceeds limit of ${maxBytes} bytes.`, { code: "RESPONSE_TOO_LARGE", endpoint, traceId });
1283
+ }
1284
+ if (this.config.verbose) {
1285
+ vlog(`\u2190 ${response.status} | binary ${buffer.length}B | ${elapsed}ms | trace=${traceId ?? "-"}`);
1286
+ }
1287
+ return { endpoint, requestTime: (/* @__PURE__ */ new Date()).toISOString(), data: buffer, contentType: ct, contentLength: buffer.length, traceId };
1288
+ }
1289
+ /** Execute fetch for binary endpoint, wrapping network errors. */
1290
+ async fetchBinary(path42, endpoint, headers, bodyJson, t0) {
1291
+ try {
1292
+ const fetchOptions = {
1293
+ method: "POST",
1294
+ headers,
1295
+ body: bodyJson || void 0,
1296
+ signal: AbortSignal.timeout(this.config.timeoutMs)
1297
+ };
1298
+ if (this.dispatcher) fetchOptions.dispatcher = this.dispatcher;
1299
+ return await fetch(`${this.config.baseUrl}${path42}`, fetchOptions);
1300
+ } catch (error) {
1301
+ if (this.config.verbose) {
1302
+ vlog(`\u2717 NetworkError after ${Date.now() - t0}ms: ${error instanceof Error ? error.message : String(error)}`);
1303
+ }
1304
+ throw new NetworkError(`Failed to call OKX endpoint ${endpoint}.`, endpoint, error);
1305
+ }
1306
+ }
1307
+ // ---------------------------------------------------------------------------
1308
+ // Header building
1309
+ // ---------------------------------------------------------------------------
1217
1310
  buildHeaders(reqConfig, requestPath, bodyJson, timestamp) {
1218
1311
  const headers = new Headers({
1219
1312
  "Content-Type": "application/json",
@@ -1235,6 +1328,9 @@ var OkxRestClient = class {
1235
1328
  }
1236
1329
  return headers;
1237
1330
  }
1331
+ // ---------------------------------------------------------------------------
1332
+ // JSON request
1333
+ // ---------------------------------------------------------------------------
1238
1334
  async request(reqConfig) {
1239
1335
  const queryString = buildQueryString(reqConfig.query);
1240
1336
  const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
@@ -1546,9 +1642,10 @@ var MODULES = [
1546
1642
  "account",
1547
1643
  "news",
1548
1644
  ...EARN_SUB_MODULE_IDS,
1549
- ...BOT_SUB_MODULE_IDS
1645
+ ...BOT_SUB_MODULE_IDS,
1646
+ "skills"
1550
1647
  ];
1551
- var DEFAULT_MODULES = ["spot", "swap", "option", "account", ...BOT_DEFAULT_SUB_MODULES];
1648
+ var DEFAULT_MODULES = ["spot", "swap", "option", "account", ...BOT_DEFAULT_SUB_MODULES, "skills"];
1552
1649
  function registerAccountTools() {
1553
1650
  return [
1554
1651
  {
@@ -1889,7 +1986,7 @@ function registerAccountTools() {
1889
1986
  {
1890
1987
  name: "account_get_config",
1891
1988
  module: "account",
1892
- description: "Get account configuration: position mode (net vs hedge), account level, auto-loan settings, etc.",
1989
+ 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.",
1893
1990
  isWrite: false,
1894
1991
  inputSchema: {
1895
1992
  type: "object",
@@ -2879,6 +2976,299 @@ function registerAuditTools() {
2879
2976
  }
2880
2977
  ];
2881
2978
  }
2979
+ function safeWriteFile(targetDir, fileName, data) {
2980
+ const safeName = basename(fileName);
2981
+ if (!safeName || safeName === "." || safeName === "..") {
2982
+ throw new Error(`Invalid file name: "${fileName}"`);
2983
+ }
2984
+ const resolvedDir = resolve(targetDir);
2985
+ const filePath = join(resolvedDir, safeName);
2986
+ const resolvedPath = resolve(filePath);
2987
+ if (!resolvedPath.startsWith(resolvedDir + sep)) {
2988
+ throw new Error(`Path traversal detected: "${fileName}" resolves outside target directory`);
2989
+ }
2990
+ mkdirSync(resolvedDir, { recursive: true });
2991
+ const tmpPath = `${resolvedPath}.${randomUUID()}.tmp`;
2992
+ try {
2993
+ writeFileSync(tmpPath, data);
2994
+ renameSync(tmpPath, resolvedPath);
2995
+ } catch (err) {
2996
+ try {
2997
+ unlinkSync(tmpPath);
2998
+ } catch {
2999
+ }
3000
+ throw err;
3001
+ }
3002
+ return resolvedPath;
3003
+ }
3004
+ function validateZipEntryPath(targetDir, entryName) {
3005
+ const resolvedDir = resolve(targetDir);
3006
+ const resolvedEntry = resolve(resolvedDir, entryName);
3007
+ if (!resolvedEntry.startsWith(resolvedDir + sep) && resolvedEntry !== resolvedDir) {
3008
+ throw new Error(`Zip path traversal detected: "${entryName}" resolves outside extraction directory`);
3009
+ }
3010
+ return resolvedEntry;
3011
+ }
3012
+ var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
3013
+ async function downloadSkillZip(client, name, targetDir) {
3014
+ const result = await client.privatePostBinary(
3015
+ "/api/v5/skill/download",
3016
+ { name },
3017
+ { maxBytes: MAX_DOWNLOAD_BYTES }
3018
+ );
3019
+ const fileName = `${name}.zip`;
3020
+ const filePath = safeWriteFile(targetDir, fileName, result.data);
3021
+ return filePath;
3022
+ }
3023
+ var DEFAULT_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
3024
+ var DEFAULT_MAX_FILES = 1e3;
3025
+ var DEFAULT_MAX_COMPRESSION_RATIO = 100;
3026
+ function validateEntry(entry, targetDir, state, limits) {
3027
+ state.fileCount++;
3028
+ if (state.fileCount > limits.maxFiles) {
3029
+ throw new Error(
3030
+ `Zip contains more than ${limits.maxFiles} entries, exceeding limit of ${limits.maxFiles}. Possible zip bomb.`
3031
+ );
3032
+ }
3033
+ const resolvedPath = validateZipEntryPath(targetDir, entry.fileName);
3034
+ const externalAttrs = entry.externalFileAttributes;
3035
+ if (externalAttrs) {
3036
+ const unixMode = externalAttrs >>> 16 & 65535;
3037
+ if ((unixMode & 40960) === 40960) {
3038
+ throw new Error(`Zip entry "${entry.fileName}" is a symlink. Symlinks are not allowed for security.`);
3039
+ }
3040
+ }
3041
+ if (entry.compressedSize > 0) {
3042
+ const ratio = entry.uncompressedSize / entry.compressedSize;
3043
+ if (ratio > limits.maxCompressionRatio) {
3044
+ throw new Error(
3045
+ `Zip entry "${entry.fileName}" has compression ratio ${ratio.toFixed(1)}, exceeding limit of ${limits.maxCompressionRatio}. Possible zip bomb.`
3046
+ );
3047
+ }
3048
+ }
3049
+ state.totalBytes += entry.uncompressedSize;
3050
+ if (state.totalBytes > limits.maxTotalBytes) {
3051
+ throw new Error(
3052
+ `Extracted size ${state.totalBytes} bytes exceeds limit of ${limits.maxTotalBytes} bytes. Possible zip bomb.`
3053
+ );
3054
+ }
3055
+ return resolvedPath;
3056
+ }
3057
+ async function extractSkillZip(zipPath, targetDir, limits) {
3058
+ const maxTotalBytes = limits?.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
3059
+ const maxFiles = limits?.maxFiles ?? DEFAULT_MAX_FILES;
3060
+ const maxCompressionRatio = limits?.maxCompressionRatio ?? DEFAULT_MAX_COMPRESSION_RATIO;
3061
+ const resolvedTarget = resolve2(targetDir);
3062
+ mkdirSync2(resolvedTarget, { recursive: true });
3063
+ return new Promise((resolvePromise, reject) => {
3064
+ yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
3065
+ if (err) return reject(err);
3066
+ const state = { fileCount: 0, totalBytes: 0 };
3067
+ zipfile.readEntry();
3068
+ zipfile.on("entry", (entry) => {
3069
+ if (entry.fileName.endsWith("/")) {
3070
+ zipfile.readEntry();
3071
+ return;
3072
+ }
3073
+ let resolvedPath;
3074
+ try {
3075
+ resolvedPath = validateEntry(entry, resolvedTarget, state, { maxFiles, maxTotalBytes, maxCompressionRatio });
3076
+ } catch (e) {
3077
+ zipfile.close();
3078
+ return reject(e);
3079
+ }
3080
+ zipfile.openReadStream(entry, (streamErr, readStream) => {
3081
+ if (streamErr) {
3082
+ zipfile.close();
3083
+ return reject(streamErr);
3084
+ }
3085
+ mkdirSync2(dirname(resolvedPath), { recursive: true });
3086
+ const writeStream = createWriteStream(resolvedPath);
3087
+ readStream.pipe(writeStream);
3088
+ writeStream.on("close", () => zipfile.readEntry());
3089
+ writeStream.on("error", (writeErr) => {
3090
+ zipfile.close();
3091
+ reject(writeErr);
3092
+ });
3093
+ });
3094
+ });
3095
+ zipfile.on("end", () => resolvePromise(resolvedTarget));
3096
+ zipfile.on("error", reject);
3097
+ });
3098
+ });
3099
+ }
3100
+ function readMetaJson(contentDir) {
3101
+ const metaPath = join2(contentDir, "_meta.json");
3102
+ if (!existsSync(metaPath)) {
3103
+ throw new Error(`_meta.json not found in ${contentDir}. Invalid skill package.`);
3104
+ }
3105
+ const raw = readFileSync(metaPath, "utf-8");
3106
+ let parsed;
3107
+ try {
3108
+ parsed = JSON.parse(raw);
3109
+ } catch {
3110
+ throw new Error(`Failed to parse _meta.json: invalid JSON`);
3111
+ }
3112
+ const meta = parsed;
3113
+ if (typeof meta.name !== "string" || !meta.name) {
3114
+ throw new Error(`_meta.json: "name" field is required`);
3115
+ }
3116
+ if (typeof meta.version !== "string" || !meta.version) {
3117
+ throw new Error(`_meta.json: "version" field is required`);
3118
+ }
3119
+ return {
3120
+ name: String(meta.name),
3121
+ version: String(meta.version),
3122
+ title: typeof meta.title === "string" ? meta.title : "",
3123
+ description: typeof meta.description === "string" ? meta.description : ""
3124
+ };
3125
+ }
3126
+ function validateSkillMdExists(contentDir) {
3127
+ const skillMdPath = join2(contentDir, "SKILL.md");
3128
+ if (!existsSync(skillMdPath)) {
3129
+ throw new Error(`SKILL.md not found in ${contentDir}. Invalid skill package.`);
3130
+ }
3131
+ }
3132
+ var DEFAULT_REGISTRY_PATH = join3(homedir(), ".okx", "skills", "registry.json");
3133
+ function readRegistry(registryPath = DEFAULT_REGISTRY_PATH) {
3134
+ if (!existsSync2(registryPath)) {
3135
+ return { version: 1, skills: {} };
3136
+ }
3137
+ try {
3138
+ const raw = readFileSync2(registryPath, "utf-8");
3139
+ return JSON.parse(raw);
3140
+ } catch {
3141
+ return { version: 1, skills: {} };
3142
+ }
3143
+ }
3144
+ function writeRegistry(registry, registryPath = DEFAULT_REGISTRY_PATH) {
3145
+ mkdirSync3(dirname2(registryPath), { recursive: true });
3146
+ writeFileSync2(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
3147
+ }
3148
+ function upsertSkillRecord(meta, registryPath = DEFAULT_REGISTRY_PATH) {
3149
+ const registry = readRegistry(registryPath);
3150
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3151
+ const existing = registry.skills[meta.name];
3152
+ registry.skills[meta.name] = {
3153
+ name: meta.name,
3154
+ version: meta.version,
3155
+ description: meta.description,
3156
+ installedAt: existing?.installedAt ?? now,
3157
+ updatedAt: now,
3158
+ source: "marketplace"
3159
+ };
3160
+ writeRegistry(registry, registryPath);
3161
+ }
3162
+ function removeSkillRecord(name, registryPath = DEFAULT_REGISTRY_PATH) {
3163
+ const registry = readRegistry(registryPath);
3164
+ if (!(name in registry.skills)) return false;
3165
+ delete registry.skills[name];
3166
+ writeRegistry(registry, registryPath);
3167
+ return true;
3168
+ }
3169
+ function getSkillRecord(name, registryPath = DEFAULT_REGISTRY_PATH) {
3170
+ const registry = readRegistry(registryPath);
3171
+ return registry.skills[name];
3172
+ }
3173
+ function registerSkillsTools() {
3174
+ return [
3175
+ {
3176
+ name: "skills_get_categories",
3177
+ module: "skills",
3178
+ 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 \u2014 use skills_search or skills_download.",
3179
+ inputSchema: {
3180
+ type: "object",
3181
+ properties: {},
3182
+ additionalProperties: false
3183
+ },
3184
+ isWrite: false,
3185
+ handler: handleGetCategories
3186
+ },
3187
+ {
3188
+ name: "skills_search",
3189
+ module: "skills",
3190
+ 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 \u2014 use skills_download.",
3191
+ inputSchema: {
3192
+ type: "object",
3193
+ properties: {
3194
+ keyword: {
3195
+ type: "string",
3196
+ description: "Search keyword (matches name, description, tags)"
3197
+ },
3198
+ categories: {
3199
+ type: "string",
3200
+ description: "Filter by category ID"
3201
+ },
3202
+ page: {
3203
+ type: "string",
3204
+ description: "Page number, starting from 1. Default: 1"
3205
+ },
3206
+ limit: {
3207
+ type: "string",
3208
+ description: "Results per page. Default: 20, max: 100"
3209
+ }
3210
+ },
3211
+ additionalProperties: false
3212
+ },
3213
+ isWrite: false,
3214
+ handler: handleSearch
3215
+ },
3216
+ {
3217
+ name: "skills_download",
3218
+ module: "skills",
3219
+ description: "Download a skill zip file 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: This only downloads the zip \u2014 it 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.",
3220
+ inputSchema: {
3221
+ type: "object",
3222
+ properties: {
3223
+ name: {
3224
+ type: "string",
3225
+ description: "Skill name (unique identifier)"
3226
+ },
3227
+ targetDir: {
3228
+ type: "string",
3229
+ description: "Directory path where the zip file will be saved"
3230
+ }
3231
+ },
3232
+ required: ["name", "targetDir"],
3233
+ additionalProperties: false
3234
+ },
3235
+ isWrite: true,
3236
+ handler: handleDownload
3237
+ }
3238
+ ];
3239
+ }
3240
+ async function handleGetCategories(_args, ctx) {
3241
+ const result = await ctx.client.privateGet(
3242
+ "/api/v5/skill/categories"
3243
+ );
3244
+ return result;
3245
+ }
3246
+ async function handleSearch(args, ctx) {
3247
+ const query = {};
3248
+ if (args.keyword) query.keyword = String(args.keyword);
3249
+ if (args.categories) query.categories = String(args.categories);
3250
+ if (args.page) query.page = String(args.page);
3251
+ if (args.limit) query.limit = String(args.limit);
3252
+ const result = await ctx.client.privateGet(
3253
+ "/api/v5/skill/search",
3254
+ query
3255
+ );
3256
+ const totalPage = result.raw?.totalPage;
3257
+ return { ...result, totalPage };
3258
+ }
3259
+ async function handleDownload(args, ctx) {
3260
+ const name = String(args.name);
3261
+ const targetDir = String(args.targetDir);
3262
+ const filePath = await downloadSkillZip(ctx.client, name, targetDir);
3263
+ return {
3264
+ endpoint: "POST /api/v5/skill/download",
3265
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
3266
+ data: {
3267
+ name,
3268
+ filePath
3269
+ }
3270
+ };
3271
+ }
2882
3272
  function normalizeWrite(response) {
2883
3273
  const data = response.data;
2884
3274
  if (Array.isArray(data) && data.length > 0) {
@@ -3477,7 +3867,6 @@ function registerEarnTools() {
3477
3867
  required: ["ccy", "amt"]
3478
3868
  },
3479
3869
  handler: async (rawArgs, context) => {
3480
- assertNotDemo(context.config, "earn_savings_purchase");
3481
3870
  const args = asRecord(rawArgs);
3482
3871
  const response = await context.client.privatePost(
3483
3872
  "/api/v5/finance/savings/purchase-redempt",
@@ -3512,7 +3901,6 @@ function registerEarnTools() {
3512
3901
  required: ["ccy", "amt"]
3513
3902
  },
3514
3903
  handler: async (rawArgs, context) => {
3515
- assertNotDemo(context.config, "earn_savings_redeem");
3516
3904
  const args = asRecord(rawArgs);
3517
3905
  const response = await context.client.privatePost(
3518
3906
  "/api/v5/finance/savings/purchase-redempt",
@@ -3546,7 +3934,6 @@ function registerEarnTools() {
3546
3934
  required: ["ccy", "rate"]
3547
3935
  },
3548
3936
  handler: async (rawArgs, context) => {
3549
- assertNotDemo(context.config, "earn_set_lending_rate");
3550
3937
  const args = asRecord(rawArgs);
3551
3938
  const response = await context.client.privatePost(
3552
3939
  "/api/v5/finance/savings/set-lending-rate",
@@ -3690,7 +4077,7 @@ function registerOnchainEarnTools() {
3690
4077
  {
3691
4078
  name: "onchain_earn_purchase",
3692
4079
  module: "earn.onchain",
3693
- description: "Invest in a staking/DeFi product. [CAUTION] Moves real funds. Not available in demo mode.",
4080
+ description: "Invest in a staking/DeFi product. [CAUTION] Moves real funds.",
3694
4081
  isWrite: true,
3695
4082
  inputSchema: {
3696
4083
  type: "object",
@@ -3723,7 +4110,6 @@ function registerOnchainEarnTools() {
3723
4110
  required: ["productId", "investData"]
3724
4111
  },
3725
4112
  handler: async (rawArgs, context) => {
3726
- assertNotDemo(context.config, "onchain_earn_purchase");
3727
4113
  const args = asRecord(rawArgs);
3728
4114
  const response = await context.client.privatePost(
3729
4115
  "/api/v5/finance/staking-defi/purchase",
@@ -3744,7 +4130,7 @@ function registerOnchainEarnTools() {
3744
4130
  {
3745
4131
  name: "onchain_earn_redeem",
3746
4132
  module: "earn.onchain",
3747
- description: "Redeem a staking/DeFi investment. [CAUTION] Some products have lock periods, early redemption may incur penalties. Not available in demo mode.",
4133
+ description: "Redeem a staking/DeFi investment. [CAUTION] Some products have lock periods, early redemption may incur penalties.",
3748
4134
  isWrite: true,
3749
4135
  inputSchema: {
3750
4136
  type: "object",
@@ -3765,7 +4151,6 @@ function registerOnchainEarnTools() {
3765
4151
  required: ["ordId", "protocolType"]
3766
4152
  },
3767
4153
  handler: async (rawArgs, context) => {
3768
- assertNotDemo(context.config, "onchain_earn_redeem");
3769
4154
  const args = asRecord(rawArgs);
3770
4155
  const response = await context.client.privatePost(
3771
4156
  "/api/v5/finance/staking-defi/redeem",
@@ -3785,7 +4170,7 @@ function registerOnchainEarnTools() {
3785
4170
  {
3786
4171
  name: "onchain_earn_cancel",
3787
4172
  module: "earn.onchain",
3788
- description: "Cancel a pending staking/DeFi purchase order. [CAUTION] Not available in demo mode.",
4173
+ description: "Cancel a pending staking/DeFi purchase order. [CAUTION]",
3789
4174
  isWrite: true,
3790
4175
  inputSchema: {
3791
4176
  type: "object",
@@ -3802,7 +4187,6 @@ function registerOnchainEarnTools() {
3802
4187
  required: ["ordId", "protocolType"]
3803
4188
  },
3804
4189
  handler: async (rawArgs, context) => {
3805
- assertNotDemo(context.config, "onchain_earn_cancel");
3806
4190
  const args = asRecord(rawArgs);
3807
4191
  const response = await context.client.privatePost(
3808
4192
  "/api/v5/finance/staking-defi/cancel",
@@ -4095,7 +4479,6 @@ function registerDcdTools() {
4095
4479
  required: ["productId", "notionalSz", "notionalCcy"]
4096
4480
  },
4097
4481
  handler: async (rawArgs, context) => {
4098
- assertNotDemo(context.config, "dcd_subscribe");
4099
4482
  const args = asRecord(rawArgs);
4100
4483
  const productId = requireString(args, "productId");
4101
4484
  const notionalSz = requireString(args, "notionalSz");
@@ -4267,13 +4650,30 @@ function registerAutoEarnTools() {
4267
4650
  }
4268
4651
  ];
4269
4652
  }
4653
+ var EARN_DEMO_MESSAGE = "Earn features (savings, DCD, on-chain staking, auto-earn) are not available in simulated trading mode.";
4654
+ var EARN_DEMO_SUGGESTION = "Switch to a live account to use Earn features.";
4655
+ var DEMO_GUARD_SKIP = /* @__PURE__ */ new Set(["dcd_redeem"]);
4656
+ function withDemoGuard(tool) {
4657
+ if (!tool.isWrite || DEMO_GUARD_SKIP.has(tool.name)) return tool;
4658
+ const originalHandler = tool.handler;
4659
+ return {
4660
+ ...tool,
4661
+ handler: async (args, context) => {
4662
+ if (context.config.demo) {
4663
+ throw new ConfigError(EARN_DEMO_MESSAGE, EARN_DEMO_SUGGESTION);
4664
+ }
4665
+ return originalHandler(args, context);
4666
+ }
4667
+ };
4668
+ }
4270
4669
  function registerAllEarnTools() {
4271
- return [
4670
+ const tools = [
4272
4671
  ...registerEarnTools(),
4273
4672
  ...registerOnchainEarnTools(),
4274
4673
  ...registerDcdTools(),
4275
4674
  ...registerAutoEarnTools()
4276
4675
  ];
4676
+ return tools.map(withDemoGuard);
4277
4677
  }
4278
4678
  function buildContractTradeTools(cfg) {
4279
4679
  const { prefix, module, label, instTypes, instIdExample } = cfg;
@@ -4285,7 +4685,7 @@ function buildContractTradeTools(cfg) {
4285
4685
  {
4286
4686
  name: n("place_order"),
4287
4687
  module,
4288
- description: `Place ${label} order. Attach TP/SL via tpTriggerPx/slTriggerPx. [CAUTION] Executes real trades.`,
4688
+ description: `Place ${label} order. Attach TP/SL via tpTriggerPx/slTriggerPx. Before placing, use market_get_instruments to get ctVal (contract face value) \u2014 do NOT assume contract sizes. [CAUTION] Executes real trades.`,
4289
4689
  isWrite: true,
4290
4690
  inputSchema: {
4291
4691
  type: "object",
@@ -4313,7 +4713,7 @@ function buildContractTradeTools(cfg) {
4313
4713
  },
4314
4714
  sz: {
4315
4715
  type: "string",
4316
- description: "Contracts count by default. Set tgtCcy=quote_ccy to use USDT amount instead."
4716
+ description: "Number of contracts. Each contract = ctVal units (e.g. 0.1 ETH for ETH-USDT-SWAP). Query market_get_instruments for exact ctVal. Set tgtCcy=quote_ccy to specify sz in USDT instead."
4317
4717
  },
4318
4718
  tgtCcy: {
4319
4719
  type: "string",
@@ -5297,7 +5697,7 @@ function registerMarketTools() {
5297
5697
  {
5298
5698
  name: "market_get_stock_tokens",
5299
5699
  module: "market",
5300
- description: "Get all stock token instruments (instCategory=3). Stock tokens track real-world stock prices on OKX (e.g. AAPL-USDT-SWAP). Filters client-side by instCategory=3.",
5700
+ 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).',
5301
5701
  isWrite: false,
5302
5702
  inputSchema: {
5303
5703
  type: "object",
@@ -5327,6 +5727,46 @@ function registerMarketTools() {
5327
5727
  const filtered = Array.isArray(data) ? data.filter((item) => item.instCategory === "3") : data;
5328
5728
  return normalizeResponse({ ...response, data: filtered });
5329
5729
  }
5730
+ },
5731
+ {
5732
+ name: "market_get_instruments_by_category",
5733
+ module: "market",
5734
+ 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). Use this to find instIds before querying prices or placing orders. Filters client-side by instCategory.",
5735
+ isWrite: false,
5736
+ inputSchema: {
5737
+ type: "object",
5738
+ properties: {
5739
+ instCategory: {
5740
+ type: "string",
5741
+ enum: ["3", "4", "5", "6", "7"],
5742
+ description: "Asset category: 3=Stock tokens, 4=Metals, 5=Commodities, 6=Forex, 7=Bonds"
5743
+ },
5744
+ instType: {
5745
+ type: "string",
5746
+ enum: ["SPOT", "SWAP"],
5747
+ description: "Instrument type. Default: SWAP"
5748
+ },
5749
+ instId: {
5750
+ type: "string",
5751
+ description: "Optional: filter by specific instrument ID"
5752
+ }
5753
+ },
5754
+ required: ["instCategory"]
5755
+ },
5756
+ handler: async (rawArgs, context) => {
5757
+ const args = asRecord(rawArgs);
5758
+ const instCategory = requireString(args, "instCategory");
5759
+ const instType = readString(args, "instType") ?? "SWAP";
5760
+ const instId = readString(args, "instId");
5761
+ const response = await context.client.publicGet(
5762
+ "/api/v5/public/instruments",
5763
+ compactObject({ instType, instId }),
5764
+ publicRateLimit("market_get_instruments_by_category", 20)
5765
+ );
5766
+ const data = response.data;
5767
+ const filtered = Array.isArray(data) ? data.filter((item) => item.instCategory === instCategory) : data;
5768
+ return normalizeResponse({ ...response, data: filtered });
5769
+ }
5330
5770
  }
5331
5771
  ];
5332
5772
  }
@@ -5894,7 +6334,7 @@ function registerOptionTools() {
5894
6334
  {
5895
6335
  name: "option_place_order",
5896
6336
  module: "option",
5897
- description: "Place OPTION order. instId: {uly}-{expiry}-{strike}-C/P, e.g. BTC-USD-241227-50000-C. [CAUTION] Executes real trades.",
6337
+ 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) \u2014 do NOT assume contract sizes. [CAUTION] Executes real trades.",
5898
6338
  isWrite: true,
5899
6339
  inputSchema: {
5900
6340
  type: "object",
@@ -7104,7 +7544,8 @@ function allToolSpecs() {
7104
7544
  ...registerNewsTools(),
7105
7545
  ...registerBotTools(),
7106
7546
  ...registerAllEarnTools(),
7107
- ...registerAuditTools()
7547
+ ...registerAuditTools(),
7548
+ ...registerSkillsTools()
7108
7549
  ];
7109
7550
  }
7110
7551
  function createToolRunner(client, config) {
@@ -7119,12 +7560,12 @@ function createToolRunner(client, config) {
7119
7560
  };
7120
7561
  }
7121
7562
  function configFilePath() {
7122
- return join(homedir(), ".okx", "config.toml");
7563
+ return join4(homedir2(), ".okx", "config.toml");
7123
7564
  }
7124
7565
  function readFullConfig() {
7125
7566
  const path42 = configFilePath();
7126
- if (!existsSync(path42)) return { profiles: {} };
7127
- const raw = readFileSync(path42, "utf-8");
7567
+ if (!existsSync3(path42)) return { profiles: {} };
7568
+ const raw = readFileSync3(path42, "utf-8");
7128
7569
  try {
7129
7570
  return parse(raw);
7130
7571
  } catch (err) {
@@ -7152,11 +7593,11 @@ var CONFIG_HEADER = `# OKX Trade Kit Configuration
7152
7593
  `;
7153
7594
  function writeFullConfig(config) {
7154
7595
  const path42 = configFilePath();
7155
- const dir = dirname(path42);
7156
- if (!existsSync(dir)) {
7157
- mkdirSync(dir, { recursive: true });
7596
+ const dir = dirname3(path42);
7597
+ if (!existsSync3(dir)) {
7598
+ mkdirSync4(dir, { recursive: true });
7158
7599
  }
7159
- writeFileSync(path42, CONFIG_HEADER + stringify(config), "utf-8");
7600
+ writeFileSync3(path42, CONFIG_HEADER + stringify(config), "utf-8");
7160
7601
  }
7161
7602
  function expandShorthand(moduleId) {
7162
7603
  if (moduleId === "all") return [...MODULES];
@@ -7270,12 +7711,12 @@ function loadConfig(cli) {
7270
7711
  verbose: cli.verbose ?? false
7271
7712
  };
7272
7713
  }
7273
- var CACHE_FILE = join2(homedir2(), ".okx", "update-check.json");
7714
+ var CACHE_FILE = join5(homedir3(), ".okx", "update-check.json");
7274
7715
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
7275
7716
  function readCache() {
7276
7717
  try {
7277
- if (existsSync2(CACHE_FILE)) {
7278
- return JSON.parse(readFileSync2(CACHE_FILE, "utf-8"));
7718
+ if (existsSync4(CACHE_FILE)) {
7719
+ return JSON.parse(readFileSync4(CACHE_FILE, "utf-8"));
7279
7720
  }
7280
7721
  } catch {
7281
7722
  }
@@ -7283,8 +7724,8 @@ function readCache() {
7283
7724
  }
7284
7725
  function writeCache(cache) {
7285
7726
  try {
7286
- mkdirSync2(join2(homedir2(), ".okx"), { recursive: true });
7287
- writeFileSync2(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7727
+ mkdirSync5(join5(homedir3(), ".okx"), { recursive: true });
7728
+ writeFileSync4(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7288
7729
  } catch {
7289
7730
  }
7290
7731
  }
@@ -7591,26 +8032,26 @@ var Report = class {
7591
8032
  this.lines.push({ key, value });
7592
8033
  }
7593
8034
  print() {
7594
- const sep = "\u2500".repeat(52);
8035
+ const sep2 = "\u2500".repeat(52);
7595
8036
  outputLine("");
7596
- outputLine(` \u2500\u2500 Diagnostic Report (copy & share) ${sep.slice(35)}`);
8037
+ outputLine(` \u2500\u2500 Diagnostic Report (copy & share) ${sep2.slice(35)}`);
7597
8038
  for (const { key, value } of this.lines) {
7598
8039
  outputLine(` ${key.padEnd(14)} ${value}`);
7599
8040
  }
7600
- outputLine(` ${sep}`);
8041
+ outputLine(` ${sep2}`);
7601
8042
  outputLine("");
7602
8043
  }
7603
8044
  /** Write report to a file path, returns true on success. */
7604
8045
  writeToFile(filePath) {
7605
8046
  try {
7606
- const sep = "-".repeat(52);
8047
+ const sep2 = "-".repeat(52);
7607
8048
  const lines = [
7608
- `-- Diagnostic Report (copy & share) ${sep.slice(35)}`
8049
+ `-- Diagnostic Report (copy & share) ${sep2.slice(35)}`
7609
8050
  ];
7610
8051
  for (const { key, value } of this.lines) {
7611
8052
  lines.push(`${key.padEnd(14)} ${value}`);
7612
8053
  }
7613
- lines.push(sep, "");
8054
+ lines.push(sep2, "");
7614
8055
  fs2.writeFileSync(filePath, lines.join("\n"), "utf8");
7615
8056
  return true;
7616
8057
  } catch (_e) {
@@ -7986,13 +8427,13 @@ async function checkStdioHandshake(entryPath, report) {
7986
8427
  clientInfo: { name: "okx-diagnose", version: "1.0" }
7987
8428
  }
7988
8429
  });
7989
- return new Promise((resolve) => {
8430
+ return new Promise((resolve3) => {
7990
8431
  let settled = false;
7991
8432
  const settle = (passed) => {
7992
8433
  if (settled) return;
7993
8434
  settled = true;
7994
8435
  clearTimeout(timer);
7995
- resolve(passed);
8436
+ resolve3(passed);
7996
8437
  };
7997
8438
  const child = spawn(process.execPath, [entryPath], {
7998
8439
  stdio: ["pipe", "pipe", "pipe"],
@@ -8113,7 +8554,7 @@ async function cmdDiagnoseMcp(options = {}) {
8113
8554
 
8114
8555
  // src/commands/diagnose.ts
8115
8556
  var CLI_VERSION = readCliVersion();
8116
- var GIT_HASH = true ? "7b97fe6" : "dev";
8557
+ var GIT_HASH = true ? "cb0891d" : "dev";
8117
8558
  function maskKey2(key) {
8118
8559
  if (!key) return "(not set)";
8119
8560
  if (key.length <= 8) return "****";
@@ -8130,7 +8571,7 @@ async function checkDns(hostname) {
8130
8571
  }
8131
8572
  async function checkSocket(createFn, successEvent, timeoutMs) {
8132
8573
  const t0 = Date.now();
8133
- return new Promise((resolve) => {
8574
+ return new Promise((resolve3) => {
8134
8575
  const socket = createFn();
8135
8576
  const cleanup = () => {
8136
8577
  socket.removeAllListeners();
@@ -8138,15 +8579,15 @@ async function checkSocket(createFn, successEvent, timeoutMs) {
8138
8579
  };
8139
8580
  socket.once(successEvent, () => {
8140
8581
  cleanup();
8141
- resolve({ ok: true, ms: Date.now() - t0 });
8582
+ resolve3({ ok: true, ms: Date.now() - t0 });
8142
8583
  });
8143
8584
  socket.once("timeout", () => {
8144
8585
  cleanup();
8145
- resolve({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
8586
+ resolve3({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
8146
8587
  });
8147
8588
  socket.once("error", (err) => {
8148
8589
  cleanup();
8149
- resolve({ ok: false, ms: Date.now() - t0, error: err.message });
8590
+ resolve3({ ok: false, ms: Date.now() - t0, error: err.message });
8150
8591
  });
8151
8592
  });
8152
8593
  }
@@ -8410,24 +8851,24 @@ async function runCliChecks(config, profile, outputPath) {
8410
8851
 
8411
8852
  // src/commands/upgrade.ts
8412
8853
  import { spawnSync as spawnSync2 } from "child_process";
8413
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
8414
- import { dirname as dirname3, join as join4 } from "path";
8415
- import { homedir as homedir4 } from "os";
8854
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
8855
+ import { dirname as dirname5, join as join7 } from "path";
8856
+ import { homedir as homedir5 } from "os";
8416
8857
  var PACKAGES = ["@okx_ai/okx-trade-mcp", "@okx_ai/okx-trade-cli"];
8417
- var CACHE_FILE2 = join4(homedir4(), ".okx", "last_check");
8858
+ var CACHE_FILE2 = join7(homedir5(), ".okx", "last_check");
8418
8859
  var THROTTLE_MS = 12 * 60 * 60 * 1e3;
8419
- var NPM_BIN = join4(dirname3(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
8860
+ var NPM_BIN = join7(dirname5(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
8420
8861
  function readLastCheck() {
8421
8862
  try {
8422
- return parseInt(readFileSync4(CACHE_FILE2, "utf-8").trim(), 10) || 0;
8863
+ return parseInt(readFileSync6(CACHE_FILE2, "utf-8").trim(), 10) || 0;
8423
8864
  } catch {
8424
8865
  return 0;
8425
8866
  }
8426
8867
  }
8427
8868
  function writeLastCheck() {
8428
8869
  try {
8429
- mkdirSync4(join4(homedir4(), ".okx"), { recursive: true });
8430
- writeFileSync4(CACHE_FILE2, String(Math.floor(Date.now() / 1e3)), "utf-8");
8870
+ mkdirSync7(join7(homedir5(), ".okx"), { recursive: true });
8871
+ writeFileSync6(CACHE_FILE2, String(Math.floor(Date.now() / 1e3)), "utf-8");
8431
8872
  } catch {
8432
8873
  }
8433
8874
  }
@@ -8632,7 +9073,12 @@ async function cmdNewsDetail(run, id, opts) {
8632
9073
  return;
8633
9074
  }
8634
9075
  const rawContent = article["content"] ? String(article["content"]) : void 0;
8635
- const content = rawContent ? rawContent.length > 500 ? rawContent.slice(0, 500) + "... (use --json for full text)" : rawContent : void 0;
9076
+ let content;
9077
+ if (rawContent && rawContent.length > 500) {
9078
+ content = rawContent.slice(0, 500) + "... (use --json for full text)";
9079
+ } else {
9080
+ content = rawContent;
9081
+ }
8636
9082
  printKv({
8637
9083
  id: article["id"],
8638
9084
  title: article["title"],
@@ -8820,7 +9266,11 @@ var HELP_TREE = {
8820
9266
  },
8821
9267
  "stock-tokens": {
8822
9268
  usage: "okx market stock-tokens [--instType <SPOT|SWAP>] [--instId <id>]",
8823
- description: "List all stock token instruments (instCategory=3, e.g. AAPL-USDT-SWAP)"
9269
+ description: "[Deprecated: use instruments-by-category --instCategory 3] List all stock token instruments (instCategory=3, e.g. AAPL-USDT-SWAP)"
9270
+ },
9271
+ "instruments-by-category": {
9272
+ usage: "okx market instruments-by-category --instCategory <4|5|6|7> [--instType <SPOT|SWAP>] [--instId <id>]",
9273
+ description: "List instruments by asset category: 4=Metals (gold/silver), 5=Commodities (oil/gas), 6=Forex (EUR/USD), 7=Bonds"
8824
9274
  }
8825
9275
  }
8826
9276
  },
@@ -9577,6 +10027,7 @@ var CLI_OPTIONS = {
9577
10027
  live: { type: "boolean", default: false },
9578
10028
  // market extras
9579
10029
  instType: { type: "string" },
10030
+ instCategory: { type: "string" },
9580
10031
  quoteCcy: { type: "string" },
9581
10032
  // account extras
9582
10033
  archive: { type: "boolean", default: false },
@@ -9666,6 +10117,10 @@ var CLI_OPTIONS = {
9666
10117
  period: { type: "string" },
9667
10118
  points: { type: "string" },
9668
10119
  "sort-by": { type: "string" },
10120
+ // skill marketplace
10121
+ categories: { type: "string" },
10122
+ dir: { type: "string" },
10123
+ page: { type: "string" },
9669
10124
  // diagnostics — cli/mcp/all/output are diagnose-specific; verbose is shared
9670
10125
  verbose: { type: "boolean", default: false },
9671
10126
  mcp: { type: "boolean", default: false },
@@ -9949,6 +10404,37 @@ async function cmdMarketIndicator(run, indicator, instId, opts) {
9949
10404
  }
9950
10405
  }
9951
10406
  }
10407
+ async function cmdMarketInstrumentsByCategory(run, opts) {
10408
+ const result = await run("market_get_instruments_by_category", {
10409
+ instCategory: opts.instCategory,
10410
+ instType: opts.instType,
10411
+ instId: opts.instId
10412
+ });
10413
+ const items = getData2(result);
10414
+ if (opts.json) return printJson(items);
10415
+ const CATEGORY_LABELS = {
10416
+ "3": "Stock tokens",
10417
+ "4": "Metals",
10418
+ "5": "Commodities",
10419
+ "6": "Forex",
10420
+ "7": "Bonds"
10421
+ };
10422
+ const label = CATEGORY_LABELS[opts.instCategory] ?? opts.instCategory;
10423
+ process.stdout.write(`instCategory=${opts.instCategory} (${label}) \u2014 ${items?.length ?? 0} instruments
10424
+
10425
+ `);
10426
+ printTable(
10427
+ (items ?? []).slice(0, 50).map((t) => ({
10428
+ instId: t["instId"],
10429
+ instCategory: t["instCategory"],
10430
+ ctVal: t["ctVal"],
10431
+ lotSz: t["lotSz"],
10432
+ minSz: t["minSz"],
10433
+ tickSz: t["tickSz"],
10434
+ state: t["state"]
10435
+ }))
10436
+ );
10437
+ }
9952
10438
  async function cmdMarketStockTokens(run, opts) {
9953
10439
  const result = await run("market_get_stock_tokens", { instType: opts.instType, instId: opts.instId });
9954
10440
  const items = getData2(result);
@@ -11384,7 +11870,7 @@ Config saved to ${p}
11384
11870
  }
11385
11871
  };
11386
11872
  function prompt(rl, question) {
11387
- return new Promise((resolve) => rl.question(question, resolve));
11873
+ return new Promise((resolve3) => rl.question(question, resolve3));
11388
11874
  }
11389
11875
  function cmdConfigShow(json) {
11390
11876
  const config = readFullConfig();
@@ -12349,10 +12835,194 @@ async function cmdDcdQuoteAndBuy(run, opts) {
12349
12835
  }
12350
12836
  }
12351
12837
 
12838
+ // src/commands/skill.ts
12839
+ import { tmpdir, homedir as homedir7 } from "os";
12840
+ import { join as join9, dirname as dirname6 } from "path";
12841
+ import { mkdirSync as mkdirSync8, rmSync, existsSync as existsSync7, copyFileSync as copyFileSync2 } from "fs";
12842
+ import { execFileSync as execFileSync2 } from "child_process";
12843
+ import { randomUUID as randomUUID2 } from "crypto";
12844
+ function resolveNpx() {
12845
+ const sibling = join9(dirname6(process.execPath), "npx");
12846
+ if (existsSync7(sibling)) return sibling;
12847
+ return "npx";
12848
+ }
12849
+ async function cmdSkillSearch(run, opts) {
12850
+ const args = {};
12851
+ if (opts.keyword) args.keyword = opts.keyword;
12852
+ if (opts.categories) args.categories = opts.categories;
12853
+ if (opts.page) args.page = opts.page;
12854
+ if (opts.limit) args.limit = opts.limit;
12855
+ const result = await run("skills_search", args);
12856
+ const data = result.data;
12857
+ const totalPage = result.totalPage;
12858
+ if (opts.json) {
12859
+ outputLine(JSON.stringify(result, null, 2));
12860
+ return;
12861
+ }
12862
+ if (!Array.isArray(data) || data.length === 0) {
12863
+ outputLine("No skills found.");
12864
+ return;
12865
+ }
12866
+ outputLine("");
12867
+ outputLine(" NAME VERSION DESCRIPTION");
12868
+ for (const item of data) {
12869
+ const name = (item.name ?? "").padEnd(20);
12870
+ const ver = (item.latestVersion ?? "").padEnd(10);
12871
+ const desc = (item.description ?? "").slice(0, 50);
12872
+ outputLine(` ${name}${ver}${desc}`);
12873
+ }
12874
+ outputLine("");
12875
+ const page = opts.page ?? "1";
12876
+ const pageInfo = totalPage ? ` (page ${page}/${totalPage})` : "";
12877
+ outputLine(`${data.length} skills found${pageInfo}. Use \`okx skill add <name>\` to install.`);
12878
+ }
12879
+ async function cmdSkillCategories(run, json) {
12880
+ const result = await run("skills_get_categories", {});
12881
+ const data = result.data;
12882
+ if (json) {
12883
+ outputLine(JSON.stringify(result, null, 2));
12884
+ return;
12885
+ }
12886
+ if (!Array.isArray(data) || data.length === 0) {
12887
+ outputLine("No categories found.");
12888
+ return;
12889
+ }
12890
+ outputLine("");
12891
+ outputLine(" ID NAME");
12892
+ for (const cat of data) {
12893
+ outputLine(` ${(cat.categoryId ?? "").padEnd(20)}${cat.name ?? ""}`);
12894
+ }
12895
+ outputLine("");
12896
+ }
12897
+ async function cmdSkillAdd(name, config, json) {
12898
+ const tmpBase = join9(tmpdir(), `okx-skill-${randomUUID2()}`);
12899
+ mkdirSync8(tmpBase, { recursive: true });
12900
+ try {
12901
+ outputLine(`Downloading ${name}...`);
12902
+ const client = new OkxRestClient(config);
12903
+ const zipPath = await downloadSkillZip(client, name, tmpBase);
12904
+ const contentDir = await extractSkillZip(zipPath, join9(tmpBase, "content"));
12905
+ const meta = readMetaJson(contentDir);
12906
+ validateSkillMdExists(contentDir);
12907
+ outputLine("Installing to detected agents...");
12908
+ try {
12909
+ execFileSync2(resolveNpx(), ["skills", "add", contentDir, "-y", "-g"], {
12910
+ stdio: "inherit",
12911
+ timeout: 6e4
12912
+ });
12913
+ } catch (e) {
12914
+ const savedZip = join9(process.cwd(), `${name}.zip`);
12915
+ try {
12916
+ copyFileSync2(zipPath, savedZip);
12917
+ } catch {
12918
+ }
12919
+ errorLine(`npx skills add failed. The zip has been downloaded but not installed.`);
12920
+ errorLine(`You can manually install from: ${savedZip}`);
12921
+ throw e;
12922
+ }
12923
+ upsertSkillRecord(meta);
12924
+ if (json) {
12925
+ outputLine(JSON.stringify({ name: meta.name, version: meta.version, status: "installed" }, null, 2));
12926
+ } else {
12927
+ outputLine(`\u2713 Skill "${meta.name}" v${meta.version} installed`);
12928
+ }
12929
+ } finally {
12930
+ rmSync(tmpBase, { recursive: true, force: true });
12931
+ }
12932
+ }
12933
+ async function cmdSkillDownload(name, targetDir, config, json) {
12934
+ outputLine(`Downloading ${name}...`);
12935
+ const client = new OkxRestClient(config);
12936
+ const filePath = await downloadSkillZip(client, name, targetDir);
12937
+ if (json) {
12938
+ outputLine(JSON.stringify({ name, filePath }, null, 2));
12939
+ } else {
12940
+ outputLine(`\u2713 Downloaded ${name}.zip`);
12941
+ outputLine(` Path: ${filePath}`);
12942
+ }
12943
+ }
12944
+ function cmdSkillRemove(name, json) {
12945
+ const removed = removeSkillRecord(name);
12946
+ if (!removed) {
12947
+ errorLine(`Skill "${name}" is not installed.`);
12948
+ process.exitCode = 1;
12949
+ return;
12950
+ }
12951
+ try {
12952
+ execFileSync2(resolveNpx(), ["skills", "remove", name, "-y", "-g"], {
12953
+ stdio: "inherit",
12954
+ timeout: 6e4
12955
+ });
12956
+ } catch {
12957
+ const agentsPath = join9(homedir7(), ".agents", "skills", name);
12958
+ try {
12959
+ rmSync(agentsPath, { recursive: true, force: true });
12960
+ } catch {
12961
+ }
12962
+ }
12963
+ if (json) {
12964
+ outputLine(JSON.stringify({ name, status: "removed" }, null, 2));
12965
+ } else {
12966
+ outputLine(`\u2713 Skill "${name}" removed`);
12967
+ }
12968
+ }
12969
+ async function cmdSkillCheck(run, name, json) {
12970
+ const local = getSkillRecord(name);
12971
+ if (!local) {
12972
+ errorLine(`Skill "${name}" is not installed.`);
12973
+ process.exitCode = 1;
12974
+ return;
12975
+ }
12976
+ const result = await run("skills_search", { keyword: name });
12977
+ const data = result.data;
12978
+ const remote = data?.find((s) => s.name === name);
12979
+ if (!remote) {
12980
+ errorLine(`Skill "${name}" not found in marketplace.`);
12981
+ process.exitCode = 1;
12982
+ return;
12983
+ }
12984
+ const upToDate = local.version === remote.latestVersion;
12985
+ if (json) {
12986
+ outputLine(JSON.stringify({
12987
+ name,
12988
+ installedVersion: local.version,
12989
+ latestVersion: remote.latestVersion,
12990
+ upToDate
12991
+ }, null, 2));
12992
+ } else if (upToDate) {
12993
+ outputLine(`${name}: installed v${local.version} \u2192 latest v${remote.latestVersion} (up to date)`);
12994
+ } else {
12995
+ outputLine(`${name}: installed v${local.version} \u2192 latest v${remote.latestVersion} (update available)`);
12996
+ outputLine(` Use \`okx skill add ${name}\` to update.`);
12997
+ }
12998
+ }
12999
+ function cmdSkillList(json) {
13000
+ const registry = readRegistry();
13001
+ const skills = Object.values(registry.skills);
13002
+ if (json) {
13003
+ outputLine(JSON.stringify(registry, null, 2));
13004
+ return;
13005
+ }
13006
+ if (skills.length === 0) {
13007
+ outputLine("No skills installed.");
13008
+ return;
13009
+ }
13010
+ outputLine("");
13011
+ outputLine(" NAME VERSION INSTALLED AT");
13012
+ for (const s of skills) {
13013
+ const name = s.name.padEnd(20);
13014
+ const ver = s.version.padEnd(10);
13015
+ const date = s.installedAt.slice(0, 19).replace("T", " ");
13016
+ outputLine(` ${name}${ver}${date}`);
13017
+ }
13018
+ outputLine("");
13019
+ outputLine(`${skills.length} skills installed.`);
13020
+ }
13021
+
12352
13022
  // src/index.ts
12353
13023
  var _require3 = createRequire3(import.meta.url);
12354
13024
  var CLI_VERSION2 = _require3("../package.json").version;
12355
- var GIT_HASH2 = true ? "7b97fe6" : "dev";
13025
+ var GIT_HASH2 = true ? "cb0891d" : "dev";
12356
13026
  function handleConfigCommand(action, rest, json, lang, force) {
12357
13027
  if (action === "init") return cmdConfigInit(lang === "zh" ? "zh" : "en");
12358
13028
  if (action === "show") return cmdConfigShow(json);
@@ -12395,6 +13065,13 @@ function handleMarketPublicCommand(run, action, rest, v, json) {
12395
13065
  return cmdMarketOpenInterest(run, { instType: v.instType, instId: v.instId, json });
12396
13066
  if (action === "stock-tokens")
12397
13067
  return cmdMarketStockTokens(run, { instType: v.instType, instId: v.instId, json });
13068
+ if (action === "instruments-by-category")
13069
+ return cmdMarketInstrumentsByCategory(run, {
13070
+ instCategory: v.instCategory,
13071
+ instType: v.instType,
13072
+ instId: v.instId,
13073
+ json
13074
+ });
12398
13075
  if (action === "indicator") {
12399
13076
  const limit = v.limit !== void 0 ? Number(v.limit) : void 0;
12400
13077
  const backtestTime = v["backtest-time"] !== void 0 ? Number(v["backtest-time"]) : void 0;
@@ -13141,6 +13818,42 @@ function handleNewsCommand(run, action, rest, v, json) {
13141
13818
  `);
13142
13819
  process.exitCode = 1;
13143
13820
  }
13821
+ function requireSkillName(rest, usage) {
13822
+ const name = rest[0];
13823
+ if (!name) {
13824
+ errorLine(usage);
13825
+ process.exitCode = 1;
13826
+ }
13827
+ return name;
13828
+ }
13829
+ function handleSkillAdd(rest, config, json) {
13830
+ const n = requireSkillName(rest, "Usage: okx skill add <name>");
13831
+ if (n) return cmdSkillAdd(n, config, json);
13832
+ }
13833
+ function handleSkillDownload(rest, v, config, json) {
13834
+ const n = requireSkillName(rest, "Usage: okx skill download <name> [--dir <path>]");
13835
+ if (n) return cmdSkillDownload(n, v.dir ?? process.cwd(), config, json);
13836
+ }
13837
+ function handleSkillRemove(rest, json) {
13838
+ const n = requireSkillName(rest, "Usage: okx skill remove <name>");
13839
+ if (n) cmdSkillRemove(n, json);
13840
+ }
13841
+ function handleSkillCheck(run, rest, json) {
13842
+ const n = requireSkillName(rest, "Usage: okx skill check <name>");
13843
+ if (n) return cmdSkillCheck(run, n, json);
13844
+ }
13845
+ function handleSkillCommand(run, action, rest, v, json, config) {
13846
+ if (action === "search") return cmdSkillSearch(run, { keyword: rest[0] ?? v.keyword, categories: v.categories, page: v.page, limit: v.limit, json });
13847
+ if (action === "categories") return cmdSkillCategories(run, json);
13848
+ if (action === "list") return cmdSkillList(json);
13849
+ if (action === "add") return handleSkillAdd(rest, config, json);
13850
+ if (action === "download") return handleSkillDownload(rest, v, config, json);
13851
+ if (action === "remove") return handleSkillRemove(rest, json);
13852
+ if (action === "check") return handleSkillCheck(run, rest, json);
13853
+ errorLine(`Unknown skill command: ${action}`);
13854
+ errorLine("Valid: search, categories, add, download, remove, check, list");
13855
+ process.exitCode = 1;
13856
+ }
13144
13857
  function outputResult(result, json) {
13145
13858
  if (json) {
13146
13859
  outputLine(JSON.stringify(result, null, 2));
@@ -13203,7 +13916,8 @@ async function main() {
13203
13916
  option: () => handleOptionCommand(run, action, rest, v, json),
13204
13917
  news: () => handleNewsCommand(run, action, rest, v, json),
13205
13918
  bot: () => handleBotCommand(run, action, rest, v, json),
13206
- earn: () => handleEarnCommand(run, action, rest, v, json)
13919
+ earn: () => handleEarnCommand(run, action, rest, v, json),
13920
+ skill: () => handleSkillCommand(run, action, rest, v, json, config)
13207
13921
  };
13208
13922
  const handler = moduleHandlers[module];
13209
13923
  if (handler) return handler();
@@ -13234,6 +13948,7 @@ export {
13234
13948
  handleOptionAlgoCommand,
13235
13949
  handleOptionCommand,
13236
13950
  handleSetupCommand,
13951
+ handleSkillCommand,
13237
13952
  handleSpotAlgoCommand,
13238
13953
  handleSpotCommand,
13239
13954
  handleSwapAlgoCommand,