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

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
  {
@@ -1901,7 +1998,16 @@ function registerAccountTools() {
1901
1998
  {},
1902
1999
  privateRateLimit("account_get_config", 5)
1903
2000
  );
1904
- return normalizeResponse(response);
2001
+ const result = normalizeResponse(response);
2002
+ if (Array.isArray(result.data)) {
2003
+ return {
2004
+ ...result,
2005
+ data: result.data.map(
2006
+ ({ settleCcy, settleCcyList, ...rest }) => rest
2007
+ )
2008
+ };
2009
+ }
2010
+ return result;
1905
2011
  }
1906
2012
  },
1907
2013
  {
@@ -2879,6 +2985,299 @@ function registerAuditTools() {
2879
2985
  }
2880
2986
  ];
2881
2987
  }
2988
+ function safeWriteFile(targetDir, fileName, data) {
2989
+ const safeName = basename(fileName);
2990
+ if (!safeName || safeName === "." || safeName === "..") {
2991
+ throw new Error(`Invalid file name: "${fileName}"`);
2992
+ }
2993
+ const resolvedDir = resolve(targetDir);
2994
+ const filePath = join(resolvedDir, safeName);
2995
+ const resolvedPath = resolve(filePath);
2996
+ if (!resolvedPath.startsWith(resolvedDir + sep)) {
2997
+ throw new Error(`Path traversal detected: "${fileName}" resolves outside target directory`);
2998
+ }
2999
+ mkdirSync(resolvedDir, { recursive: true });
3000
+ const tmpPath = `${resolvedPath}.${randomUUID()}.tmp`;
3001
+ try {
3002
+ writeFileSync(tmpPath, data);
3003
+ renameSync(tmpPath, resolvedPath);
3004
+ } catch (err) {
3005
+ try {
3006
+ unlinkSync(tmpPath);
3007
+ } catch {
3008
+ }
3009
+ throw err;
3010
+ }
3011
+ return resolvedPath;
3012
+ }
3013
+ function validateZipEntryPath(targetDir, entryName) {
3014
+ const resolvedDir = resolve(targetDir);
3015
+ const resolvedEntry = resolve(resolvedDir, entryName);
3016
+ if (!resolvedEntry.startsWith(resolvedDir + sep) && resolvedEntry !== resolvedDir) {
3017
+ throw new Error(`Zip path traversal detected: "${entryName}" resolves outside extraction directory`);
3018
+ }
3019
+ return resolvedEntry;
3020
+ }
3021
+ var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
3022
+ async function downloadSkillZip(client, name, targetDir) {
3023
+ const result = await client.privatePostBinary(
3024
+ "/api/v5/skill/download",
3025
+ { name },
3026
+ { maxBytes: MAX_DOWNLOAD_BYTES }
3027
+ );
3028
+ const fileName = `${name}.zip`;
3029
+ const filePath = safeWriteFile(targetDir, fileName, result.data);
3030
+ return filePath;
3031
+ }
3032
+ var DEFAULT_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
3033
+ var DEFAULT_MAX_FILES = 1e3;
3034
+ var DEFAULT_MAX_COMPRESSION_RATIO = 100;
3035
+ function validateEntry(entry, targetDir, state, limits) {
3036
+ state.fileCount++;
3037
+ if (state.fileCount > limits.maxFiles) {
3038
+ throw new Error(
3039
+ `Zip contains more than ${limits.maxFiles} entries, exceeding limit of ${limits.maxFiles}. Possible zip bomb.`
3040
+ );
3041
+ }
3042
+ const resolvedPath = validateZipEntryPath(targetDir, entry.fileName);
3043
+ const externalAttrs = entry.externalFileAttributes;
3044
+ if (externalAttrs) {
3045
+ const unixMode = externalAttrs >>> 16 & 65535;
3046
+ if ((unixMode & 40960) === 40960) {
3047
+ throw new Error(`Zip entry "${entry.fileName}" is a symlink. Symlinks are not allowed for security.`);
3048
+ }
3049
+ }
3050
+ if (entry.compressedSize > 0) {
3051
+ const ratio = entry.uncompressedSize / entry.compressedSize;
3052
+ if (ratio > limits.maxCompressionRatio) {
3053
+ throw new Error(
3054
+ `Zip entry "${entry.fileName}" has compression ratio ${ratio.toFixed(1)}, exceeding limit of ${limits.maxCompressionRatio}. Possible zip bomb.`
3055
+ );
3056
+ }
3057
+ }
3058
+ state.totalBytes += entry.uncompressedSize;
3059
+ if (state.totalBytes > limits.maxTotalBytes) {
3060
+ throw new Error(
3061
+ `Extracted size ${state.totalBytes} bytes exceeds limit of ${limits.maxTotalBytes} bytes. Possible zip bomb.`
3062
+ );
3063
+ }
3064
+ return resolvedPath;
3065
+ }
3066
+ async function extractSkillZip(zipPath, targetDir, limits) {
3067
+ const maxTotalBytes = limits?.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
3068
+ const maxFiles = limits?.maxFiles ?? DEFAULT_MAX_FILES;
3069
+ const maxCompressionRatio = limits?.maxCompressionRatio ?? DEFAULT_MAX_COMPRESSION_RATIO;
3070
+ const resolvedTarget = resolve2(targetDir);
3071
+ mkdirSync2(resolvedTarget, { recursive: true });
3072
+ return new Promise((resolvePromise, reject) => {
3073
+ yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
3074
+ if (err) return reject(err);
3075
+ const state = { fileCount: 0, totalBytes: 0 };
3076
+ zipfile.readEntry();
3077
+ zipfile.on("entry", (entry) => {
3078
+ if (entry.fileName.endsWith("/")) {
3079
+ zipfile.readEntry();
3080
+ return;
3081
+ }
3082
+ let resolvedPath;
3083
+ try {
3084
+ resolvedPath = validateEntry(entry, resolvedTarget, state, { maxFiles, maxTotalBytes, maxCompressionRatio });
3085
+ } catch (e) {
3086
+ zipfile.close();
3087
+ return reject(e);
3088
+ }
3089
+ zipfile.openReadStream(entry, (streamErr, readStream) => {
3090
+ if (streamErr) {
3091
+ zipfile.close();
3092
+ return reject(streamErr);
3093
+ }
3094
+ mkdirSync2(dirname(resolvedPath), { recursive: true });
3095
+ const writeStream = createWriteStream(resolvedPath);
3096
+ readStream.pipe(writeStream);
3097
+ writeStream.on("close", () => zipfile.readEntry());
3098
+ writeStream.on("error", (writeErr) => {
3099
+ zipfile.close();
3100
+ reject(writeErr);
3101
+ });
3102
+ });
3103
+ });
3104
+ zipfile.on("end", () => resolvePromise(resolvedTarget));
3105
+ zipfile.on("error", reject);
3106
+ });
3107
+ });
3108
+ }
3109
+ function readMetaJson(contentDir) {
3110
+ const metaPath = join2(contentDir, "_meta.json");
3111
+ if (!existsSync(metaPath)) {
3112
+ throw new Error(`_meta.json not found in ${contentDir}. Invalid skill package.`);
3113
+ }
3114
+ const raw = readFileSync(metaPath, "utf-8");
3115
+ let parsed;
3116
+ try {
3117
+ parsed = JSON.parse(raw);
3118
+ } catch {
3119
+ throw new Error(`Failed to parse _meta.json: invalid JSON`);
3120
+ }
3121
+ const meta = parsed;
3122
+ if (typeof meta.name !== "string" || !meta.name) {
3123
+ throw new Error(`_meta.json: "name" field is required`);
3124
+ }
3125
+ if (typeof meta.version !== "string" || !meta.version) {
3126
+ throw new Error(`_meta.json: "version" field is required`);
3127
+ }
3128
+ return {
3129
+ name: String(meta.name),
3130
+ version: String(meta.version),
3131
+ title: typeof meta.title === "string" ? meta.title : "",
3132
+ description: typeof meta.description === "string" ? meta.description : ""
3133
+ };
3134
+ }
3135
+ function validateSkillMdExists(contentDir) {
3136
+ const skillMdPath = join2(contentDir, "SKILL.md");
3137
+ if (!existsSync(skillMdPath)) {
3138
+ throw new Error(`SKILL.md not found in ${contentDir}. Invalid skill package.`);
3139
+ }
3140
+ }
3141
+ var DEFAULT_REGISTRY_PATH = join3(homedir(), ".okx", "skills", "registry.json");
3142
+ function readRegistry(registryPath = DEFAULT_REGISTRY_PATH) {
3143
+ if (!existsSync2(registryPath)) {
3144
+ return { version: 1, skills: {} };
3145
+ }
3146
+ try {
3147
+ const raw = readFileSync2(registryPath, "utf-8");
3148
+ return JSON.parse(raw);
3149
+ } catch {
3150
+ return { version: 1, skills: {} };
3151
+ }
3152
+ }
3153
+ function writeRegistry(registry, registryPath = DEFAULT_REGISTRY_PATH) {
3154
+ mkdirSync3(dirname2(registryPath), { recursive: true });
3155
+ writeFileSync2(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
3156
+ }
3157
+ function upsertSkillRecord(meta, registryPath = DEFAULT_REGISTRY_PATH) {
3158
+ const registry = readRegistry(registryPath);
3159
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3160
+ const existing = registry.skills[meta.name];
3161
+ registry.skills[meta.name] = {
3162
+ name: meta.name,
3163
+ version: meta.version,
3164
+ description: meta.description,
3165
+ installedAt: existing?.installedAt ?? now,
3166
+ updatedAt: now,
3167
+ source: "marketplace"
3168
+ };
3169
+ writeRegistry(registry, registryPath);
3170
+ }
3171
+ function removeSkillRecord(name, registryPath = DEFAULT_REGISTRY_PATH) {
3172
+ const registry = readRegistry(registryPath);
3173
+ if (!(name in registry.skills)) return false;
3174
+ delete registry.skills[name];
3175
+ writeRegistry(registry, registryPath);
3176
+ return true;
3177
+ }
3178
+ function getSkillRecord(name, registryPath = DEFAULT_REGISTRY_PATH) {
3179
+ const registry = readRegistry(registryPath);
3180
+ return registry.skills[name];
3181
+ }
3182
+ function registerSkillsTools() {
3183
+ return [
3184
+ {
3185
+ name: "skills_get_categories",
3186
+ module: "skills",
3187
+ 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.",
3188
+ inputSchema: {
3189
+ type: "object",
3190
+ properties: {},
3191
+ additionalProperties: false
3192
+ },
3193
+ isWrite: false,
3194
+ handler: handleGetCategories
3195
+ },
3196
+ {
3197
+ name: "skills_search",
3198
+ module: "skills",
3199
+ 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.",
3200
+ inputSchema: {
3201
+ type: "object",
3202
+ properties: {
3203
+ keyword: {
3204
+ type: "string",
3205
+ description: "Search keyword (matches name, description, tags)"
3206
+ },
3207
+ categories: {
3208
+ type: "string",
3209
+ description: "Filter by category ID"
3210
+ },
3211
+ page: {
3212
+ type: "string",
3213
+ description: "Page number, starting from 1. Default: 1"
3214
+ },
3215
+ limit: {
3216
+ type: "string",
3217
+ description: "Results per page. Default: 20, max: 100"
3218
+ }
3219
+ },
3220
+ additionalProperties: false
3221
+ },
3222
+ isWrite: false,
3223
+ handler: handleSearch
3224
+ },
3225
+ {
3226
+ name: "skills_download",
3227
+ module: "skills",
3228
+ 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.",
3229
+ inputSchema: {
3230
+ type: "object",
3231
+ properties: {
3232
+ name: {
3233
+ type: "string",
3234
+ description: "Skill name (unique identifier)"
3235
+ },
3236
+ targetDir: {
3237
+ type: "string",
3238
+ description: "Directory path where the zip file will be saved"
3239
+ }
3240
+ },
3241
+ required: ["name", "targetDir"],
3242
+ additionalProperties: false
3243
+ },
3244
+ isWrite: true,
3245
+ handler: handleDownload
3246
+ }
3247
+ ];
3248
+ }
3249
+ async function handleGetCategories(_args, ctx) {
3250
+ const result = await ctx.client.privateGet(
3251
+ "/api/v5/skill/categories"
3252
+ );
3253
+ return result;
3254
+ }
3255
+ async function handleSearch(args, ctx) {
3256
+ const query = {};
3257
+ if (args.keyword) query.keyword = String(args.keyword);
3258
+ if (args.categories) query.categories = String(args.categories);
3259
+ if (args.page) query.page = String(args.page);
3260
+ if (args.limit) query.limit = String(args.limit);
3261
+ const result = await ctx.client.privateGet(
3262
+ "/api/v5/skill/search",
3263
+ query
3264
+ );
3265
+ const totalPage = result.raw?.totalPage;
3266
+ return { ...result, totalPage };
3267
+ }
3268
+ async function handleDownload(args, ctx) {
3269
+ const name = String(args.name);
3270
+ const targetDir = String(args.targetDir);
3271
+ const filePath = await downloadSkillZip(ctx.client, name, targetDir);
3272
+ return {
3273
+ endpoint: "POST /api/v5/skill/download",
3274
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
3275
+ data: {
3276
+ name,
3277
+ filePath
3278
+ }
3279
+ };
3280
+ }
2882
3281
  function normalizeWrite(response) {
2883
3282
  const data = response.data;
2884
3283
  if (Array.isArray(data) && data.length > 0) {
@@ -3477,7 +3876,6 @@ function registerEarnTools() {
3477
3876
  required: ["ccy", "amt"]
3478
3877
  },
3479
3878
  handler: async (rawArgs, context) => {
3480
- assertNotDemo(context.config, "earn_savings_purchase");
3481
3879
  const args = asRecord(rawArgs);
3482
3880
  const response = await context.client.privatePost(
3483
3881
  "/api/v5/finance/savings/purchase-redempt",
@@ -3512,7 +3910,6 @@ function registerEarnTools() {
3512
3910
  required: ["ccy", "amt"]
3513
3911
  },
3514
3912
  handler: async (rawArgs, context) => {
3515
- assertNotDemo(context.config, "earn_savings_redeem");
3516
3913
  const args = asRecord(rawArgs);
3517
3914
  const response = await context.client.privatePost(
3518
3915
  "/api/v5/finance/savings/purchase-redempt",
@@ -3546,7 +3943,6 @@ function registerEarnTools() {
3546
3943
  required: ["ccy", "rate"]
3547
3944
  },
3548
3945
  handler: async (rawArgs, context) => {
3549
- assertNotDemo(context.config, "earn_set_lending_rate");
3550
3946
  const args = asRecord(rawArgs);
3551
3947
  const response = await context.client.privatePost(
3552
3948
  "/api/v5/finance/savings/set-lending-rate",
@@ -3690,7 +4086,7 @@ function registerOnchainEarnTools() {
3690
4086
  {
3691
4087
  name: "onchain_earn_purchase",
3692
4088
  module: "earn.onchain",
3693
- description: "Invest in a staking/DeFi product. [CAUTION] Moves real funds. Not available in demo mode.",
4089
+ description: "Invest in a staking/DeFi product. [CAUTION] Moves real funds.",
3694
4090
  isWrite: true,
3695
4091
  inputSchema: {
3696
4092
  type: "object",
@@ -3723,7 +4119,6 @@ function registerOnchainEarnTools() {
3723
4119
  required: ["productId", "investData"]
3724
4120
  },
3725
4121
  handler: async (rawArgs, context) => {
3726
- assertNotDemo(context.config, "onchain_earn_purchase");
3727
4122
  const args = asRecord(rawArgs);
3728
4123
  const response = await context.client.privatePost(
3729
4124
  "/api/v5/finance/staking-defi/purchase",
@@ -3744,7 +4139,7 @@ function registerOnchainEarnTools() {
3744
4139
  {
3745
4140
  name: "onchain_earn_redeem",
3746
4141
  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.",
4142
+ description: "Redeem a staking/DeFi investment. [CAUTION] Some products have lock periods, early redemption may incur penalties.",
3748
4143
  isWrite: true,
3749
4144
  inputSchema: {
3750
4145
  type: "object",
@@ -3765,7 +4160,6 @@ function registerOnchainEarnTools() {
3765
4160
  required: ["ordId", "protocolType"]
3766
4161
  },
3767
4162
  handler: async (rawArgs, context) => {
3768
- assertNotDemo(context.config, "onchain_earn_redeem");
3769
4163
  const args = asRecord(rawArgs);
3770
4164
  const response = await context.client.privatePost(
3771
4165
  "/api/v5/finance/staking-defi/redeem",
@@ -3785,7 +4179,7 @@ function registerOnchainEarnTools() {
3785
4179
  {
3786
4180
  name: "onchain_earn_cancel",
3787
4181
  module: "earn.onchain",
3788
- description: "Cancel a pending staking/DeFi purchase order. [CAUTION] Not available in demo mode.",
4182
+ description: "Cancel a pending staking/DeFi purchase order. [CAUTION]",
3789
4183
  isWrite: true,
3790
4184
  inputSchema: {
3791
4185
  type: "object",
@@ -3802,7 +4196,6 @@ function registerOnchainEarnTools() {
3802
4196
  required: ["ordId", "protocolType"]
3803
4197
  },
3804
4198
  handler: async (rawArgs, context) => {
3805
- assertNotDemo(context.config, "onchain_earn_cancel");
3806
4199
  const args = asRecord(rawArgs);
3807
4200
  const response = await context.client.privatePost(
3808
4201
  "/api/v5/finance/staking-defi/cancel",
@@ -4095,7 +4488,6 @@ function registerDcdTools() {
4095
4488
  required: ["productId", "notionalSz", "notionalCcy"]
4096
4489
  },
4097
4490
  handler: async (rawArgs, context) => {
4098
- assertNotDemo(context.config, "dcd_subscribe");
4099
4491
  const args = asRecord(rawArgs);
4100
4492
  const productId = requireString(args, "productId");
4101
4493
  const notionalSz = requireString(args, "notionalSz");
@@ -4267,13 +4659,30 @@ function registerAutoEarnTools() {
4267
4659
  }
4268
4660
  ];
4269
4661
  }
4662
+ var EARN_DEMO_MESSAGE = "Earn features (savings, DCD, on-chain staking, auto-earn) are not available in simulated trading mode.";
4663
+ var EARN_DEMO_SUGGESTION = "Switch to a live account to use Earn features.";
4664
+ var DEMO_GUARD_SKIP = /* @__PURE__ */ new Set(["dcd_redeem"]);
4665
+ function withDemoGuard(tool) {
4666
+ if (!tool.isWrite || DEMO_GUARD_SKIP.has(tool.name)) return tool;
4667
+ const originalHandler = tool.handler;
4668
+ return {
4669
+ ...tool,
4670
+ handler: async (args, context) => {
4671
+ if (context.config.demo) {
4672
+ throw new ConfigError(EARN_DEMO_MESSAGE, EARN_DEMO_SUGGESTION);
4673
+ }
4674
+ return originalHandler(args, context);
4675
+ }
4676
+ };
4677
+ }
4270
4678
  function registerAllEarnTools() {
4271
- return [
4679
+ const tools = [
4272
4680
  ...registerEarnTools(),
4273
4681
  ...registerOnchainEarnTools(),
4274
4682
  ...registerDcdTools(),
4275
4683
  ...registerAutoEarnTools()
4276
4684
  ];
4685
+ return tools.map(withDemoGuard);
4277
4686
  }
4278
4687
  function buildContractTradeTools(cfg) {
4279
4688
  const { prefix, module, label, instTypes, instIdExample } = cfg;
@@ -5297,7 +5706,7 @@ function registerMarketTools() {
5297
5706
  {
5298
5707
  name: "market_get_stock_tokens",
5299
5708
  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.",
5709
+ 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
5710
  isWrite: false,
5302
5711
  inputSchema: {
5303
5712
  type: "object",
@@ -5327,6 +5736,46 @@ function registerMarketTools() {
5327
5736
  const filtered = Array.isArray(data) ? data.filter((item) => item.instCategory === "3") : data;
5328
5737
  return normalizeResponse({ ...response, data: filtered });
5329
5738
  }
5739
+ },
5740
+ {
5741
+ name: "market_get_instruments_by_category",
5742
+ module: "market",
5743
+ 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.",
5744
+ isWrite: false,
5745
+ inputSchema: {
5746
+ type: "object",
5747
+ properties: {
5748
+ instCategory: {
5749
+ type: "string",
5750
+ enum: ["3", "4", "5", "6", "7"],
5751
+ description: "Asset category: 3=Stock tokens, 4=Metals, 5=Commodities, 6=Forex, 7=Bonds"
5752
+ },
5753
+ instType: {
5754
+ type: "string",
5755
+ enum: ["SPOT", "SWAP"],
5756
+ description: "Instrument type. Default: SWAP"
5757
+ },
5758
+ instId: {
5759
+ type: "string",
5760
+ description: "Optional: filter by specific instrument ID"
5761
+ }
5762
+ },
5763
+ required: ["instCategory"]
5764
+ },
5765
+ handler: async (rawArgs, context) => {
5766
+ const args = asRecord(rawArgs);
5767
+ const instCategory = requireString(args, "instCategory");
5768
+ const instType = readString(args, "instType") ?? "SWAP";
5769
+ const instId = readString(args, "instId");
5770
+ const response = await context.client.publicGet(
5771
+ "/api/v5/public/instruments",
5772
+ compactObject({ instType, instId }),
5773
+ publicRateLimit("market_get_instruments_by_category", 20)
5774
+ );
5775
+ const data = response.data;
5776
+ const filtered = Array.isArray(data) ? data.filter((item) => item.instCategory === instCategory) : data;
5777
+ return normalizeResponse({ ...response, data: filtered });
5778
+ }
5330
5779
  }
5331
5780
  ];
5332
5781
  }
@@ -7104,7 +7553,8 @@ function allToolSpecs() {
7104
7553
  ...registerNewsTools(),
7105
7554
  ...registerBotTools(),
7106
7555
  ...registerAllEarnTools(),
7107
- ...registerAuditTools()
7556
+ ...registerAuditTools(),
7557
+ ...registerSkillsTools()
7108
7558
  ];
7109
7559
  }
7110
7560
  function createToolRunner(client, config) {
@@ -7119,12 +7569,12 @@ function createToolRunner(client, config) {
7119
7569
  };
7120
7570
  }
7121
7571
  function configFilePath() {
7122
- return join(homedir(), ".okx", "config.toml");
7572
+ return join4(homedir2(), ".okx", "config.toml");
7123
7573
  }
7124
7574
  function readFullConfig() {
7125
7575
  const path42 = configFilePath();
7126
- if (!existsSync(path42)) return { profiles: {} };
7127
- const raw = readFileSync(path42, "utf-8");
7576
+ if (!existsSync3(path42)) return { profiles: {} };
7577
+ const raw = readFileSync3(path42, "utf-8");
7128
7578
  try {
7129
7579
  return parse(raw);
7130
7580
  } catch (err) {
@@ -7152,11 +7602,11 @@ var CONFIG_HEADER = `# OKX Trade Kit Configuration
7152
7602
  `;
7153
7603
  function writeFullConfig(config) {
7154
7604
  const path42 = configFilePath();
7155
- const dir = dirname(path42);
7156
- if (!existsSync(dir)) {
7157
- mkdirSync(dir, { recursive: true });
7605
+ const dir = dirname3(path42);
7606
+ if (!existsSync3(dir)) {
7607
+ mkdirSync4(dir, { recursive: true });
7158
7608
  }
7159
- writeFileSync(path42, CONFIG_HEADER + stringify(config), "utf-8");
7609
+ writeFileSync3(path42, CONFIG_HEADER + stringify(config), "utf-8");
7160
7610
  }
7161
7611
  function expandShorthand(moduleId) {
7162
7612
  if (moduleId === "all") return [...MODULES];
@@ -7270,12 +7720,12 @@ function loadConfig(cli) {
7270
7720
  verbose: cli.verbose ?? false
7271
7721
  };
7272
7722
  }
7273
- var CACHE_FILE = join2(homedir2(), ".okx", "update-check.json");
7723
+ var CACHE_FILE = join5(homedir3(), ".okx", "update-check.json");
7274
7724
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
7275
7725
  function readCache() {
7276
7726
  try {
7277
- if (existsSync2(CACHE_FILE)) {
7278
- return JSON.parse(readFileSync2(CACHE_FILE, "utf-8"));
7727
+ if (existsSync4(CACHE_FILE)) {
7728
+ return JSON.parse(readFileSync4(CACHE_FILE, "utf-8"));
7279
7729
  }
7280
7730
  } catch {
7281
7731
  }
@@ -7283,8 +7733,8 @@ function readCache() {
7283
7733
  }
7284
7734
  function writeCache(cache) {
7285
7735
  try {
7286
- mkdirSync2(join2(homedir2(), ".okx"), { recursive: true });
7287
- writeFileSync2(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7736
+ mkdirSync5(join5(homedir3(), ".okx"), { recursive: true });
7737
+ writeFileSync4(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7288
7738
  } catch {
7289
7739
  }
7290
7740
  }
@@ -7591,26 +8041,26 @@ var Report = class {
7591
8041
  this.lines.push({ key, value });
7592
8042
  }
7593
8043
  print() {
7594
- const sep = "\u2500".repeat(52);
8044
+ const sep2 = "\u2500".repeat(52);
7595
8045
  outputLine("");
7596
- outputLine(` \u2500\u2500 Diagnostic Report (copy & share) ${sep.slice(35)}`);
8046
+ outputLine(` \u2500\u2500 Diagnostic Report (copy & share) ${sep2.slice(35)}`);
7597
8047
  for (const { key, value } of this.lines) {
7598
8048
  outputLine(` ${key.padEnd(14)} ${value}`);
7599
8049
  }
7600
- outputLine(` ${sep}`);
8050
+ outputLine(` ${sep2}`);
7601
8051
  outputLine("");
7602
8052
  }
7603
8053
  /** Write report to a file path, returns true on success. */
7604
8054
  writeToFile(filePath) {
7605
8055
  try {
7606
- const sep = "-".repeat(52);
8056
+ const sep2 = "-".repeat(52);
7607
8057
  const lines = [
7608
- `-- Diagnostic Report (copy & share) ${sep.slice(35)}`
8058
+ `-- Diagnostic Report (copy & share) ${sep2.slice(35)}`
7609
8059
  ];
7610
8060
  for (const { key, value } of this.lines) {
7611
8061
  lines.push(`${key.padEnd(14)} ${value}`);
7612
8062
  }
7613
- lines.push(sep, "");
8063
+ lines.push(sep2, "");
7614
8064
  fs2.writeFileSync(filePath, lines.join("\n"), "utf8");
7615
8065
  return true;
7616
8066
  } catch (_e) {
@@ -7986,13 +8436,13 @@ async function checkStdioHandshake(entryPath, report) {
7986
8436
  clientInfo: { name: "okx-diagnose", version: "1.0" }
7987
8437
  }
7988
8438
  });
7989
- return new Promise((resolve) => {
8439
+ return new Promise((resolve3) => {
7990
8440
  let settled = false;
7991
8441
  const settle = (passed) => {
7992
8442
  if (settled) return;
7993
8443
  settled = true;
7994
8444
  clearTimeout(timer);
7995
- resolve(passed);
8445
+ resolve3(passed);
7996
8446
  };
7997
8447
  const child = spawn(process.execPath, [entryPath], {
7998
8448
  stdio: ["pipe", "pipe", "pipe"],
@@ -8113,7 +8563,7 @@ async function cmdDiagnoseMcp(options = {}) {
8113
8563
 
8114
8564
  // src/commands/diagnose.ts
8115
8565
  var CLI_VERSION = readCliVersion();
8116
- var GIT_HASH = true ? "7b97fe6" : "dev";
8566
+ var GIT_HASH = true ? "f0a9e50" : "dev";
8117
8567
  function maskKey2(key) {
8118
8568
  if (!key) return "(not set)";
8119
8569
  if (key.length <= 8) return "****";
@@ -8130,7 +8580,7 @@ async function checkDns(hostname) {
8130
8580
  }
8131
8581
  async function checkSocket(createFn, successEvent, timeoutMs) {
8132
8582
  const t0 = Date.now();
8133
- return new Promise((resolve) => {
8583
+ return new Promise((resolve3) => {
8134
8584
  const socket = createFn();
8135
8585
  const cleanup = () => {
8136
8586
  socket.removeAllListeners();
@@ -8138,15 +8588,15 @@ async function checkSocket(createFn, successEvent, timeoutMs) {
8138
8588
  };
8139
8589
  socket.once(successEvent, () => {
8140
8590
  cleanup();
8141
- resolve({ ok: true, ms: Date.now() - t0 });
8591
+ resolve3({ ok: true, ms: Date.now() - t0 });
8142
8592
  });
8143
8593
  socket.once("timeout", () => {
8144
8594
  cleanup();
8145
- resolve({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
8595
+ resolve3({ ok: false, ms: Date.now() - t0, error: `timed out after ${timeoutMs}ms` });
8146
8596
  });
8147
8597
  socket.once("error", (err) => {
8148
8598
  cleanup();
8149
- resolve({ ok: false, ms: Date.now() - t0, error: err.message });
8599
+ resolve3({ ok: false, ms: Date.now() - t0, error: err.message });
8150
8600
  });
8151
8601
  });
8152
8602
  }
@@ -8410,24 +8860,24 @@ async function runCliChecks(config, profile, outputPath) {
8410
8860
 
8411
8861
  // src/commands/upgrade.ts
8412
8862
  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";
8863
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
8864
+ import { dirname as dirname5, join as join7 } from "path";
8865
+ import { homedir as homedir5 } from "os";
8416
8866
  var PACKAGES = ["@okx_ai/okx-trade-mcp", "@okx_ai/okx-trade-cli"];
8417
- var CACHE_FILE2 = join4(homedir4(), ".okx", "last_check");
8867
+ var CACHE_FILE2 = join7(homedir5(), ".okx", "last_check");
8418
8868
  var THROTTLE_MS = 12 * 60 * 60 * 1e3;
8419
- var NPM_BIN = join4(dirname3(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
8869
+ var NPM_BIN = join7(dirname5(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
8420
8870
  function readLastCheck() {
8421
8871
  try {
8422
- return parseInt(readFileSync4(CACHE_FILE2, "utf-8").trim(), 10) || 0;
8872
+ return parseInt(readFileSync6(CACHE_FILE2, "utf-8").trim(), 10) || 0;
8423
8873
  } catch {
8424
8874
  return 0;
8425
8875
  }
8426
8876
  }
8427
8877
  function writeLastCheck() {
8428
8878
  try {
8429
- mkdirSync4(join4(homedir4(), ".okx"), { recursive: true });
8430
- writeFileSync4(CACHE_FILE2, String(Math.floor(Date.now() / 1e3)), "utf-8");
8879
+ mkdirSync7(join7(homedir5(), ".okx"), { recursive: true });
8880
+ writeFileSync6(CACHE_FILE2, String(Math.floor(Date.now() / 1e3)), "utf-8");
8431
8881
  } catch {
8432
8882
  }
8433
8883
  }
@@ -8632,7 +9082,12 @@ async function cmdNewsDetail(run, id, opts) {
8632
9082
  return;
8633
9083
  }
8634
9084
  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;
9085
+ let content;
9086
+ if (rawContent && rawContent.length > 500) {
9087
+ content = rawContent.slice(0, 500) + "... (use --json for full text)";
9088
+ } else {
9089
+ content = rawContent;
9090
+ }
8636
9091
  printKv({
8637
9092
  id: article["id"],
8638
9093
  title: article["title"],
@@ -8820,7 +9275,11 @@ var HELP_TREE = {
8820
9275
  },
8821
9276
  "stock-tokens": {
8822
9277
  usage: "okx market stock-tokens [--instType <SPOT|SWAP>] [--instId <id>]",
8823
- description: "List all stock token instruments (instCategory=3, e.g. AAPL-USDT-SWAP)"
9278
+ description: "[Deprecated: use instruments-by-category --instCategory 3] List all stock token instruments (instCategory=3, e.g. AAPL-USDT-SWAP)"
9279
+ },
9280
+ "instruments-by-category": {
9281
+ usage: "okx market instruments-by-category --instCategory <4|5|6|7> [--instType <SPOT|SWAP>] [--instId <id>]",
9282
+ description: "List instruments by asset category: 4=Metals (gold/silver), 5=Commodities (oil/gas), 6=Forex (EUR/USD), 7=Bonds"
8824
9283
  }
8825
9284
  }
8826
9285
  },
@@ -9577,6 +10036,7 @@ var CLI_OPTIONS = {
9577
10036
  live: { type: "boolean", default: false },
9578
10037
  // market extras
9579
10038
  instType: { type: "string" },
10039
+ instCategory: { type: "string" },
9580
10040
  quoteCcy: { type: "string" },
9581
10041
  // account extras
9582
10042
  archive: { type: "boolean", default: false },
@@ -9666,6 +10126,10 @@ var CLI_OPTIONS = {
9666
10126
  period: { type: "string" },
9667
10127
  points: { type: "string" },
9668
10128
  "sort-by": { type: "string" },
10129
+ // skill marketplace
10130
+ categories: { type: "string" },
10131
+ dir: { type: "string" },
10132
+ page: { type: "string" },
9669
10133
  // diagnostics — cli/mcp/all/output are diagnose-specific; verbose is shared
9670
10134
  verbose: { type: "boolean", default: false },
9671
10135
  mcp: { type: "boolean", default: false },
@@ -9949,6 +10413,37 @@ async function cmdMarketIndicator(run, indicator, instId, opts) {
9949
10413
  }
9950
10414
  }
9951
10415
  }
10416
+ async function cmdMarketInstrumentsByCategory(run, opts) {
10417
+ const result = await run("market_get_instruments_by_category", {
10418
+ instCategory: opts.instCategory,
10419
+ instType: opts.instType,
10420
+ instId: opts.instId
10421
+ });
10422
+ const items = getData2(result);
10423
+ if (opts.json) return printJson(items);
10424
+ const CATEGORY_LABELS = {
10425
+ "3": "Stock tokens",
10426
+ "4": "Metals",
10427
+ "5": "Commodities",
10428
+ "6": "Forex",
10429
+ "7": "Bonds"
10430
+ };
10431
+ const label = CATEGORY_LABELS[opts.instCategory] ?? opts.instCategory;
10432
+ process.stdout.write(`instCategory=${opts.instCategory} (${label}) \u2014 ${items?.length ?? 0} instruments
10433
+
10434
+ `);
10435
+ printTable(
10436
+ (items ?? []).slice(0, 50).map((t) => ({
10437
+ instId: t["instId"],
10438
+ instCategory: t["instCategory"],
10439
+ ctVal: t["ctVal"],
10440
+ lotSz: t["lotSz"],
10441
+ minSz: t["minSz"],
10442
+ tickSz: t["tickSz"],
10443
+ state: t["state"]
10444
+ }))
10445
+ );
10446
+ }
9952
10447
  async function cmdMarketStockTokens(run, opts) {
9953
10448
  const result = await run("market_get_stock_tokens", { instType: opts.instType, instId: opts.instId });
9954
10449
  const items = getData2(result);
@@ -11384,7 +11879,7 @@ Config saved to ${p}
11384
11879
  }
11385
11880
  };
11386
11881
  function prompt(rl, question) {
11387
- return new Promise((resolve) => rl.question(question, resolve));
11882
+ return new Promise((resolve3) => rl.question(question, resolve3));
11388
11883
  }
11389
11884
  function cmdConfigShow(json) {
11390
11885
  const config = readFullConfig();
@@ -12349,10 +12844,194 @@ async function cmdDcdQuoteAndBuy(run, opts) {
12349
12844
  }
12350
12845
  }
12351
12846
 
12847
+ // src/commands/skill.ts
12848
+ import { tmpdir, homedir as homedir7 } from "os";
12849
+ import { join as join9, dirname as dirname6 } from "path";
12850
+ import { mkdirSync as mkdirSync8, rmSync, existsSync as existsSync7, copyFileSync as copyFileSync2 } from "fs";
12851
+ import { execFileSync as execFileSync2 } from "child_process";
12852
+ import { randomUUID as randomUUID2 } from "crypto";
12853
+ function resolveNpx() {
12854
+ const sibling = join9(dirname6(process.execPath), "npx");
12855
+ if (existsSync7(sibling)) return sibling;
12856
+ return "npx";
12857
+ }
12858
+ async function cmdSkillSearch(run, opts) {
12859
+ const args = {};
12860
+ if (opts.keyword) args.keyword = opts.keyword;
12861
+ if (opts.categories) args.categories = opts.categories;
12862
+ if (opts.page) args.page = opts.page;
12863
+ if (opts.limit) args.limit = opts.limit;
12864
+ const result = await run("skills_search", args);
12865
+ const data = result.data;
12866
+ const totalPage = result.totalPage;
12867
+ if (opts.json) {
12868
+ outputLine(JSON.stringify(result, null, 2));
12869
+ return;
12870
+ }
12871
+ if (!Array.isArray(data) || data.length === 0) {
12872
+ outputLine("No skills found.");
12873
+ return;
12874
+ }
12875
+ outputLine("");
12876
+ outputLine(" NAME VERSION DESCRIPTION");
12877
+ for (const item of data) {
12878
+ const name = (item.name ?? "").padEnd(20);
12879
+ const ver = (item.latestVersion ?? "").padEnd(10);
12880
+ const desc = (item.description ?? "").slice(0, 50);
12881
+ outputLine(` ${name}${ver}${desc}`);
12882
+ }
12883
+ outputLine("");
12884
+ const page = opts.page ?? "1";
12885
+ const pageInfo = totalPage ? ` (page ${page}/${totalPage})` : "";
12886
+ outputLine(`${data.length} skills found${pageInfo}. Use \`okx skill add <name>\` to install.`);
12887
+ }
12888
+ async function cmdSkillCategories(run, json) {
12889
+ const result = await run("skills_get_categories", {});
12890
+ const data = result.data;
12891
+ if (json) {
12892
+ outputLine(JSON.stringify(result, null, 2));
12893
+ return;
12894
+ }
12895
+ if (!Array.isArray(data) || data.length === 0) {
12896
+ outputLine("No categories found.");
12897
+ return;
12898
+ }
12899
+ outputLine("");
12900
+ outputLine(" ID NAME");
12901
+ for (const cat of data) {
12902
+ outputLine(` ${(cat.categoryId ?? "").padEnd(20)}${cat.name ?? ""}`);
12903
+ }
12904
+ outputLine("");
12905
+ }
12906
+ async function cmdSkillAdd(name, config, json) {
12907
+ const tmpBase = join9(tmpdir(), `okx-skill-${randomUUID2()}`);
12908
+ mkdirSync8(tmpBase, { recursive: true });
12909
+ try {
12910
+ outputLine(`Downloading ${name}...`);
12911
+ const client = new OkxRestClient(config);
12912
+ const zipPath = await downloadSkillZip(client, name, tmpBase);
12913
+ const contentDir = await extractSkillZip(zipPath, join9(tmpBase, "content"));
12914
+ const meta = readMetaJson(contentDir);
12915
+ validateSkillMdExists(contentDir);
12916
+ outputLine("Installing to detected agents...");
12917
+ try {
12918
+ execFileSync2(resolveNpx(), ["skills", "add", contentDir, "-y", "-g"], {
12919
+ stdio: "inherit",
12920
+ timeout: 6e4
12921
+ });
12922
+ } catch (e) {
12923
+ const savedZip = join9(process.cwd(), `${name}.zip`);
12924
+ try {
12925
+ copyFileSync2(zipPath, savedZip);
12926
+ } catch {
12927
+ }
12928
+ errorLine(`npx skills add failed. The zip has been downloaded but not installed.`);
12929
+ errorLine(`You can manually install from: ${savedZip}`);
12930
+ throw e;
12931
+ }
12932
+ upsertSkillRecord(meta);
12933
+ if (json) {
12934
+ outputLine(JSON.stringify({ name: meta.name, version: meta.version, status: "installed" }, null, 2));
12935
+ } else {
12936
+ outputLine(`\u2713 Skill "${meta.name}" v${meta.version} installed`);
12937
+ }
12938
+ } finally {
12939
+ rmSync(tmpBase, { recursive: true, force: true });
12940
+ }
12941
+ }
12942
+ async function cmdSkillDownload(name, targetDir, config, json) {
12943
+ outputLine(`Downloading ${name}...`);
12944
+ const client = new OkxRestClient(config);
12945
+ const filePath = await downloadSkillZip(client, name, targetDir);
12946
+ if (json) {
12947
+ outputLine(JSON.stringify({ name, filePath }, null, 2));
12948
+ } else {
12949
+ outputLine(`\u2713 Downloaded ${name}.zip`);
12950
+ outputLine(` Path: ${filePath}`);
12951
+ }
12952
+ }
12953
+ function cmdSkillRemove(name, json) {
12954
+ const removed = removeSkillRecord(name);
12955
+ if (!removed) {
12956
+ errorLine(`Skill "${name}" is not installed.`);
12957
+ process.exitCode = 1;
12958
+ return;
12959
+ }
12960
+ try {
12961
+ execFileSync2(resolveNpx(), ["skills", "remove", name, "-y", "-g"], {
12962
+ stdio: "inherit",
12963
+ timeout: 6e4
12964
+ });
12965
+ } catch {
12966
+ const agentsPath = join9(homedir7(), ".agents", "skills", name);
12967
+ try {
12968
+ rmSync(agentsPath, { recursive: true, force: true });
12969
+ } catch {
12970
+ }
12971
+ }
12972
+ if (json) {
12973
+ outputLine(JSON.stringify({ name, status: "removed" }, null, 2));
12974
+ } else {
12975
+ outputLine(`\u2713 Skill "${name}" removed`);
12976
+ }
12977
+ }
12978
+ async function cmdSkillCheck(run, name, json) {
12979
+ const local = getSkillRecord(name);
12980
+ if (!local) {
12981
+ errorLine(`Skill "${name}" is not installed.`);
12982
+ process.exitCode = 1;
12983
+ return;
12984
+ }
12985
+ const result = await run("skills_search", { keyword: name });
12986
+ const data = result.data;
12987
+ const remote = data?.find((s) => s.name === name);
12988
+ if (!remote) {
12989
+ errorLine(`Skill "${name}" not found in marketplace.`);
12990
+ process.exitCode = 1;
12991
+ return;
12992
+ }
12993
+ const upToDate = local.version === remote.latestVersion;
12994
+ if (json) {
12995
+ outputLine(JSON.stringify({
12996
+ name,
12997
+ installedVersion: local.version,
12998
+ latestVersion: remote.latestVersion,
12999
+ upToDate
13000
+ }, null, 2));
13001
+ } else if (upToDate) {
13002
+ outputLine(`${name}: installed v${local.version} \u2192 latest v${remote.latestVersion} (up to date)`);
13003
+ } else {
13004
+ outputLine(`${name}: installed v${local.version} \u2192 latest v${remote.latestVersion} (update available)`);
13005
+ outputLine(` Use \`okx skill add ${name}\` to update.`);
13006
+ }
13007
+ }
13008
+ function cmdSkillList(json) {
13009
+ const registry = readRegistry();
13010
+ const skills = Object.values(registry.skills);
13011
+ if (json) {
13012
+ outputLine(JSON.stringify(registry, null, 2));
13013
+ return;
13014
+ }
13015
+ if (skills.length === 0) {
13016
+ outputLine("No skills installed.");
13017
+ return;
13018
+ }
13019
+ outputLine("");
13020
+ outputLine(" NAME VERSION INSTALLED AT");
13021
+ for (const s of skills) {
13022
+ const name = s.name.padEnd(20);
13023
+ const ver = s.version.padEnd(10);
13024
+ const date = s.installedAt.slice(0, 19).replace("T", " ");
13025
+ outputLine(` ${name}${ver}${date}`);
13026
+ }
13027
+ outputLine("");
13028
+ outputLine(`${skills.length} skills installed.`);
13029
+ }
13030
+
12352
13031
  // src/index.ts
12353
13032
  var _require3 = createRequire3(import.meta.url);
12354
13033
  var CLI_VERSION2 = _require3("../package.json").version;
12355
- var GIT_HASH2 = true ? "7b97fe6" : "dev";
13034
+ var GIT_HASH2 = true ? "f0a9e50" : "dev";
12356
13035
  function handleConfigCommand(action, rest, json, lang, force) {
12357
13036
  if (action === "init") return cmdConfigInit(lang === "zh" ? "zh" : "en");
12358
13037
  if (action === "show") return cmdConfigShow(json);
@@ -12395,6 +13074,13 @@ function handleMarketPublicCommand(run, action, rest, v, json) {
12395
13074
  return cmdMarketOpenInterest(run, { instType: v.instType, instId: v.instId, json });
12396
13075
  if (action === "stock-tokens")
12397
13076
  return cmdMarketStockTokens(run, { instType: v.instType, instId: v.instId, json });
13077
+ if (action === "instruments-by-category")
13078
+ return cmdMarketInstrumentsByCategory(run, {
13079
+ instCategory: v.instCategory,
13080
+ instType: v.instType,
13081
+ instId: v.instId,
13082
+ json
13083
+ });
12398
13084
  if (action === "indicator") {
12399
13085
  const limit = v.limit !== void 0 ? Number(v.limit) : void 0;
12400
13086
  const backtestTime = v["backtest-time"] !== void 0 ? Number(v["backtest-time"]) : void 0;
@@ -13141,6 +13827,42 @@ function handleNewsCommand(run, action, rest, v, json) {
13141
13827
  `);
13142
13828
  process.exitCode = 1;
13143
13829
  }
13830
+ function requireSkillName(rest, usage) {
13831
+ const name = rest[0];
13832
+ if (!name) {
13833
+ errorLine(usage);
13834
+ process.exitCode = 1;
13835
+ }
13836
+ return name;
13837
+ }
13838
+ function handleSkillAdd(rest, config, json) {
13839
+ const n = requireSkillName(rest, "Usage: okx skill add <name>");
13840
+ if (n) return cmdSkillAdd(n, config, json);
13841
+ }
13842
+ function handleSkillDownload(rest, v, config, json) {
13843
+ const n = requireSkillName(rest, "Usage: okx skill download <name> [--dir <path>]");
13844
+ if (n) return cmdSkillDownload(n, v.dir ?? process.cwd(), config, json);
13845
+ }
13846
+ function handleSkillRemove(rest, json) {
13847
+ const n = requireSkillName(rest, "Usage: okx skill remove <name>");
13848
+ if (n) cmdSkillRemove(n, json);
13849
+ }
13850
+ function handleSkillCheck(run, rest, json) {
13851
+ const n = requireSkillName(rest, "Usage: okx skill check <name>");
13852
+ if (n) return cmdSkillCheck(run, n, json);
13853
+ }
13854
+ function handleSkillCommand(run, action, rest, v, json, config) {
13855
+ if (action === "search") return cmdSkillSearch(run, { keyword: rest[0] ?? v.keyword, categories: v.categories, page: v.page, limit: v.limit, json });
13856
+ if (action === "categories") return cmdSkillCategories(run, json);
13857
+ if (action === "list") return cmdSkillList(json);
13858
+ if (action === "add") return handleSkillAdd(rest, config, json);
13859
+ if (action === "download") return handleSkillDownload(rest, v, config, json);
13860
+ if (action === "remove") return handleSkillRemove(rest, json);
13861
+ if (action === "check") return handleSkillCheck(run, rest, json);
13862
+ errorLine(`Unknown skill command: ${action}`);
13863
+ errorLine("Valid: search, categories, add, download, remove, check, list");
13864
+ process.exitCode = 1;
13865
+ }
13144
13866
  function outputResult(result, json) {
13145
13867
  if (json) {
13146
13868
  outputLine(JSON.stringify(result, null, 2));
@@ -13203,7 +13925,8 @@ async function main() {
13203
13925
  option: () => handleOptionCommand(run, action, rest, v, json),
13204
13926
  news: () => handleNewsCommand(run, action, rest, v, json),
13205
13927
  bot: () => handleBotCommand(run, action, rest, v, json),
13206
- earn: () => handleEarnCommand(run, action, rest, v, json)
13928
+ earn: () => handleEarnCommand(run, action, rest, v, json),
13929
+ skill: () => handleSkillCommand(run, action, rest, v, json, config)
13207
13930
  };
13208
13931
  const handler = moduleHandlers[module];
13209
13932
  if (handler) return handler();
@@ -13234,6 +13957,7 @@ export {
13234
13957
  handleOptionAlgoCommand,
13235
13958
  handleOptionCommand,
13236
13959
  handleSetupCommand,
13960
+ handleSkillCommand,
13237
13961
  handleSpotAlgoCommand,
13238
13962
  handleSpotCommand,
13239
13963
  handleSwapAlgoCommand,