@aihubspot/agent-trade-mcp 0.1.1 → 0.1.3
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/README.md +6 -0
- package/dist/index.js +388 -73
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,3 +18,9 @@ ai-hub-trade-mcp setup --client codex --profile default
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Setup registers the currently installed MCP binary through its absolute Node runtime and entrypoint path, so desktop clients do not depend on a global PATH or an unpublished `npx` package. Cursor and Claude Desktop configurations are merged with existing MCP servers through their JSON configurations. Claude Code and Codex are registered through their official CLIs; Claude Code uses user scope. Existing JSON configurations are validated, atomically updated, and backed up before their first modification. The setup command stores no API credentials; the MCP server continues to read its profile from `~/.ai-hub/config.toml`.
|
|
21
|
+
|
|
22
|
+
## Spot market-order units
|
|
23
|
+
|
|
24
|
+
Use `spot_prepare_market_buy` or `margin_prepare_market_buy` only with `quoteAmount` (for example, the exact USDT amount to spend for `ETHUSDT`). Use `spot_prepare_market_sell` or `margin_prepare_market_sell` only with `baseQuantity` (the exact ETH amount to sell for `ETHUSDT`). A market buy cannot guarantee an exact base-asset quantity; it must never reinterpret a requested base quantity as a quote amount.
|
|
25
|
+
|
|
26
|
+
Before any order preview, MCP lazily loads `/sapi/v2/symbols` once per local profile and caches the symbol rules in memory for five minutes. Known quantity/price precision and limit-order minimum violations are rejected before confirmation.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
4
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
7
|
|
|
6
8
|
// ../core/src/errors.ts
|
|
@@ -990,14 +992,186 @@ var marketTools = [
|
|
|
990
992
|
}
|
|
991
993
|
];
|
|
992
994
|
|
|
995
|
+
// ../core/src/tools/symbol-rules.ts
|
|
996
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
997
|
+
var cache = /* @__PURE__ */ new Map();
|
|
998
|
+
var pendingLoads = /* @__PURE__ */ new Map();
|
|
999
|
+
function cacheKey(context) {
|
|
1000
|
+
return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
|
|
1001
|
+
}
|
|
1002
|
+
function normalizedSymbol(symbol) {
|
|
1003
|
+
return symbol.replaceAll("/", "").trim().toUpperCase();
|
|
1004
|
+
}
|
|
1005
|
+
function stringValue(value) {
|
|
1006
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1007
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1008
|
+
return void 0;
|
|
1009
|
+
}
|
|
1010
|
+
function nonNegativeInteger(value) {
|
|
1011
|
+
const result2 = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
1012
|
+
return Number.isInteger(result2) && result2 >= 0 ? result2 : void 0;
|
|
1013
|
+
}
|
|
1014
|
+
function rows(response) {
|
|
1015
|
+
if (Array.isArray(response)) return response.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
1016
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return [];
|
|
1017
|
+
const root = response;
|
|
1018
|
+
const candidates = [root.symbols, root.data, root.data?.symbols];
|
|
1019
|
+
for (const candidate of candidates) {
|
|
1020
|
+
if (Array.isArray(candidate)) return candidate.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
1021
|
+
}
|
|
1022
|
+
return [];
|
|
1023
|
+
}
|
|
1024
|
+
function parseRules(response) {
|
|
1025
|
+
const result2 = /* @__PURE__ */ new Map();
|
|
1026
|
+
for (const row of rows(response)) {
|
|
1027
|
+
const symbol = stringValue(row.symbol);
|
|
1028
|
+
if (!symbol) continue;
|
|
1029
|
+
result2.set(normalizedSymbol(symbol), {
|
|
1030
|
+
symbol,
|
|
1031
|
+
quantityPrecision: nonNegativeInteger(row.quantityPrecision),
|
|
1032
|
+
pricePrecision: nonNegativeInteger(row.pricePrecision),
|
|
1033
|
+
limitVolumeMin: stringValue(row.limitVolumeMin),
|
|
1034
|
+
limitAmountMin: stringValue(row.limitAmountMin),
|
|
1035
|
+
limitPriceMin: stringValue(row.limitPriceMin)
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
return result2;
|
|
1039
|
+
}
|
|
1040
|
+
async function loadRules(context) {
|
|
1041
|
+
const key = cacheKey(context);
|
|
1042
|
+
const current = cache.get(key);
|
|
1043
|
+
if (current && current.expiresAt > Date.now()) return current;
|
|
1044
|
+
const pending = pendingLoads.get(key);
|
|
1045
|
+
if (pending) return pending;
|
|
1046
|
+
const load = context.api.symbols().then((response) => {
|
|
1047
|
+
const entry = { rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
|
|
1048
|
+
cache.set(key, entry);
|
|
1049
|
+
return entry;
|
|
1050
|
+
}).finally(() => pendingLoads.delete(key));
|
|
1051
|
+
pendingLoads.set(key, load);
|
|
1052
|
+
return load;
|
|
1053
|
+
}
|
|
1054
|
+
async function getSymbolRule(context, symbol) {
|
|
1055
|
+
const entry = await loadRules(context);
|
|
1056
|
+
const rule = entry.rules.get(normalizedSymbol(symbol));
|
|
1057
|
+
if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1058
|
+
return rule;
|
|
1059
|
+
}
|
|
1060
|
+
function decimalScale(value) {
|
|
1061
|
+
return value.split(".")[1]?.length ?? 0;
|
|
1062
|
+
}
|
|
1063
|
+
function decimalParts(value) {
|
|
1064
|
+
const [whole, fraction = ""] = value.split(".");
|
|
1065
|
+
return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
|
|
1066
|
+
}
|
|
1067
|
+
function compareDecimal(left, right) {
|
|
1068
|
+
const a = decimalParts(left);
|
|
1069
|
+
const b = decimalParts(right);
|
|
1070
|
+
const scale = Math.max(a.scale, b.scale);
|
|
1071
|
+
const leftValue = a.digits * 10n ** BigInt(scale - a.scale);
|
|
1072
|
+
const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
|
|
1073
|
+
return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
|
|
1074
|
+
}
|
|
1075
|
+
function multipliedDecimal(left, right) {
|
|
1076
|
+
const a = decimalParts(left);
|
|
1077
|
+
const b = decimalParts(right);
|
|
1078
|
+
const scale = a.scale + b.scale;
|
|
1079
|
+
const digits = (a.digits * b.digits).toString();
|
|
1080
|
+
if (scale === 0) return digits;
|
|
1081
|
+
const padded = digits.padStart(scale + 1, "0");
|
|
1082
|
+
return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
|
|
1083
|
+
}
|
|
1084
|
+
function assertPrecision(name, value, precision, symbol) {
|
|
1085
|
+
if (precision !== void 0 && decimalScale(value) > precision) {
|
|
1086
|
+
throw new AiHubError("AI_HUB_SYMBOL_PRECISION_INVALID", `${name} for ${symbol} allows at most ${precision} decimal places; received ${value}.`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function assertAtLeast(name, value, minimum, symbol) {
|
|
1090
|
+
if (minimum && compareDecimal(value, minimum) < 0) {
|
|
1091
|
+
throw new AiHubError("AI_HUB_SYMBOL_MINIMUM_NOT_MET", `${name} for ${symbol} must be at least ${minimum}; received ${value}.`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function preflightSymbolOrder(context, order) {
|
|
1095
|
+
const rule = await getSymbolRule(context, order.symbol);
|
|
1096
|
+
if (order.type === "MARKET" && order.side === "BUY" && order.quoteAmount) {
|
|
1097
|
+
assertPrecision("quoteAmount", order.quoteAmount, rule.pricePrecision, rule.symbol);
|
|
1098
|
+
}
|
|
1099
|
+
if (order.baseQuantity) assertPrecision("baseQuantity", order.baseQuantity, rule.quantityPrecision, rule.symbol);
|
|
1100
|
+
if (order.price) assertPrecision("price", order.price, rule.pricePrecision, rule.symbol);
|
|
1101
|
+
if (order.type === "LIMIT" && order.baseQuantity && order.price) {
|
|
1102
|
+
assertAtLeast("baseQuantity", order.baseQuantity, rule.limitVolumeMin, rule.symbol);
|
|
1103
|
+
assertAtLeast("price", order.price, rule.limitPriceMin, rule.symbol);
|
|
1104
|
+
if (rule.limitAmountMin && compareDecimal(rule.limitAmountMin, "0") > 0) {
|
|
1105
|
+
assertAtLeast("limit order amount", multipliedDecimal(order.baseQuantity, order.price), rule.limitAmountMin, rule.symbol);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return rule;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
993
1111
|
// ../core/src/tools/margin-tools.ts
|
|
994
|
-
function
|
|
995
|
-
const value = strictObject(input, ["symbol", "
|
|
1112
|
+
function validateMarginSemanticOrder(input) {
|
|
1113
|
+
const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "newClientOrderId", "isolated"]);
|
|
1114
|
+
const symbol = requiredString(value, "symbol");
|
|
1115
|
+
const side = requiredEnum(value, "side", ["BUY", "SELL"]);
|
|
996
1116
|
const type = requiredEnum(value, "type", ["LIMIT", "MARKET"]);
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
if (type === "MARKET" &&
|
|
1000
|
-
|
|
1117
|
+
const isolated = optionalBoolean(value, "isolated");
|
|
1118
|
+
const common = { symbol, side, type, newClientOrderId: optionalClientOrderId(value), ...isolated !== void 0 ? { isolated } : {} };
|
|
1119
|
+
if (type === "MARKET" && side === "BUY") {
|
|
1120
|
+
if (value.baseQuantity !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET BUY uses quoteAmount (the amount of quote asset to spend), not baseQuantity.");
|
|
1121
|
+
if (value.price !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
|
|
1122
|
+
const quoteAmount = positiveDecimal(value, "quoteAmount");
|
|
1123
|
+
return { ...common, volume: quoteAmount, intent: "market_buy", quoteAmount };
|
|
1124
|
+
}
|
|
1125
|
+
if (type === "MARKET" && side === "SELL") {
|
|
1126
|
+
if (value.quoteAmount !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET SELL uses baseQuantity (the amount of base asset to sell), not quoteAmount.");
|
|
1127
|
+
if (value.price !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
|
|
1128
|
+
const baseQuantity2 = positiveDecimal(value, "baseQuantity");
|
|
1129
|
+
return { ...common, volume: baseQuantity2, intent: "market_sell", baseQuantity: baseQuantity2 };
|
|
1130
|
+
}
|
|
1131
|
+
if (value.quoteAmount !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "LIMIT orders use baseQuantity (the amount of base asset), not quoteAmount.");
|
|
1132
|
+
const baseQuantity = positiveDecimal(value, "baseQuantity");
|
|
1133
|
+
const price = positiveDecimal(value, "price");
|
|
1134
|
+
return { ...common, volume: baseQuantity, price, intent: "limit", baseQuantity };
|
|
1135
|
+
}
|
|
1136
|
+
function validateMarginMarketBuy(input) {
|
|
1137
|
+
const value = strictObject(input, ["symbol", "quoteAmount", "newClientOrderId", "isolated"]);
|
|
1138
|
+
return validateMarginSemanticOrder({ ...value, side: "BUY", type: "MARKET" });
|
|
1139
|
+
}
|
|
1140
|
+
function validateMarginMarketSell(input) {
|
|
1141
|
+
const value = strictObject(input, ["symbol", "baseQuantity", "newClientOrderId", "isolated"]);
|
|
1142
|
+
return validateMarginSemanticOrder({ ...value, side: "SELL", type: "MARKET" });
|
|
1143
|
+
}
|
|
1144
|
+
function validateMarginLimitOrder(input) {
|
|
1145
|
+
const value = strictObject(input, ["symbol", "side", "baseQuantity", "price", "newClientOrderId", "isolated"]);
|
|
1146
|
+
return validateMarginSemanticOrder({ ...value, type: "LIMIT" });
|
|
1147
|
+
}
|
|
1148
|
+
function toMarginOpenApiOrder(order) {
|
|
1149
|
+
return {
|
|
1150
|
+
symbol: order.symbol,
|
|
1151
|
+
volume: order.volume,
|
|
1152
|
+
side: order.side,
|
|
1153
|
+
type: order.type,
|
|
1154
|
+
newClientOrderId: order.newClientOrderId,
|
|
1155
|
+
...order.price ? { price: order.price } : {},
|
|
1156
|
+
...order.isolated !== void 0 ? { isolated: order.isolated } : {}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function marginOrderSummary(order) {
|
|
1160
|
+
return {
|
|
1161
|
+
action: order.intent === "market_buy" ? "margin_market_buy" : order.intent === "market_sell" ? "margin_market_sell" : "margin_limit_order",
|
|
1162
|
+
symbol: order.symbol,
|
|
1163
|
+
side: order.side,
|
|
1164
|
+
type: order.type,
|
|
1165
|
+
...order.quoteAmount ? { quoteAmount: order.quoteAmount, amountMeaning: "exact quote-asset amount to spend" } : {},
|
|
1166
|
+
...order.baseQuantity ? { baseQuantity: order.baseQuantity, amountMeaning: "exact base-asset quantity" } : {},
|
|
1167
|
+
...order.price ? { price: order.price } : {},
|
|
1168
|
+
isolated: order.isolated ?? false,
|
|
1169
|
+
newClientOrderId: order.newClientOrderId
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async function preflightMarginOrder(order, context) {
|
|
1173
|
+
await preflightSymbolOrder(context, order);
|
|
1174
|
+
return order;
|
|
1001
1175
|
}
|
|
1002
1176
|
function validateMarginOrderLookup(input) {
|
|
1003
1177
|
const value = strictObject(input, ["symbol", "orderId", "newClientOrderId", "isolated"]);
|
|
@@ -1055,22 +1229,52 @@ var marginTools = [
|
|
|
1055
1229
|
handler: (input, context) => context.api.signedGet("/sapi/v2/margin/myTrades", input, signed(context))
|
|
1056
1230
|
},
|
|
1057
1231
|
{
|
|
1058
|
-
name: "
|
|
1059
|
-
title: "
|
|
1060
|
-
description: "
|
|
1061
|
-
cliPath: ["margin", "order", "
|
|
1232
|
+
name: "margin_market_buy",
|
|
1233
|
+
title: "Margin Market Buy by Quote Amount",
|
|
1234
|
+
description: "Spend an exact quote-asset amount for an isolated or cross-margin market buy. For ETHUSDT, quoteAmount is USDT. A market buy cannot guarantee an exact base-asset quantity.",
|
|
1235
|
+
cliPath: ["margin", "order", "market-buy"],
|
|
1062
1236
|
module: "spot-margin",
|
|
1063
1237
|
access: "signed",
|
|
1064
1238
|
operation: "write",
|
|
1065
1239
|
riskLevel: "high",
|
|
1066
|
-
inputSchema: { type: "object", properties: { symbol: { type: "string" },
|
|
1240
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, quoteAmount: { type: "string", description: "Required exact quote-asset amount to spend." }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "quoteAmount"], additionalProperties: false },
|
|
1067
1241
|
errorCodes: writeErrors,
|
|
1068
|
-
validate:
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1242
|
+
validate: validateMarginMarketBuy,
|
|
1243
|
+
preflight: preflightMarginOrder,
|
|
1244
|
+
handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", toMarginOpenApiOrder(input), signed(context)),
|
|
1245
|
+
writeSummary: (input) => marginOrderSummary(input)
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
name: "margin_market_sell",
|
|
1249
|
+
title: "Margin Market Sell by Base Quantity",
|
|
1250
|
+
description: "Sell an exact base-asset quantity at market for an isolated or cross-margin account. For ETHUSDT, baseQuantity is ETH.",
|
|
1251
|
+
cliPath: ["margin", "order", "market-sell"],
|
|
1252
|
+
module: "spot-margin",
|
|
1253
|
+
access: "signed",
|
|
1254
|
+
operation: "write",
|
|
1255
|
+
riskLevel: "high",
|
|
1256
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, baseQuantity: { type: "string", description: "Required exact base-asset quantity to sell." }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "baseQuantity"], additionalProperties: false },
|
|
1257
|
+
errorCodes: writeErrors,
|
|
1258
|
+
validate: validateMarginMarketSell,
|
|
1259
|
+
preflight: preflightMarginOrder,
|
|
1260
|
+
handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", toMarginOpenApiOrder(input), signed(context)),
|
|
1261
|
+
writeSummary: (input) => marginOrderSummary(input)
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
name: "margin_limit_order",
|
|
1265
|
+
title: "Place Margin Limit Order",
|
|
1266
|
+
description: "Place an isolated or cross-margin LIMIT BUY or SELL using an exact base-asset quantity and a limit price.",
|
|
1267
|
+
cliPath: ["margin", "order", "limit"],
|
|
1268
|
+
module: "spot-margin",
|
|
1269
|
+
access: "signed",
|
|
1270
|
+
operation: "write",
|
|
1271
|
+
riskLevel: "high",
|
|
1272
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, baseQuantity: { type: "string", description: "Required exact base-asset quantity." }, price: { type: "string", description: "Required limit price in the quote asset." }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "side", "baseQuantity", "price"], additionalProperties: false },
|
|
1273
|
+
errorCodes: writeErrors,
|
|
1274
|
+
validate: validateMarginLimitOrder,
|
|
1275
|
+
preflight: preflightMarginOrder,
|
|
1276
|
+
handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", toMarginOpenApiOrder(input), signed(context)),
|
|
1277
|
+
writeSummary: (input) => marginOrderSummary(input)
|
|
1074
1278
|
},
|
|
1075
1279
|
{
|
|
1076
1280
|
name: "margin_cancel_order",
|
|
@@ -1123,53 +1327,89 @@ function clientOrderId(value) {
|
|
|
1123
1327
|
if (supplied) return supplied;
|
|
1124
1328
|
return `agent_${randomUUID3().replaceAll("-", "").slice(0, 24)}`;
|
|
1125
1329
|
}
|
|
1126
|
-
function
|
|
1127
|
-
const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
|
|
1128
|
-
const type = orderType(value);
|
|
1129
|
-
const price = optionalString(value, "price");
|
|
1130
|
-
if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
|
|
1131
|
-
if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
|
|
1330
|
+
function optionalOrderFields(value) {
|
|
1132
1331
|
const timeInForce = optionalString(value, "timeInForce")?.toUpperCase();
|
|
1133
1332
|
if (timeInForce && !["GTC", "IOC", "FOK"].includes(timeInForce)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "timeInForce must be GTC, IOC, or FOK.");
|
|
1333
|
+
const recvWindow = optionalString(value, "recvWindow");
|
|
1134
1334
|
return {
|
|
1135
|
-
symbol: requiredString(value, "symbol"),
|
|
1136
|
-
volume: positiveDecimal2(value, "volume"),
|
|
1137
|
-
side: orderSide(value),
|
|
1138
|
-
type,
|
|
1139
|
-
...price ? { price } : {},
|
|
1140
|
-
...timeInForce ? { timeInForce } : {},
|
|
1141
1335
|
newClientOrderId: clientOrderId(value),
|
|
1142
|
-
...
|
|
1336
|
+
...recvWindow ? { recvWindow } : {},
|
|
1337
|
+
...timeInForce ? { timeInForce } : {}
|
|
1143
1338
|
};
|
|
1144
1339
|
}
|
|
1340
|
+
function validateSemanticOrder(input) {
|
|
1341
|
+
const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
|
|
1342
|
+
const side = orderSide(value);
|
|
1343
|
+
const type = orderType(value);
|
|
1344
|
+
const symbol = requiredString(value, "symbol");
|
|
1345
|
+
if (type === "MARKET" && side === "BUY") {
|
|
1346
|
+
if (value.baseQuantity !== void 0) {
|
|
1347
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET BUY uses quoteAmount (the amount of quote asset to spend), not baseQuantity.");
|
|
1348
|
+
}
|
|
1349
|
+
if (value.price !== void 0 || value.timeInForce !== void 0) {
|
|
1350
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price and timeInForce are not allowed for MARKET orders.");
|
|
1351
|
+
}
|
|
1352
|
+
const quoteAmount = positiveDecimal2(value, "quoteAmount");
|
|
1353
|
+
return { symbol, volume: quoteAmount, side, type, intent: "market_buy", quoteAmount, ...optionalOrderFields(value) };
|
|
1354
|
+
}
|
|
1355
|
+
if (type === "MARKET" && side === "SELL") {
|
|
1356
|
+
if (value.quoteAmount !== void 0) {
|
|
1357
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET SELL uses baseQuantity (the amount of base asset to sell), not quoteAmount.");
|
|
1358
|
+
}
|
|
1359
|
+
if (value.price !== void 0 || value.timeInForce !== void 0) {
|
|
1360
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price and timeInForce are not allowed for MARKET orders.");
|
|
1361
|
+
}
|
|
1362
|
+
const baseQuantity2 = positiveDecimal2(value, "baseQuantity");
|
|
1363
|
+
return { symbol, volume: baseQuantity2, side, type, intent: "market_sell", baseQuantity: baseQuantity2, ...optionalOrderFields(value) };
|
|
1364
|
+
}
|
|
1365
|
+
if (value.quoteAmount !== void 0) {
|
|
1366
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "LIMIT orders use baseQuantity (the amount of base asset), not quoteAmount.");
|
|
1367
|
+
}
|
|
1368
|
+
const baseQuantity = positiveDecimal2(value, "baseQuantity");
|
|
1369
|
+
const price = positiveDecimal2(value, "price");
|
|
1370
|
+
return { symbol, volume: baseQuantity, side, type, price, intent: "limit", baseQuantity, ...optionalOrderFields(value) };
|
|
1371
|
+
}
|
|
1372
|
+
function validateMarketBuy(input) {
|
|
1373
|
+
const value = strictObject(input, ["symbol", "quoteAmount", "newClientOrderId", "recvWindow"]);
|
|
1374
|
+
return validateSemanticOrder({ ...value, side: "BUY", type: "MARKET" });
|
|
1375
|
+
}
|
|
1376
|
+
function toOpenApiOrder(order) {
|
|
1377
|
+
return {
|
|
1378
|
+
symbol: order.symbol,
|
|
1379
|
+
volume: order.volume,
|
|
1380
|
+
side: order.side,
|
|
1381
|
+
type: order.type,
|
|
1382
|
+
newClientOrderId: order.newClientOrderId,
|
|
1383
|
+
...order.price ? { price: order.price } : {},
|
|
1384
|
+
...order.timeInForce ? { timeInForce: order.timeInForce } : {},
|
|
1385
|
+
...order.recvWindow ? { recvWindow: order.recvWindow } : {}
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function validateMarketSell(input) {
|
|
1389
|
+
const value = strictObject(input, ["symbol", "baseQuantity", "newClientOrderId", "recvWindow"]);
|
|
1390
|
+
return validateSemanticOrder({ ...value, side: "SELL", type: "MARKET" });
|
|
1391
|
+
}
|
|
1392
|
+
function validateLimitOrder(input) {
|
|
1393
|
+
const value = strictObject(input, ["symbol", "side", "baseQuantity", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
|
|
1394
|
+
return validateSemanticOrder({ ...value, type: "LIMIT" });
|
|
1395
|
+
}
|
|
1145
1396
|
function validateCancelOrder(input) {
|
|
1146
1397
|
const value = strictObject(input, ["symbol", "orderId", "newClientOrderId"]);
|
|
1147
1398
|
return { symbol: requiredString(value, "symbol"), orderId: requiredString(value, "orderId"), ...optionalString(value, "newClientOrderId") ? { newClientOrderId: optionalString(value, "newClientOrderId") } : {} };
|
|
1148
1399
|
}
|
|
1149
1400
|
function validateTestOrder(input) {
|
|
1150
|
-
|
|
1151
|
-
const type = orderType(value);
|
|
1152
|
-
const price = optionalString(value, "price");
|
|
1153
|
-
if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
|
|
1154
|
-
if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
|
|
1155
|
-
return { symbol: requiredString(value, "symbol"), volume: positiveDecimal2(value, "volume"), side: orderSide(value), type, ...price ? { price } : {}, ...optionalString(value, "newClientOrderId") ? { newClientOrderId: optionalString(value, "newClientOrderId") } : {}, ...optionalString(value, "recvWindow") ? { recvWindow: optionalString(value, "recvWindow") } : {} };
|
|
1401
|
+
return toOpenApiOrder(validateSemanticOrder(input));
|
|
1156
1402
|
}
|
|
1157
1403
|
function validateBatchOrders(input) {
|
|
1158
1404
|
const value = strictObject(input, ["symbol", "orders"]);
|
|
1405
|
+
const symbol = requiredString(value, "symbol");
|
|
1159
1406
|
const orders = value.orders;
|
|
1160
1407
|
if (!Array.isArray(orders) || orders.length < 1 || orders.length > 10) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "orders must contain between 1 and 10 orders.");
|
|
1161
|
-
|
|
1162
|
-
if (!
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const side = orderSide(order);
|
|
1167
|
-
const batchType = requiredEnum(order, "batchType", ["LIMIT", "MARKET"]);
|
|
1168
|
-
if (batchType === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}].price is required for LIMIT.`);
|
|
1169
|
-
if (batchType === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}].price is not allowed for MARKET.`);
|
|
1170
|
-
return { volume, side, batchType, ...price ? { price } : {} };
|
|
1171
|
-
});
|
|
1172
|
-
return { symbol: requiredString(value, "symbol"), orders: normalized };
|
|
1408
|
+
return { symbol, orders: orders.map((order, index) => {
|
|
1409
|
+
if (!order || typeof order !== "object" || Array.isArray(order)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}] must be an object.`);
|
|
1410
|
+
const semantic = validateSemanticOrder({ ...order, symbol });
|
|
1411
|
+
return { volume: semantic.volume, side: semantic.side, batchType: semantic.type, ...semantic.price ? { price: semantic.price } : {} };
|
|
1412
|
+
}) };
|
|
1173
1413
|
}
|
|
1174
1414
|
function validateBatchCancel(input) {
|
|
1175
1415
|
const value = strictObject(input, ["symbol", "orderIds"]);
|
|
@@ -1178,17 +1418,44 @@ function validateBatchCancel(input) {
|
|
|
1178
1418
|
}
|
|
1179
1419
|
return { symbol: requiredString(value, "symbol"), orderIds: value.orderIds.map((id) => id.trim()) };
|
|
1180
1420
|
}
|
|
1421
|
+
function semanticOrderSummary(order) {
|
|
1422
|
+
return {
|
|
1423
|
+
action: order.intent === "market_buy" ? "market_buy" : order.intent === "market_sell" ? "market_sell" : "limit_order",
|
|
1424
|
+
symbol: order.symbol,
|
|
1425
|
+
side: order.side,
|
|
1426
|
+
type: order.type,
|
|
1427
|
+
...order.quoteAmount ? { quoteAmount: order.quoteAmount, amountMeaning: "exact quote-asset amount to spend" } : {},
|
|
1428
|
+
...order.baseQuantity ? { baseQuantity: order.baseQuantity, amountMeaning: "exact base-asset quantity" } : {},
|
|
1429
|
+
...order.price ? { price: order.price } : {},
|
|
1430
|
+
...order.timeInForce ? { timeInForce: order.timeInForce } : {},
|
|
1431
|
+
newClientOrderId: order.newClientOrderId
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
async function preflightSpotOrder(order, context) {
|
|
1435
|
+
await preflightSymbolOrder(context, order);
|
|
1436
|
+
return order;
|
|
1437
|
+
}
|
|
1438
|
+
async function preflightSpotBatch(input, context) {
|
|
1439
|
+
await Promise.all(input.orders.map((order) => preflightSymbolOrder(context, {
|
|
1440
|
+
symbol: input.symbol,
|
|
1441
|
+
side: order.side,
|
|
1442
|
+
type: order.batchType,
|
|
1443
|
+
...order.batchType === "MARKET" && order.side === "BUY" ? { quoteAmount: order.volume } : { baseQuantity: order.volume },
|
|
1444
|
+
...order.price ? { price: order.price } : {}
|
|
1445
|
+
})));
|
|
1446
|
+
return input;
|
|
1447
|
+
}
|
|
1181
1448
|
var orderTools = [
|
|
1182
1449
|
{
|
|
1183
1450
|
name: "spot_test_order",
|
|
1184
1451
|
title: "Test Spot Order",
|
|
1185
|
-
description: "Validate
|
|
1452
|
+
description: "Validate an order without sending it to the matching engine. MARKET BUY requires quoteAmount (quote asset to spend); MARKET SELL and LIMIT require baseQuantity. LIMIT also requires price.",
|
|
1186
1453
|
cliPath: ["spot", "order", "test"],
|
|
1187
1454
|
module: "spot-order",
|
|
1188
1455
|
access: "signed",
|
|
1189
1456
|
operation: "read",
|
|
1190
1457
|
riskLevel: "low",
|
|
1191
|
-
inputSchema: { type: "object", properties: { symbol: { type: "string" },
|
|
1458
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, quoteAmount: { type: "string", description: "Required only for MARKET BUY: exact quote-asset amount to spend." }, baseQuantity: { type: "string", description: "Required for MARKET SELL and LIMIT: exact base-asset quantity." }, price: { type: "string", description: "Required only for LIMIT." }, timeInForce: { type: "string", enum: ["GTC", "IOC", "FOK"] }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "side", "type"], additionalProperties: false },
|
|
1192
1459
|
errorCodes: signedReadErrors2,
|
|
1193
1460
|
validate: validateTestOrder,
|
|
1194
1461
|
handler: (input, context) => context.api.signedPost("/sapi/v2/order/test", input, signed2(context))
|
|
@@ -1213,15 +1480,16 @@ var orderTools = [
|
|
|
1213
1480
|
{
|
|
1214
1481
|
name: "spot_batch_place_orders",
|
|
1215
1482
|
title: "Batch Place Spot Orders",
|
|
1216
|
-
description: "Create up to 10 spot orders after confirmation.",
|
|
1483
|
+
description: "Create up to 10 spot orders after confirmation. Each MARKET BUY item requires quoteAmount; MARKET SELL and LIMIT items require baseQuantity; LIMIT items also require price.",
|
|
1217
1484
|
cliPath: ["spot", "order", "batch-place"],
|
|
1218
1485
|
module: "spot-order",
|
|
1219
1486
|
access: "signed",
|
|
1220
1487
|
operation: "write",
|
|
1221
1488
|
riskLevel: "high",
|
|
1222
|
-
inputSchema: { type: "object", properties: { symbol: { type: "string" }, orders: { type: "array" } }, required: ["symbol", "orders"], additionalProperties: false },
|
|
1489
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, orders: { type: "array", description: "Items use side/type plus quoteAmount for MARKET BUY, baseQuantity for MARKET SELL/LIMIT, and price for LIMIT." } }, required: ["symbol", "orders"], additionalProperties: false },
|
|
1223
1490
|
errorCodes: writeErrors2,
|
|
1224
1491
|
validate: validateBatchOrders,
|
|
1492
|
+
preflight: preflightSpotBatch,
|
|
1225
1493
|
handler: (input, context) => context.api.signedPost("/sapi/v2/batchOrders", input, signed2(context)),
|
|
1226
1494
|
writeSummary: (input) => {
|
|
1227
1495
|
const value = input;
|
|
@@ -1248,7 +1516,7 @@ var orderTools = [
|
|
|
1248
1516
|
},
|
|
1249
1517
|
{
|
|
1250
1518
|
name: "spot_get_open_orders",
|
|
1251
|
-
title: "Get Open
|
|
1519
|
+
title: "Get Spot Open Orders",
|
|
1252
1520
|
description: "Get current signed spot orders.",
|
|
1253
1521
|
cliPath: ["spot", "order", "open"],
|
|
1254
1522
|
module: "spot-order",
|
|
@@ -1281,22 +1549,52 @@ var orderTools = [
|
|
|
1281
1549
|
handler: (input, context) => context.api.getMyTrades(input, signed2(context))
|
|
1282
1550
|
},
|
|
1283
1551
|
{
|
|
1284
|
-
name: "
|
|
1285
|
-
title: "
|
|
1286
|
-
description: "
|
|
1287
|
-
cliPath: ["spot", "order", "
|
|
1552
|
+
name: "spot_market_buy",
|
|
1553
|
+
title: "Market Buy by Quote Amount",
|
|
1554
|
+
description: "Spend an exact quote-asset amount to buy at market. For ETHUSDT, quoteAmount is USDT. Do not use this tool to request a base-asset quantity such as 1 ETH; market execution cannot guarantee an exact base quantity.",
|
|
1555
|
+
cliPath: ["spot", "order", "market-buy"],
|
|
1288
1556
|
module: "spot-order",
|
|
1289
1557
|
access: "signed",
|
|
1290
1558
|
operation: "write",
|
|
1291
1559
|
riskLevel: "high",
|
|
1292
|
-
inputSchema: { type: "object", properties: { symbol: { type: "string" },
|
|
1560
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, quoteAmount: { type: "string", description: "Required exact quote-asset amount to spend, e.g. 100 means spend 100 USDT for ETHUSDT." }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "quoteAmount"], additionalProperties: false },
|
|
1293
1561
|
errorCodes: writeErrors2,
|
|
1294
|
-
validate:
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1562
|
+
validate: validateMarketBuy,
|
|
1563
|
+
preflight: preflightSpotOrder,
|
|
1564
|
+
handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
|
|
1565
|
+
writeSummary: (input) => semanticOrderSummary(input)
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
name: "spot_market_sell",
|
|
1569
|
+
title: "Market Sell by Base Quantity",
|
|
1570
|
+
description: "Sell an exact base-asset quantity at market. For ETHUSDT, baseQuantity is ETH.",
|
|
1571
|
+
cliPath: ["spot", "order", "market-sell"],
|
|
1572
|
+
module: "spot-order",
|
|
1573
|
+
access: "signed",
|
|
1574
|
+
operation: "write",
|
|
1575
|
+
riskLevel: "high",
|
|
1576
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, baseQuantity: { type: "string", description: "Required exact base-asset quantity to sell, e.g. 0.5 means sell 0.5 ETH for ETHUSDT." }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "baseQuantity"], additionalProperties: false },
|
|
1577
|
+
errorCodes: writeErrors2,
|
|
1578
|
+
validate: validateMarketSell,
|
|
1579
|
+
preflight: preflightSpotOrder,
|
|
1580
|
+
handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
|
|
1581
|
+
writeSummary: (input) => semanticOrderSummary(input)
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
name: "spot_limit_order",
|
|
1585
|
+
title: "Place Limit Spot Order",
|
|
1586
|
+
description: "Place a LIMIT BUY or SELL using an exact base-asset quantity and a limit price.",
|
|
1587
|
+
cliPath: ["spot", "order", "limit"],
|
|
1588
|
+
module: "spot-order",
|
|
1589
|
+
access: "signed",
|
|
1590
|
+
operation: "write",
|
|
1591
|
+
riskLevel: "high",
|
|
1592
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, baseQuantity: { type: "string", description: "Required exact base-asset quantity." }, price: { type: "string", description: "Required limit price in the quote asset." }, timeInForce: { type: "string", enum: ["GTC", "IOC", "FOK"] }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "side", "baseQuantity", "price"], additionalProperties: false },
|
|
1593
|
+
errorCodes: writeErrors2,
|
|
1594
|
+
validate: validateLimitOrder,
|
|
1595
|
+
preflight: preflightSpotOrder,
|
|
1596
|
+
handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
|
|
1597
|
+
writeSummary: (input) => semanticOrderSummary(input)
|
|
1300
1598
|
},
|
|
1301
1599
|
{
|
|
1302
1600
|
name: "spot_cancel_order",
|
|
@@ -1598,22 +1896,22 @@ var ToolRegistry = class {
|
|
|
1598
1896
|
const validInput = tool.validate(input);
|
|
1599
1897
|
return tool.handler(validInput, context);
|
|
1600
1898
|
}
|
|
1601
|
-
prepareWrite(name, input, context, confirmations) {
|
|
1899
|
+
async prepareWrite(name, input, context, confirmations) {
|
|
1602
1900
|
const tool = this.byName(name);
|
|
1603
1901
|
if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
|
|
1604
1902
|
const validInput = tool.validate(input);
|
|
1903
|
+
const preflightInput = tool.preflight ? await tool.preflight(validInput, context) : validInput;
|
|
1605
1904
|
return confirmations.prepare({
|
|
1606
1905
|
action: tool.name,
|
|
1607
|
-
payload:
|
|
1906
|
+
payload: preflightInput,
|
|
1608
1907
|
context: confirmationContext(context),
|
|
1609
|
-
summary: tool.writeSummary(
|
|
1908
|
+
summary: tool.writeSummary(preflightInput)
|
|
1610
1909
|
});
|
|
1611
1910
|
}
|
|
1612
1911
|
async executeConfirmed(action, payload, context) {
|
|
1613
1912
|
const tool = this.byName(action);
|
|
1614
1913
|
if (tool.operation !== "write") throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${action}" is not a write Tool.`);
|
|
1615
|
-
|
|
1616
|
-
return tool.handler(validInput, context);
|
|
1914
|
+
return tool.handler(payload, context);
|
|
1617
1915
|
}
|
|
1618
1916
|
capabilities(context, options = {}) {
|
|
1619
1917
|
return this.list(options).map((tool) => ({
|
|
@@ -1866,10 +2164,10 @@ function createServer(profileName, readOnly) {
|
|
|
1866
2164
|
const registry = createToolRegistry();
|
|
1867
2165
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
1868
2166
|
const server = new Server(
|
|
1869
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
2167
|
+
{ name: "ai-hub-agent-trade", version: "0.1.3" },
|
|
1870
2168
|
{
|
|
1871
2169
|
capabilities: { tools: {} },
|
|
1872
|
-
instructions: "For every state-changing action, call only a spot_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message."
|
|
2170
|
+
instructions: "For every state-changing action, call only a spot_prepare_* or margin_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message. For spot and margin orders, MARKET BUY always uses quoteAmount (the quote asset to spend); MARKET SELL uses baseQuantity (the base asset to sell). Never reinterpret a requested base quantity as quoteAmount."
|
|
1873
2171
|
}
|
|
1874
2172
|
);
|
|
1875
2173
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -1913,7 +2211,7 @@ function createServer(profileName, readOnly) {
|
|
|
1913
2211
|
}
|
|
1914
2212
|
const preparedTool = registry.list({ readOnly: false }).find((tool) => tool.operation === "write" && prepareToolName(tool) === request.params.name);
|
|
1915
2213
|
if (preparedTool) {
|
|
1916
|
-
return result({ ok: true, data: writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
|
|
2214
|
+
return result({ ok: true, data: await writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
|
|
1917
2215
|
}
|
|
1918
2216
|
const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
|
|
1919
2217
|
return result({ ok: true, data });
|
|
@@ -1932,7 +2230,16 @@ function readOption(args, option) {
|
|
|
1932
2230
|
if (!value || value.startsWith("--")) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${option} requires a value.`);
|
|
1933
2231
|
return value;
|
|
1934
2232
|
}
|
|
2233
|
+
function printUsage() {
|
|
2234
|
+
process.stdout.write(
|
|
2235
|
+
"AI Hub Agent Trade MCP\n\nUsage:\n ai-hub-trade-mcp [--profile <name>] [--read-only]\n ai-hub-trade-mcp setup --client <client> [--profile <name>]\n\nThe default command starts the local stdio MCP server.\nUse setup to register it with Cursor, Claude Desktop, Claude Code, or Codex.\n"
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
1935
2238
|
async function main(argv) {
|
|
2239
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
2240
|
+
printUsage();
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
1936
2243
|
if (argv[0] === "setup") {
|
|
1937
2244
|
const client = readOption(argv.slice(1), "--client");
|
|
1938
2245
|
const profile = readOption(argv.slice(1), "--profile");
|
|
@@ -1953,7 +2260,15 @@ async function main(argv) {
|
|
|
1953
2260
|
const server = createServer(profileName, readOnly);
|
|
1954
2261
|
await server.connect(new StdioServerTransport());
|
|
1955
2262
|
}
|
|
1956
|
-
|
|
2263
|
+
function isDirectExecution() {
|
|
2264
|
+
if (!process.argv[1]) return false;
|
|
2265
|
+
try {
|
|
2266
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
2267
|
+
} catch {
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (isDirectExecution()) main(process.argv.slice(2)).catch((error) => {
|
|
1957
2272
|
const payload = error instanceof AiHubError ? { code: error.code, message: error.message } : { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
|
|
1958
2273
|
process.stderr.write(`${JSON.stringify(payload)}
|
|
1959
2274
|
`);
|