@aihubspot/agent-trade-mcp 0.1.0

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 ADDED
@@ -0,0 +1,1658 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // ../core/src/errors.ts
7
+ var AiHubError = class extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.name = "AiHubError";
12
+ this.code = code;
13
+ }
14
+ };
15
+
16
+ // ../core/src/credential.ts
17
+ import { createHash } from "crypto";
18
+ import keytar from "keytar";
19
+ var SERVICE_NAME = "ai-hub-agent-trade";
20
+ function validate(credentials) {
21
+ const apiKey = credentials.apiKey.trim();
22
+ const secretKey = credentials.secretKey.trim();
23
+ if (!apiKey || !secretKey) {
24
+ throw new AiHubError("AI_HUB_INVALID_CREDENTIAL", "API key and secret key must both be present.");
25
+ }
26
+ return { apiKey, secretKey };
27
+ }
28
+ function versionOf(credentials) {
29
+ return createHash("sha256").update(`${credentials.apiKey}\0${credentials.secretKey}`).digest("hex");
30
+ }
31
+ var CredentialStore = class {
32
+ async set(profile, credentials) {
33
+ const value = validate(credentials);
34
+ await keytar.setPassword(SERVICE_NAME, profile, JSON.stringify(value));
35
+ return { credentialRef: `keychain:${SERVICE_NAME}/${profile}`, credentialVersion: versionOf(value) };
36
+ }
37
+ async get(profile) {
38
+ const stored = await keytar.getPassword(SERVICE_NAME, profile);
39
+ if (!stored) return void 0;
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(stored);
43
+ } catch {
44
+ throw new AiHubError("AI_HUB_CREDENTIAL_INVALID", "Stored credentials are unreadable. Set credentials again.");
45
+ }
46
+ const credentials = validate(parsed);
47
+ return { ...credentials, credentialVersion: versionOf(credentials) };
48
+ }
49
+ async remove(profile) {
50
+ return keytar.deletePassword(SERVICE_NAME, profile);
51
+ }
52
+ };
53
+
54
+ // ../core/src/confirmation.ts
55
+ import { createHash as createHash2, randomUUID } from "crypto";
56
+ function stableJson(value) {
57
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
58
+ if (value && typeof value === "object") {
59
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
60
+ }
61
+ return JSON.stringify(value);
62
+ }
63
+ function hash(input) {
64
+ return createHash2("sha256").update(stableJson({ action: input.action, payload: input.payload, context: input.context })).digest("hex");
65
+ }
66
+ var ConfirmationService = class {
67
+ constructor(ttlMs = 5 * 60 * 1e3, now = () => Date.now()) {
68
+ this.ttlMs = ttlMs;
69
+ this.now = now;
70
+ }
71
+ ttlMs;
72
+ now;
73
+ actions = /* @__PURE__ */ new Map();
74
+ prepare(input) {
75
+ const expiresAtMs = this.now() + this.ttlMs;
76
+ const confirmationId = randomUUID();
77
+ const requestHash = hash(input);
78
+ this.actions.set(confirmationId, { ...input, confirmationId, requestHash, expiresAtMs, consumed: false });
79
+ return {
80
+ confirmationId,
81
+ expiresAt: new Date(expiresAtMs).toISOString(),
82
+ requestHash,
83
+ action: input.action,
84
+ summary: input.summary,
85
+ requiresNewUserConfirmation: true,
86
+ nextStep: "Stop and wait for a new explicit user confirmation message. Do not call confirm_action from the same user instruction that prepared this request."
87
+ };
88
+ }
89
+ confirm(confirmationId, userConfirmation, context) {
90
+ if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
91
+ throw new AiHubError("AI_HUB_CONFIRMATION_REQUIRED", "A non-empty new explicit user confirmation message is required before a state-changing request can execute.");
92
+ }
93
+ const pending = this.actions.get(confirmationId);
94
+ if (!pending) throw new AiHubError("AI_HUB_CONFIRMATION_NOT_FOUND", "Confirmation was not found or has already been consumed.");
95
+ if (pending.consumed) throw new AiHubError("AI_HUB_CONFIRMATION_CONSUMED", "Confirmation has already been consumed.");
96
+ if (pending.expiresAtMs < this.now()) {
97
+ this.actions.delete(confirmationId);
98
+ throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
99
+ }
100
+ if (stableJson(pending.context) !== stableJson(context)) {
101
+ this.actions.delete(confirmationId);
102
+ throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
103
+ }
104
+ pending.consumed = true;
105
+ this.actions.delete(confirmationId);
106
+ return { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
107
+ }
108
+ };
109
+
110
+ // ../core/src/openapi.ts
111
+ import { createHmac } from "crypto";
112
+ function parseJsonPreservingLargeIntegers(raw) {
113
+ let transformed = "";
114
+ let inString = false;
115
+ let escaping = false;
116
+ let index = 0;
117
+ while (index < raw.length) {
118
+ const character = raw[index] ?? "";
119
+ if (inString) {
120
+ transformed += character;
121
+ if (escaping) escaping = false;
122
+ else if (character === "\\") escaping = true;
123
+ else if (character === '"') inString = false;
124
+ index += 1;
125
+ continue;
126
+ }
127
+ if (character === '"') {
128
+ inString = true;
129
+ transformed += character;
130
+ index += 1;
131
+ continue;
132
+ }
133
+ if (character !== "-" && (character < "0" || character > "9")) {
134
+ transformed += character;
135
+ index += 1;
136
+ continue;
137
+ }
138
+ let end = index + 1;
139
+ while (end < raw.length && /[0-9eE+.\-]/.test(raw[end] ?? "")) end += 1;
140
+ const token = raw.slice(index, end);
141
+ const absolute = token.startsWith("-") ? token.slice(1) : token;
142
+ if (/^\d+$/.test(token) && (absolute.length > 15 || Number(absolute) > Number.MAX_SAFE_INTEGER)) {
143
+ transformed += `"${token}"`;
144
+ } else {
145
+ transformed += token;
146
+ }
147
+ index = end;
148
+ }
149
+ return JSON.parse(transformed);
150
+ }
151
+ function queryString(values) {
152
+ const params = new URLSearchParams();
153
+ for (const [name, value] of Object.entries(values)) {
154
+ if (value !== void 0) params.append(name, String(value));
155
+ }
156
+ return params.toString();
157
+ }
158
+ function joinUrl(baseUrl, path, query) {
159
+ if (!path.startsWith("/")) throw new AiHubError("AI_HUB_INVALID_PATH", "OpenAPI paths must start with '/'.");
160
+ const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
161
+ return query ? `${url}?${query}` : url;
162
+ }
163
+ function signRequest(timestamp, method, path, secretKey, query, body) {
164
+ const payload = `${timestamp}${method.toUpperCase()}${path}${query ? `?${query}` : ""}${body ?? ""}`;
165
+ return createHmac("sha256", secretKey).update(payload).digest("hex");
166
+ }
167
+ var AiHubSpotApi = class {
168
+ constructor(baseUrl, options = {}) {
169
+ this.baseUrl = baseUrl;
170
+ this.timeoutMs = options.timeoutMs ?? 15e3;
171
+ this.userAgent = options.userAgent ?? "ai-hub-agent-trade/0.1.0";
172
+ }
173
+ baseUrl;
174
+ timeoutMs;
175
+ userAgent;
176
+ time() {
177
+ return this.request("GET", "/sapi/v2/time");
178
+ }
179
+ ping() {
180
+ return this.request("GET", "/sapi/v2/ping");
181
+ }
182
+ symbols() {
183
+ return this.request("GET", "/sapi/v2/symbols");
184
+ }
185
+ ticker(params = {}) {
186
+ return this.request("GET", "/sapi/v2/ticker", { timeZone: params.timeZone ?? "UTC+08", symbol: params.symbol, symbols: params.symbols });
187
+ }
188
+ depth(symbol, limit = 20) {
189
+ return this.request("GET", "/sapi/v2/depth", { symbol, limit });
190
+ }
191
+ trades(symbol, limit = 100) {
192
+ return this.request("GET", "/sapi/v2/trades", { symbol, limit });
193
+ }
194
+ klines(params) {
195
+ return this.request("GET", "/sapi/v2/klines", {
196
+ symbol: params.symbol,
197
+ interval: params.interval,
198
+ startTime: params.startTime,
199
+ endTime: params.endTime,
200
+ timezone: params.timezone ?? "UTC+08",
201
+ limit: params.limit ?? 100
202
+ });
203
+ }
204
+ account(credentials) {
205
+ return this.request("GET", "/sapi/v2/account", {}, void 0, credentials);
206
+ }
207
+ getOrder(params, credentials) {
208
+ return this.request("GET", "/sapi/v2/order", params, void 0, credentials);
209
+ }
210
+ getOpenOrders(params, credentials) {
211
+ return this.request("GET", "/sapi/v2/openOrders", params, void 0, credentials);
212
+ }
213
+ getMyTrades(params, credentials) {
214
+ return this.request("GET", "/sapi/v2/myTrades", params, void 0, credentials);
215
+ }
216
+ placeOrder(params, credentials) {
217
+ const body = {
218
+ symbol: params.symbol,
219
+ volume: params.volume,
220
+ side: params.side,
221
+ type: params.type,
222
+ newClientOrderId: params.newClientOrderId
223
+ };
224
+ if (params.price) body.price = params.price;
225
+ if (params.timeInForce) body.timeInForce = params.timeInForce;
226
+ if (params.recvWindow) body.recvWindow = params.recvWindow;
227
+ return this.request("POST", "/sapi/v2/order", {}, body, credentials);
228
+ }
229
+ cancelOrder(params, credentials) {
230
+ const body = { symbol: params.symbol, orderId: params.orderId };
231
+ if (params.newClientOrderId) body.newClientOrderId = params.newClientOrderId;
232
+ return this.request("POST", "/sapi/v2/cancel", {}, body, credentials);
233
+ }
234
+ /**
235
+ * Signed endpoint adapter for Tool Registry entries whose OpenAPI payload is
236
+ * defined by the API document. Keep endpoint selection in the individual Tool
237
+ * definition; never expose this method directly through CLI or MCP.
238
+ */
239
+ signedGet(path, query, credentials) {
240
+ return this.request("GET", path, query, void 0, credentials);
241
+ }
242
+ signedPost(path, body, credentials) {
243
+ return this.request("POST", path, {}, body, credentials);
244
+ }
245
+ async request(method, path, query = {}, body, credentials) {
246
+ const encodedQuery = queryString(query);
247
+ const bodyString = body ? JSON.stringify(body) : void 0;
248
+ const headers = {
249
+ accept: "application/json",
250
+ "admin-language": "en_US",
251
+ "user-agent": this.userAgent
252
+ };
253
+ if (bodyString) headers["content-type"] = "application/json";
254
+ if (credentials) {
255
+ const timestamp = Date.now().toString();
256
+ headers["X-CH-APIKEY"] = credentials.apiKey;
257
+ headers["X-CH-TS"] = timestamp;
258
+ headers["X-CH-SIGN"] = signRequest(timestamp, method, path, credentials.secretKey, encodedQuery || void 0, bodyString);
259
+ }
260
+ let response;
261
+ try {
262
+ response = await fetch(joinUrl(this.baseUrl, path, encodedQuery), {
263
+ method,
264
+ headers,
265
+ body: bodyString,
266
+ redirect: "manual",
267
+ signal: AbortSignal.timeout(this.timeoutMs)
268
+ });
269
+ } catch (error) {
270
+ throw new AiHubError("AI_HUB_OPENAPI_NETWORK_ERROR", error instanceof Error ? error.message : "OpenAPI request failed.");
271
+ }
272
+ const raw = await response.text();
273
+ if (response.status >= 300 && response.status < 400) {
274
+ throw new AiHubError("AI_HUB_OPENAPI_REDIRECT_REJECTED", "OpenAPI returned a redirect, which is rejected for profile safety.");
275
+ }
276
+ if (!response.ok) {
277
+ throw new AiHubError("AI_HUB_OPENAPI_HTTP_ERROR", `OpenAPI returned HTTP ${response.status}.`);
278
+ }
279
+ try {
280
+ return parseJsonPreservingLargeIntegers(raw);
281
+ } catch {
282
+ throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "OpenAPI returned a non-JSON response.");
283
+ }
284
+ }
285
+ };
286
+
287
+ // ../core/src/config.ts
288
+ import { createHash as createHash3 } from "crypto";
289
+ import { lookup } from "dns/promises";
290
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
291
+ import { homedir } from "os";
292
+ import { dirname, join } from "path";
293
+ import { isIP } from "net";
294
+ import { parse, stringify } from "smol-toml";
295
+ var CONFIG_DIRECTORY = ".ai-hub";
296
+ var CONFIG_FILENAME = "config.toml";
297
+ var PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
298
+ function configFilePath(home = homedir()) {
299
+ return join(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
300
+ }
301
+ function validateProfileName(name) {
302
+ if (!PROFILE_NAME.test(name)) {
303
+ throw new AiHubError("AI_HUB_INVALID_PROFILE", "Profile names must be 1-64 letters, numbers, hyphens, or underscores.");
304
+ }
305
+ return name;
306
+ }
307
+ function blockedIp(address) {
308
+ if (isIP(address) === 4) {
309
+ const octets = address.split(".").map(Number);
310
+ const first = octets[0] ?? -1;
311
+ const second = octets[1] ?? -1;
312
+ return first === 0 || first === 10 || first === 127 || first >= 224 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
313
+ }
314
+ const normalized = address.toLowerCase();
315
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
316
+ }
317
+ async function normalizeOpenApiBaseUrl(value) {
318
+ let url;
319
+ try {
320
+ url = new URL(value.trim());
321
+ } catch {
322
+ throw new AiHubError("AI_HUB_INVALID_OPENAPI_URL", "OpenAPI base URL must be a valid HTTPS URL.");
323
+ }
324
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
325
+ throw new AiHubError("AI_HUB_INVALID_OPENAPI_URL", "OpenAPI base URL must be an HTTPS URL without credentials, query, or fragment.");
326
+ }
327
+ if (url.hostname.toLowerCase() === "localhost") {
328
+ throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "Localhost is not allowed as an OpenAPI base URL.");
329
+ }
330
+ const records = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true, verbatim: true });
331
+ if (records.length === 0 || records.some((record) => blockedIp(record.address))) {
332
+ throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "OpenAPI base URL must resolve only to public addresses.");
333
+ }
334
+ url.pathname = url.pathname.replace(/\/+$/, "");
335
+ return url.toString().replace(/\/$/, "");
336
+ }
337
+ function defaultConfig() {
338
+ return { version: 1, default_profile: "default", profiles: {} };
339
+ }
340
+ function parseConfig(text) {
341
+ let parsed;
342
+ try {
343
+ parsed = parse(text);
344
+ } catch (error) {
345
+ throw new AiHubError("AI_HUB_CONFIG_PARSE_ERROR", `Cannot parse config.toml: ${error instanceof Error ? error.message : "unknown error"}`);
346
+ }
347
+ const raw = parsed;
348
+ if (raw.version !== 1 || typeof raw.default_profile !== "string" || !raw.profiles || typeof raw.profiles !== "object") {
349
+ throw new AiHubError("AI_HUB_CONFIG_INVALID", "config.toml must contain version = 1, default_profile, and profiles.");
350
+ }
351
+ return raw;
352
+ }
353
+ function versionOf2(config, profileName) {
354
+ return createHash3("sha256").update(JSON.stringify({ version: config.version, profileName, profile: config.profiles[profileName] })).digest("hex");
355
+ }
356
+ var ConfigStore = class {
357
+ constructor(filePath = configFilePath()) {
358
+ this.filePath = filePath;
359
+ }
360
+ filePath;
361
+ async read() {
362
+ try {
363
+ return parseConfig(await readFile(this.filePath, "utf8"));
364
+ } catch (error) {
365
+ if (error.code === "ENOENT") return defaultConfig();
366
+ throw error;
367
+ }
368
+ }
369
+ async initialize() {
370
+ const config = await this.read();
371
+ const existing = Object.keys(config.profiles).length > 0;
372
+ if (!existing) await this.write(config);
373
+ return { created: !existing, path: this.filePath };
374
+ }
375
+ async setProfile(name, openApiBaseUrl) {
376
+ validateProfileName(name);
377
+ const config = await this.read();
378
+ const normalizedUrl = await normalizeOpenApiBaseUrl(openApiBaseUrl);
379
+ const existing = config.profiles[name];
380
+ config.profiles[name] = { openapi_base_url: normalizedUrl, ...existing?.credential_ref ? { credential_ref: existing.credential_ref } : {} };
381
+ if (Object.keys(config.profiles).length === 1) config.default_profile = name;
382
+ await this.write(config);
383
+ return this.resolveFrom(config, name);
384
+ }
385
+ async showProfile(requestedName) {
386
+ const config = await this.read();
387
+ return this.resolveFrom(config, requestedName ?? config.default_profile);
388
+ }
389
+ async removeProfile(name) {
390
+ validateProfileName(name);
391
+ const config = await this.read();
392
+ if (!config.profiles[name]) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
393
+ delete config.profiles[name];
394
+ if (config.default_profile === name) config.default_profile = Object.keys(config.profiles)[0] ?? "default";
395
+ await this.write(config);
396
+ }
397
+ async setCredentialRef(name, credentialRef) {
398
+ validateProfileName(name);
399
+ const config = await this.read();
400
+ const profile = config.profiles[name];
401
+ if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
402
+ config.profiles[name] = { ...profile, credential_ref: credentialRef };
403
+ await this.write(config);
404
+ return this.resolveFrom(config, name);
405
+ }
406
+ resolveFrom(config, name) {
407
+ validateProfileName(name);
408
+ const profile = config.profiles[name];
409
+ if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
410
+ return {
411
+ name,
412
+ openApiBaseUrl: profile.openapi_base_url,
413
+ credentialRef: profile.credential_ref,
414
+ configVersion: versionOf2(config, name)
415
+ };
416
+ }
417
+ async write(config) {
418
+ const directory = dirname(this.filePath);
419
+ await mkdir(directory, { recursive: true, mode: 448 });
420
+ await chmod(directory, 448);
421
+ const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
422
+ await writeFile(temporaryPath, stringify(config), { mode: 384 });
423
+ await chmod(temporaryPath, 384);
424
+ await rename(temporaryPath, this.filePath);
425
+ await chmod(this.filePath, 384);
426
+ }
427
+ };
428
+
429
+ // ../core/src/tools/execution-context.ts
430
+ async function createToolExecutionContext(profileName) {
431
+ const profile = await new ConfigStore().showProfile(profileName);
432
+ const credentials = await new CredentialStore().get(profile.name);
433
+ return { profile, credentials, api: new AiHubSpotApi(profile.openApiBaseUrl) };
434
+ }
435
+ function confirmationContext(context) {
436
+ if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
437
+ return {
438
+ profile: context.profile.name,
439
+ openApiBaseUrl: context.profile.openApiBaseUrl,
440
+ configVersion: context.profile.configVersion,
441
+ credentialVersion: context.credentials.credentialVersion
442
+ };
443
+ }
444
+
445
+ // ../core/src/tools/validation.ts
446
+ function strictObject(input, allowedKeys) {
447
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
448
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "Tool input must be an object.");
449
+ }
450
+ const record = input;
451
+ for (const key of Object.keys(record)) {
452
+ if (!allowedKeys.includes(key)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `Unknown argument "${key}".`);
453
+ }
454
+ return record;
455
+ }
456
+ function requiredString(input, name) {
457
+ const value = input[name];
458
+ if (typeof value !== "string" || !value.trim()) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} is required.`);
459
+ return value.trim();
460
+ }
461
+ function optionalString(input, name) {
462
+ const value = input[name];
463
+ if (value === void 0) return void 0;
464
+ if (typeof value !== "string" || !value.trim()) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be a non-empty string.`);
465
+ return value.trim();
466
+ }
467
+ function optionalInteger(input, name, fallback, minimum, maximum) {
468
+ const value = input[name];
469
+ if (value === void 0) return fallback;
470
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) {
471
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be an integer between ${minimum} and ${maximum}.`);
472
+ }
473
+ return value;
474
+ }
475
+
476
+ // ../core/src/tools/account-tools.ts
477
+ var accountTools = [
478
+ {
479
+ name: "spot_get_account",
480
+ title: "Get Spot Account",
481
+ description: "Get the signed account overview for the configured profile.",
482
+ cliPath: ["account", "get"],
483
+ module: "spot-account",
484
+ access: "signed",
485
+ operation: "read",
486
+ riskLevel: "low",
487
+ inputSchema: { type: "object", additionalProperties: false },
488
+ errorCodes: ["AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"],
489
+ validate: (input) => strictObject(input, []),
490
+ handler: async (_input, context) => {
491
+ if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
492
+ return context.api.account(context.credentials);
493
+ }
494
+ }
495
+ ];
496
+
497
+ // ../core/src/tools/tool-utils.ts
498
+ import { randomUUID as randomUUID2 } from "crypto";
499
+ var signedReadErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"];
500
+ var writeErrors = [...signedReadErrors, "AI_HUB_WRITE_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_EXPIRED", "AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "AI_HUB_CONFIRMATION_NOT_FOUND"];
501
+ function signed(context) {
502
+ if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
503
+ return context.credentials;
504
+ }
505
+ function positiveDecimal(input, name) {
506
+ const value = requiredString(input, name);
507
+ if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value) || !/[1-9]/.test(value.replace(".", ""))) {
508
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be a positive decimal string.`);
509
+ }
510
+ return value;
511
+ }
512
+ function requiredEnum(input, name, allowed) {
513
+ const value = requiredString(input, name).toUpperCase();
514
+ if (!allowed.includes(value)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be one of: ${allowed.join(", ")}.`);
515
+ return value;
516
+ }
517
+ function optionalBoolean(input, name) {
518
+ const value = input[name];
519
+ if (value === void 0) return void 0;
520
+ if (typeof value !== "boolean") throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be a boolean.`);
521
+ return value;
522
+ }
523
+ function optionalPositiveInteger(input, name, fallback, maximum = 1e3) {
524
+ const value = input[name];
525
+ if (value === void 0) return fallback;
526
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > maximum) {
527
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be an integer between 1 and ${maximum}.`);
528
+ }
529
+ return value;
530
+ }
531
+ function optionalClientOrderId(input) {
532
+ return optionalString(input, "newClientOrderId") ?? `agent_${randomUUID2().replaceAll("-", "").slice(0, 24)}`;
533
+ }
534
+
535
+ // ../core/src/tools/asset-tools.ts
536
+ var accountTypes = ["1", "2", "3", "4", "5"];
537
+ function accountType(value, name) {
538
+ const result2 = requiredString(value, name);
539
+ if (!accountTypes.includes(result2)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be an account type from 1 to 5.`);
540
+ return result2;
541
+ }
542
+ function requiredTransfer(input) {
543
+ const value = strictObject(input, ["fromAccountType", "toAccountType", "symbol", "coinSymbol", "amount"]);
544
+ return { fromAccountType: accountType(value, "fromAccountType"), toAccountType: accountType(value, "toAccountType"), coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {} };
545
+ }
546
+ function transferQuery(input) {
547
+ const value = strictObject(input, ["fromAccountType", "toAccountType", "symbol", "coinSymbol", "page", "pageSize"]);
548
+ return { fromAccountType: accountType(value, "fromAccountType"), toAccountType: accountType(value, "toAccountType"), page: optionalPositiveInteger(value, "page", 1), pageSize: optionalPositiveInteger(value, "pageSize", 20, 100), ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {} };
549
+ }
550
+ function transferAccounts(input) {
551
+ const value = strictObject(input, ["coinSymbol", "amount", "fromAccount", "toAccount"]);
552
+ return { coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), fromAccount: requiredString(value, "fromAccount"), toAccount: requiredString(value, "toAccount") };
553
+ }
554
+ function transferAccountHistory(input) {
555
+ const value = strictObject(input, ["transferId", "coinSymbol", "fromAccount", "toAccount", "startTime", "endTime", "page", "limit"]);
556
+ const transferId = optionalString(value, "transferId");
557
+ if (!transferId && (!optionalString(value, "fromAccount") || !optionalString(value, "toAccount"))) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "fromAccount and toAccount are required when transferId is omitted.");
558
+ return { ...transferId ? { transferId } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {}, ...optionalString(value, "fromAccount") ? { fromAccount: optionalString(value, "fromAccount") } : {}, ...optionalString(value, "toAccount") ? { toAccount: optionalString(value, "toAccount") } : {}, ...optionalString(value, "startTime") ? { startTime: optionalString(value, "startTime") } : {}, ...optionalString(value, "endTime") ? { endTime: optionalString(value, "endTime") } : {}, page: optionalPositiveInteger(value, "page", 1), limit: optionalPositiveInteger(value, "limit", 20, 100) };
559
+ }
560
+ function pagedDates(input) {
561
+ const value = strictObject(input, ["startTime", "endTime", "page", "pageSize"]);
562
+ return { ...optionalString(value, "startTime") ? { startTime: optionalString(value, "startTime") } : {}, ...optionalString(value, "endTime") ? { endTime: optionalString(value, "endTime") } : {}, page: optionalPositiveInteger(value, "page", 1), pageSize: optionalPositiveInteger(value, "pageSize", 20, 100) };
563
+ }
564
+ function coinOnly(input) {
565
+ const value = strictObject(input, ["mainCoinSymbol"]);
566
+ return { mainCoinSymbol: requiredString(value, "mainCoinSymbol") };
567
+ }
568
+ var assetTools = [
569
+ {
570
+ name: "account_transfer",
571
+ title: "Transfer Between Accounts",
572
+ description: "Transfer an asset between documented account types after confirmation.",
573
+ cliPath: ["account", "transfer"],
574
+ module: "spot-account",
575
+ access: "signed",
576
+ operation: "write",
577
+ riskLevel: "high",
578
+ inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, amount: { type: "string" }, fromAccount: { type: "string" }, toAccount: { type: "string" } }, required: ["coinSymbol", "amount", "fromAccount", "toAccount"], additionalProperties: false },
579
+ errorCodes: writeErrors,
580
+ validate: transferAccounts,
581
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/transfer", input, signed(context)),
582
+ writeSummary: (input) => ({ action: "account_transfer", ...input })
583
+ },
584
+ {
585
+ name: "account_get_transfer_history",
586
+ title: "Get Account Transfer History",
587
+ description: "Query documented account transfer history.",
588
+ cliPath: ["account", "transfer-history"],
589
+ module: "spot-account",
590
+ access: "signed",
591
+ operation: "read",
592
+ riskLevel: "low",
593
+ inputSchema: { type: "object", properties: { transferId: { type: "string" }, coinSymbol: { type: "string" }, fromAccount: { type: "string" }, toAccount: { type: "string" }, startTime: { type: "string" }, endTime: { type: "string" }, page: { type: "integer" }, limit: { type: "integer" } }, additionalProperties: false },
594
+ errorCodes: signedReadErrors,
595
+ validate: transferAccountHistory,
596
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/transferQuery", input, signed(context))
597
+ },
598
+ {
599
+ name: "wallet_universal_transfer",
600
+ title: "Universal Asset Transfer",
601
+ description: "Transfer between account types after confirmation.",
602
+ cliPath: ["wallet", "transfer"],
603
+ module: "spot-deposit-withdraw",
604
+ access: "signed",
605
+ operation: "write",
606
+ riskLevel: "high",
607
+ inputSchema: { type: "object", properties: { fromAccountType: { type: "string" }, toAccountType: { type: "string" }, symbol: { type: "string" }, coinSymbol: { type: "string" }, amount: { type: "string" } }, required: ["fromAccountType", "toAccountType", "coinSymbol", "amount"], additionalProperties: false },
608
+ errorCodes: writeErrors,
609
+ validate: requiredTransfer,
610
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/universal_transfer", input, signed(context)),
611
+ writeSummary: (input) => ({ action: "universal_transfer", ...input })
612
+ },
613
+ {
614
+ name: "wallet_get_universal_transfer_history",
615
+ title: "Get Universal Transfer History",
616
+ description: "Query universal asset transfer history.",
617
+ cliPath: ["wallet", "transfer-history"],
618
+ module: "spot-deposit-withdraw",
619
+ access: "signed",
620
+ operation: "read",
621
+ riskLevel: "low",
622
+ inputSchema: { type: "object", properties: { fromAccountType: { type: "string" }, toAccountType: { type: "string" }, symbol: { type: "string" }, coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["fromAccountType", "toAccountType"], additionalProperties: false },
623
+ errorCodes: signedReadErrors,
624
+ validate: transferQuery,
625
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/universal_transfer_query", input, signed(context))
626
+ },
627
+ {
628
+ name: "wallet_get_deposit_history",
629
+ title: "Get Deposit History",
630
+ description: "Query deposit history.",
631
+ cliPath: ["wallet", "deposit-history"],
632
+ module: "spot-deposit-withdraw",
633
+ access: "signed",
634
+ operation: "read",
635
+ riskLevel: "low",
636
+ inputSchema: { type: "object", properties: { startTime: { type: "string" }, endTime: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, additionalProperties: false },
637
+ errorCodes: signedReadErrors,
638
+ validate: pagedDates,
639
+ handler: (input, context) => context.api.signedPost("/sapi/v1/deposit/his_list", input, signed(context))
640
+ },
641
+ {
642
+ name: "wallet_get_deposit_address",
643
+ title: "Get Deposit Addresses",
644
+ description: "Query deposit address list for a main coin.",
645
+ cliPath: ["wallet", "deposit-address"],
646
+ module: "spot-deposit-withdraw",
647
+ access: "signed",
648
+ operation: "read",
649
+ riskLevel: "low",
650
+ inputSchema: { type: "object", properties: { mainCoinSymbol: { type: "string" } }, required: ["mainCoinSymbol"], additionalProperties: false },
651
+ errorCodes: signedReadErrors,
652
+ validate: coinOnly,
653
+ handler: (input, context) => context.api.signedPost("/sapi/v1/deposit/query_address", input, signed(context))
654
+ },
655
+ {
656
+ name: "wallet_get_withdraw_address",
657
+ title: "Get Withdraw Addresses",
658
+ description: "Query withdraw address list for a main coin.",
659
+ cliPath: ["wallet", "withdraw-address"],
660
+ module: "spot-deposit-withdraw",
661
+ access: "signed",
662
+ operation: "read",
663
+ riskLevel: "low",
664
+ inputSchema: { type: "object", properties: { mainCoinSymbol: { type: "string" }, trustType: { type: "string" }, addrType: { type: "string" } }, required: ["mainCoinSymbol"], additionalProperties: false },
665
+ errorCodes: signedReadErrors,
666
+ validate: (input) => {
667
+ const value = strictObject(input, ["mainCoinSymbol", "trustType", "addrType"]);
668
+ return { mainCoinSymbol: requiredString(value, "mainCoinSymbol"), ...optionalString(value, "trustType") ? { trustType: optionalString(value, "trustType") } : {}, ...optionalString(value, "addrType") ? { addrType: optionalString(value, "addrType") } : {} };
669
+ },
670
+ handler: (input, context) => context.api.signedPost("/sapi/v1/withdraw/address/query", input, signed(context))
671
+ },
672
+ {
673
+ name: "wallet_get_transferable_assets",
674
+ title: "Get Transferable Assets",
675
+ description: "Query assets transferable from one account type.",
676
+ cliPath: ["wallet", "transferable-assets"],
677
+ module: "spot-deposit-withdraw",
678
+ access: "signed",
679
+ operation: "read",
680
+ riskLevel: "low",
681
+ inputSchema: { type: "object", properties: { accountType: { type: "string" } }, required: ["accountType"], additionalProperties: false },
682
+ errorCodes: signedReadErrors,
683
+ validate: (input) => {
684
+ const value = strictObject(input, ["accountType"]);
685
+ return { accountType: accountType(value, "accountType") };
686
+ },
687
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/account/by_type", input, signed(context))
688
+ },
689
+ {
690
+ name: "wallet_get_exchange_account",
691
+ title: "Get Exchange Account Assets",
692
+ description: "Query exchange account assets.",
693
+ cliPath: ["wallet", "exchange-account"],
694
+ module: "spot-deposit-withdraw",
695
+ access: "signed",
696
+ operation: "read",
697
+ riskLevel: "low",
698
+ inputSchema: { type: "object", additionalProperties: false },
699
+ errorCodes: signedReadErrors,
700
+ validate: (input) => {
701
+ strictObject(input, []);
702
+ return {};
703
+ },
704
+ handler: (_input, context) => context.api.signedPost("/sapi/v1/asset/exchange/account", {}, signed(context))
705
+ },
706
+ {
707
+ name: "wallet_create_withdraw",
708
+ title: "Create Withdrawal",
709
+ description: "Submit a withdrawal request after confirmation.",
710
+ cliPath: ["wallet", "withdraw"],
711
+ module: "spot-deposit-withdraw",
712
+ access: "signed",
713
+ operation: "write",
714
+ riskLevel: "high",
715
+ inputSchema: { type: "object", properties: { withdrawOrderId: { type: "string" }, symbol: { type: "string" }, amount: { type: "string" }, address: { type: "string" }, label: { type: "string" } }, required: ["withdrawOrderId", "symbol", "amount", "address"], additionalProperties: false },
716
+ errorCodes: writeErrors,
717
+ validate: (input) => {
718
+ const value = strictObject(input, ["withdrawOrderId", "symbol", "amount", "address", "label"]);
719
+ return { withdrawOrderId: requiredString(value, "withdrawOrderId"), symbol: requiredString(value, "symbol"), amount: positiveDecimal(value, "amount"), address: requiredString(value, "address"), ...optionalString(value, "label") ? { label: optionalString(value, "label") } : {} };
720
+ },
721
+ handler: (input, context) => context.api.signedPost("/sapi/v1/withdraw/apply", input, signed(context)),
722
+ writeSummary: (input) => {
723
+ const value = input;
724
+ return { action: "create_withdraw", withdrawOrderId: value.withdrawOrderId, symbol: value.symbol, amount: value.amount, address: value.address, label: value.label ?? null };
725
+ }
726
+ },
727
+ {
728
+ name: "wallet_get_withdraw_history",
729
+ title: "Get Withdrawal History",
730
+ description: "Query withdrawal request history.",
731
+ cliPath: ["wallet", "withdraw-history"],
732
+ module: "spot-deposit-withdraw",
733
+ access: "signed",
734
+ operation: "read",
735
+ riskLevel: "low",
736
+ inputSchema: { type: "object", properties: { withdrawId: { type: "string" }, withdrawOrderId: { type: "string" }, symbol: { type: "string" }, startTime: { type: "string" }, endTime: { type: "string" }, page: { type: "integer" } }, additionalProperties: false },
737
+ errorCodes: signedReadErrors,
738
+ validate: (input) => {
739
+ const value = strictObject(input, ["withdrawId", "withdrawOrderId", "symbol", "startTime", "endTime", "page"]);
740
+ return { ...optionalString(value, "withdrawId") ? { withdrawId: optionalString(value, "withdrawId") } : {}, ...optionalString(value, "withdrawOrderId") ? { withdrawOrderId: optionalString(value, "withdrawOrderId") } : {}, ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {}, ...optionalString(value, "startTime") ? { startTime: optionalString(value, "startTime") } : {}, ...optionalString(value, "endTime") ? { endTime: optionalString(value, "endTime") } : {}, page: optionalPositiveInteger(value, "page", 1) };
741
+ },
742
+ handler: (input, context) => context.api.signedPost("/sapi/v1/withdraw/query", input, signed(context))
743
+ }
744
+ ];
745
+
746
+ // ../core/src/tools/market-tools.ts
747
+ var readErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"];
748
+ var marketTools = [
749
+ {
750
+ name: "market_ping",
751
+ title: "Test OpenAPI Connection",
752
+ description: "Test the configured tenant OpenAPI connection.",
753
+ cliPath: ["market", "ping"],
754
+ module: "spot-common",
755
+ access: "public",
756
+ operation: "read",
757
+ riskLevel: "low",
758
+ inputSchema: { type: "object", additionalProperties: false },
759
+ errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"],
760
+ validate: (input) => {
761
+ strictObject(input, []);
762
+ return {};
763
+ },
764
+ handler: (_input, context) => context.api.ping()
765
+ },
766
+ {
767
+ name: "market_get_server_time",
768
+ title: "Get Server Time",
769
+ description: "Get server time from the configured tenant OpenAPI.",
770
+ cliPath: ["market", "time"],
771
+ module: "spot-common",
772
+ access: "public",
773
+ operation: "read",
774
+ riskLevel: "low",
775
+ inputSchema: { type: "object", additionalProperties: false },
776
+ errorCodes: readErrors,
777
+ validate: (input) => strictObject(input, []),
778
+ handler: (_input, context) => context.api.time()
779
+ },
780
+ {
781
+ name: "market_get_symbols",
782
+ title: "Get Spot Symbols",
783
+ description: "Get spot symbols from the configured tenant OpenAPI.",
784
+ cliPath: ["market", "symbols"],
785
+ module: "spot-common",
786
+ access: "public",
787
+ operation: "read",
788
+ riskLevel: "low",
789
+ inputSchema: { type: "object", additionalProperties: false },
790
+ errorCodes: readErrors,
791
+ validate: (input) => strictObject(input, []),
792
+ handler: (_input, context) => context.api.symbols()
793
+ },
794
+ {
795
+ name: "market_get_ticker",
796
+ title: "Get Spot Ticker",
797
+ description: "Get spot ticker data. Pass symbol or symbols when filtering is needed.",
798
+ cliPath: ["market", "ticker"],
799
+ module: "spot-common",
800
+ access: "public",
801
+ operation: "read",
802
+ riskLevel: "low",
803
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, symbols: { type: "string" }, timeZone: { type: "string" } }, additionalProperties: false },
804
+ errorCodes: readErrors,
805
+ validate: (input) => {
806
+ const value = strictObject(input, ["symbol", "symbols", "timeZone"]);
807
+ return { symbol: optionalString(value, "symbol"), symbols: optionalString(value, "symbols"), timeZone: optionalString(value, "timeZone") };
808
+ },
809
+ handler: (input, context) => context.api.ticker(input)
810
+ },
811
+ {
812
+ name: "market_get_depth",
813
+ title: "Get Spot Depth",
814
+ description: "Get the spot order book for one symbol.",
815
+ cliPath: ["market", "depth"],
816
+ module: "spot-common",
817
+ access: "public",
818
+ operation: "read",
819
+ riskLevel: "low",
820
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
821
+ errorCodes: readErrors,
822
+ validate: (input) => {
823
+ const value = strictObject(input, ["symbol", "limit"]);
824
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 100) };
825
+ },
826
+ handler: (input, context) => {
827
+ const value = input;
828
+ return context.api.depth(value.symbol, value.limit);
829
+ }
830
+ },
831
+ {
832
+ name: "market_get_trades",
833
+ title: "Get Recent Spot Trades",
834
+ description: "Get recent spot trades for one symbol.",
835
+ cliPath: ["market", "trades"],
836
+ module: "spot-common",
837
+ access: "public",
838
+ operation: "read",
839
+ riskLevel: "low",
840
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 } }, required: ["symbol"], additionalProperties: false },
841
+ errorCodes: readErrors,
842
+ validate: (input) => {
843
+ const value = strictObject(input, ["symbol", "limit"]);
844
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 100, 1, 1e3) };
845
+ },
846
+ handler: (input, context) => {
847
+ const value = input;
848
+ return context.api.trades(value.symbol, value.limit);
849
+ }
850
+ },
851
+ {
852
+ name: "market_get_klines",
853
+ title: "Get Spot Klines",
854
+ description: "Get spot candlestick data for one symbol and interval.",
855
+ cliPath: ["market", "klines"],
856
+ module: "spot-common",
857
+ access: "public",
858
+ operation: "read",
859
+ riskLevel: "low",
860
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, interval: { type: "string" }, startTime: { type: "integer" }, endTime: { type: "integer" }, timezone: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 } }, required: ["symbol", "interval"], additionalProperties: false },
861
+ errorCodes: readErrors,
862
+ validate: (input) => {
863
+ const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
864
+ const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
865
+ const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
866
+ return { symbol: requiredString(value, "symbol"), interval: requiredString(value, "interval"), startTime, endTime, timezone: optionalString(value, "timezone"), limit: optionalInteger(value, "limit", 100, 1, 1e3) };
867
+ },
868
+ handler: (input, context) => context.api.klines(input)
869
+ }
870
+ ];
871
+
872
+ // ../core/src/tools/margin-tools.ts
873
+ function validateMarginOrder(input) {
874
+ const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "newClientOrderId", "isolated"]);
875
+ const type = requiredEnum(value, "type", ["LIMIT", "MARKET"]);
876
+ const price = optionalString(value, "price");
877
+ if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
878
+ if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
879
+ return { symbol: requiredString(value, "symbol"), volume: positiveDecimal(value, "volume"), side: requiredEnum(value, "side", ["BUY", "SELL"]), type, ...price ? { price } : {}, newClientOrderId: optionalClientOrderId(value), ...optionalBoolean(value, "isolated") !== void 0 ? { isolated: optionalBoolean(value, "isolated") } : {} };
880
+ }
881
+ function validateMarginOrderLookup(input) {
882
+ const value = strictObject(input, ["symbol", "orderId", "newClientOrderId", "isolated"]);
883
+ return { symbol: requiredString(value, "symbol"), orderId: requiredString(value, "orderId"), ...optionalString(value, "newClientOrderId") ? { newClientOrderId: optionalString(value, "newClientOrderId") } : {}, ...optionalBoolean(value, "isolated") !== void 0 ? { isolated: optionalBoolean(value, "isolated") } : {} };
884
+ }
885
+ function validateMarginOpenOrders(input) {
886
+ const value = strictObject(input, ["symbol", "limit", "isolated"]);
887
+ return { ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {}, limit: optionalPositiveInteger(value, "limit", 100, 1e3), ...optionalBoolean(value, "isolated") !== void 0 ? { isolated: optionalBoolean(value, "isolated") } : {} };
888
+ }
889
+ function validateMarginTrades(input) {
890
+ const value = strictObject(input, ["symbol", "limit", "fromId"]);
891
+ return { symbol: requiredString(value, "symbol"), limit: optionalPositiveInteger(value, "limit", 100, 1e3), ...optionalString(value, "fromId") ? { fromId: optionalString(value, "fromId") } : {} };
892
+ }
893
+ var marginTools = [
894
+ {
895
+ name: "margin_get_order",
896
+ title: "Get Margin Order",
897
+ description: "Get one signed margin order from the v2 margin API.",
898
+ cliPath: ["margin", "order", "get"],
899
+ module: "spot-margin",
900
+ access: "signed",
901
+ operation: "read",
902
+ riskLevel: "low",
903
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orderId: { type: "string" }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "orderId"], additionalProperties: false },
904
+ errorCodes: signedReadErrors,
905
+ validate: validateMarginOrderLookup,
906
+ handler: (input, context) => context.api.signedGet("/sapi/v2/margin/order", input, signed(context))
907
+ },
908
+ {
909
+ name: "margin_get_open_orders",
910
+ title: "Get Open Margin Orders",
911
+ description: "Get signed open margin orders from the v2 margin API.",
912
+ cliPath: ["margin", "order", "open"],
913
+ module: "spot-margin",
914
+ access: "signed",
915
+ operation: "read",
916
+ riskLevel: "low",
917
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 }, isolated: { type: "boolean" } }, additionalProperties: false },
918
+ errorCodes: signedReadErrors,
919
+ validate: validateMarginOpenOrders,
920
+ handler: (input, context) => context.api.signedGet("/sapi/v2/margin/openOrders", input, signed(context))
921
+ },
922
+ {
923
+ name: "margin_get_fills",
924
+ title: "Get Margin Fills",
925
+ description: "Get signed margin trade history from the v2 margin API.",
926
+ cliPath: ["margin", "order", "fills"],
927
+ module: "spot-margin",
928
+ access: "signed",
929
+ operation: "read",
930
+ riskLevel: "low",
931
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 }, fromId: { type: "string" } }, required: ["symbol"], additionalProperties: false },
932
+ errorCodes: signedReadErrors,
933
+ validate: validateMarginTrades,
934
+ handler: (input, context) => context.api.signedGet("/sapi/v2/margin/myTrades", input, signed(context))
935
+ },
936
+ {
937
+ name: "margin_place_order",
938
+ title: "Place Margin Order",
939
+ description: "Create one v2 margin order after confirmation.",
940
+ cliPath: ["margin", "order", "place"],
941
+ module: "spot-margin",
942
+ access: "signed",
943
+ operation: "write",
944
+ riskLevel: "high",
945
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, volume: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, price: { type: "string" }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "volume", "side", "type"], additionalProperties: false },
946
+ errorCodes: writeErrors,
947
+ validate: validateMarginOrder,
948
+ handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", input, signed(context)),
949
+ writeSummary: (input) => {
950
+ const value = input;
951
+ return { action: "margin_place_order", symbol: value.symbol, side: value.side, type: value.type, volume: value.volume, price: value.price ?? null, isolated: value.isolated ?? false, newClientOrderId: value.newClientOrderId };
952
+ }
953
+ },
954
+ {
955
+ name: "margin_cancel_order",
956
+ title: "Cancel Margin Order",
957
+ description: "Cancel one v2 margin order after confirmation.",
958
+ cliPath: ["margin", "order", "cancel"],
959
+ module: "spot-margin",
960
+ access: "signed",
961
+ operation: "write",
962
+ riskLevel: "high",
963
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orderId: { type: "string" }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "orderId"], additionalProperties: false },
964
+ errorCodes: writeErrors,
965
+ validate: validateMarginOrderLookup,
966
+ handler: (input, context) => context.api.signedPost("/sapi/v2/margin/cancel", input, signed(context)),
967
+ writeSummary: (input) => {
968
+ const value = input;
969
+ return { action: "margin_cancel_order", symbol: value.symbol, orderId: value.orderId, isolated: value.isolated ?? false, newClientOrderId: value.newClientOrderId ?? null };
970
+ }
971
+ }
972
+ ];
973
+
974
+ // ../core/src/tools/order-tools.ts
975
+ import { randomUUID as randomUUID3 } from "crypto";
976
+ var signedReadErrors2 = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"];
977
+ var writeErrors2 = [...signedReadErrors2, "AI_HUB_WRITE_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_EXPIRED", "AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "AI_HUB_CONFIRMATION_NOT_FOUND"];
978
+ var decimal = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
979
+ function positiveDecimal2(value, name) {
980
+ const raw = requiredString(value, name);
981
+ if (!decimal.test(raw) || !/[1-9]/.test(raw.replace(".", ""))) {
982
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be a positive decimal string.`);
983
+ }
984
+ return raw;
985
+ }
986
+ function signed2(context) {
987
+ if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
988
+ return context.credentials;
989
+ }
990
+ function orderSide(value) {
991
+ const side = requiredString(value, "side").toUpperCase();
992
+ if (side !== "BUY" && side !== "SELL") throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "side must be BUY or SELL.");
993
+ return side;
994
+ }
995
+ function orderType(value) {
996
+ const type = requiredString(value, "type").toUpperCase();
997
+ if (type !== "LIMIT" && type !== "MARKET") throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "type must be LIMIT or MARKET.");
998
+ return type;
999
+ }
1000
+ function clientOrderId(value) {
1001
+ const supplied = optionalString(value, "newClientOrderId");
1002
+ if (supplied) return supplied;
1003
+ return `agent_${randomUUID3().replaceAll("-", "").slice(0, 24)}`;
1004
+ }
1005
+ function validatePlaceOrder(input) {
1006
+ const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
1007
+ const type = orderType(value);
1008
+ const price = optionalString(value, "price");
1009
+ if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
1010
+ if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
1011
+ const timeInForce = optionalString(value, "timeInForce")?.toUpperCase();
1012
+ if (timeInForce && !["GTC", "IOC", "FOK"].includes(timeInForce)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "timeInForce must be GTC, IOC, or FOK.");
1013
+ return {
1014
+ symbol: requiredString(value, "symbol"),
1015
+ volume: positiveDecimal2(value, "volume"),
1016
+ side: orderSide(value),
1017
+ type,
1018
+ ...price ? { price } : {},
1019
+ ...timeInForce ? { timeInForce } : {},
1020
+ newClientOrderId: clientOrderId(value),
1021
+ ...optionalString(value, "recvWindow") ? { recvWindow: optionalString(value, "recvWindow") } : {}
1022
+ };
1023
+ }
1024
+ function validateCancelOrder(input) {
1025
+ const value = strictObject(input, ["symbol", "orderId", "newClientOrderId"]);
1026
+ return { symbol: requiredString(value, "symbol"), orderId: requiredString(value, "orderId"), ...optionalString(value, "newClientOrderId") ? { newClientOrderId: optionalString(value, "newClientOrderId") } : {} };
1027
+ }
1028
+ function validateTestOrder(input) {
1029
+ const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "newClientOrderId", "recvWindow"]);
1030
+ const type = orderType(value);
1031
+ const price = optionalString(value, "price");
1032
+ if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
1033
+ if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
1034
+ 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") } : {} };
1035
+ }
1036
+ function validateBatchOrders(input) {
1037
+ const value = strictObject(input, ["symbol", "orders"]);
1038
+ const orders = value.orders;
1039
+ 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.");
1040
+ const normalized = orders.map((item, index) => {
1041
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}] must be an object.`);
1042
+ const order = item;
1043
+ const price = optionalString(order, "price");
1044
+ const volume = positiveDecimal2(order, "volume");
1045
+ const side = orderSide(order);
1046
+ const batchType = requiredEnum(order, "batchType", ["LIMIT", "MARKET"]);
1047
+ if (batchType === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}].price is required for LIMIT.`);
1048
+ if (batchType === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}].price is not allowed for MARKET.`);
1049
+ return { volume, side, batchType, ...price ? { price } : {} };
1050
+ });
1051
+ return { symbol: requiredString(value, "symbol"), orders: normalized };
1052
+ }
1053
+ function validateBatchCancel(input) {
1054
+ const value = strictObject(input, ["symbol", "orderIds"]);
1055
+ if (!Array.isArray(value.orderIds) || value.orderIds.length < 1 || value.orderIds.length > 10 || value.orderIds.some((id) => typeof id !== "string" || !id.trim())) {
1056
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "orderIds must contain between 1 and 10 non-empty order ID strings.");
1057
+ }
1058
+ return { symbol: requiredString(value, "symbol"), orderIds: value.orderIds.map((id) => id.trim()) };
1059
+ }
1060
+ var orderTools = [
1061
+ {
1062
+ name: "spot_test_order",
1063
+ title: "Test Spot Order",
1064
+ description: "Validate one LIMIT or MARKET spot order without sending it to the matching engine.",
1065
+ cliPath: ["spot", "order", "test"],
1066
+ module: "spot-order",
1067
+ access: "signed",
1068
+ operation: "read",
1069
+ riskLevel: "low",
1070
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, volume: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, price: { type: "string" }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "volume", "side", "type"], additionalProperties: false },
1071
+ errorCodes: signedReadErrors2,
1072
+ validate: validateTestOrder,
1073
+ handler: (input, context) => context.api.signedPost("/sapi/v2/order/test", input, signed2(context))
1074
+ },
1075
+ {
1076
+ name: "spot_get_order",
1077
+ title: "Get Spot Order",
1078
+ description: "Get one signed spot order by symbol and order ID.",
1079
+ cliPath: ["spot", "order", "get"],
1080
+ module: "spot-order",
1081
+ access: "signed",
1082
+ operation: "read",
1083
+ riskLevel: "low",
1084
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orderId: { type: "string" }, newClientOrderId: { type: "string" } }, required: ["symbol", "orderId"], additionalProperties: false },
1085
+ errorCodes: signedReadErrors2,
1086
+ validate: (input) => {
1087
+ const value = strictObject(input, ["symbol", "orderId", "newClientOrderId"]);
1088
+ return { symbol: requiredString(value, "symbol"), orderId: requiredString(value, "orderId"), newClientOrderId: optionalString(value, "newClientOrderId") };
1089
+ },
1090
+ handler: (input, context) => context.api.getOrder(input, signed2(context))
1091
+ },
1092
+ {
1093
+ name: "spot_batch_place_orders",
1094
+ title: "Batch Place Spot Orders",
1095
+ description: "Create up to 10 spot orders after confirmation.",
1096
+ cliPath: ["spot", "order", "batch-place"],
1097
+ module: "spot-order",
1098
+ access: "signed",
1099
+ operation: "write",
1100
+ riskLevel: "high",
1101
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orders: { type: "array" } }, required: ["symbol", "orders"], additionalProperties: false },
1102
+ errorCodes: writeErrors2,
1103
+ validate: validateBatchOrders,
1104
+ handler: (input, context) => context.api.signedPost("/sapi/v2/batchOrders", input, signed2(context)),
1105
+ writeSummary: (input) => {
1106
+ const value = input;
1107
+ return { action: "batch_place_orders", symbol: value.symbol, orderCount: value.orders.length, orders: value.orders };
1108
+ }
1109
+ },
1110
+ {
1111
+ name: "spot_batch_cancel_orders",
1112
+ title: "Batch Cancel Spot Orders",
1113
+ description: "Cancel up to 10 spot orders after confirmation.",
1114
+ cliPath: ["spot", "order", "batch-cancel"],
1115
+ module: "spot-order",
1116
+ access: "signed",
1117
+ operation: "write",
1118
+ riskLevel: "high",
1119
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orderIds: { type: "array" } }, required: ["symbol", "orderIds"], additionalProperties: false },
1120
+ errorCodes: writeErrors2,
1121
+ validate: validateBatchCancel,
1122
+ handler: (input, context) => context.api.signedPost("/sapi/v2/batchCancel", input, signed2(context)),
1123
+ writeSummary: (input) => {
1124
+ const value = input;
1125
+ return { action: "batch_cancel_orders", symbol: value.symbol, orderIds: value.orderIds };
1126
+ }
1127
+ },
1128
+ {
1129
+ name: "spot_get_open_orders",
1130
+ title: "Get Open Spot Orders",
1131
+ description: "Get current signed spot orders.",
1132
+ cliPath: ["spot", "order", "open"],
1133
+ module: "spot-order",
1134
+ access: "signed",
1135
+ operation: "read",
1136
+ riskLevel: "low",
1137
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 } }, additionalProperties: false },
1138
+ errorCodes: signedReadErrors2,
1139
+ validate: (input) => {
1140
+ const value = strictObject(input, ["symbol", "limit"]);
1141
+ return { symbol: optionalString(value, "symbol"), limit: optionalInteger(value, "limit", 100, 1, 1e3) };
1142
+ },
1143
+ handler: (input, context) => context.api.getOpenOrders(input, signed2(context))
1144
+ },
1145
+ {
1146
+ name: "spot_get_fills",
1147
+ title: "Get Spot Fills",
1148
+ description: "Get signed spot fills for one symbol.",
1149
+ cliPath: ["spot", "order", "fills"],
1150
+ module: "spot-order",
1151
+ access: "signed",
1152
+ operation: "read",
1153
+ riskLevel: "low",
1154
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 }, fromId: { type: "string" } }, required: ["symbol"], additionalProperties: false },
1155
+ errorCodes: signedReadErrors2,
1156
+ validate: (input) => {
1157
+ const value = strictObject(input, ["symbol", "limit", "fromId"]);
1158
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 100, 1, 1e3), fromId: optionalString(value, "fromId") };
1159
+ },
1160
+ handler: (input, context) => context.api.getMyTrades(input, signed2(context))
1161
+ },
1162
+ {
1163
+ name: "spot_place_order",
1164
+ title: "Place Spot Order",
1165
+ description: "Create one LIMIT or MARKET spot order after confirmation.",
1166
+ cliPath: ["spot", "order", "place"],
1167
+ module: "spot-order",
1168
+ access: "signed",
1169
+ operation: "write",
1170
+ riskLevel: "high",
1171
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, volume: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, price: { type: "string" }, timeInForce: { type: "string", enum: ["GTC", "IOC", "FOK"] }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "volume", "side", "type"], additionalProperties: false },
1172
+ errorCodes: writeErrors2,
1173
+ validate: validatePlaceOrder,
1174
+ handler: (input, context) => context.api.placeOrder(input, signed2(context)),
1175
+ writeSummary: (input) => {
1176
+ const value = input;
1177
+ return { action: "place_order", symbol: value.symbol, side: value.side, type: value.type, volume: value.volume, price: value.price ?? null, timeInForce: value.timeInForce ?? null, newClientOrderId: value.newClientOrderId };
1178
+ }
1179
+ },
1180
+ {
1181
+ name: "spot_cancel_order",
1182
+ title: "Cancel Spot Order",
1183
+ description: "Cancel one spot order after confirmation.",
1184
+ cliPath: ["spot", "order", "cancel"],
1185
+ module: "spot-order",
1186
+ access: "signed",
1187
+ operation: "write",
1188
+ riskLevel: "high",
1189
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orderId: { type: "string" }, newClientOrderId: { type: "string" } }, required: ["symbol", "orderId"], additionalProperties: false },
1190
+ errorCodes: writeErrors2,
1191
+ validate: validateCancelOrder,
1192
+ handler: (input, context) => context.api.cancelOrder(input, signed2(context)),
1193
+ writeSummary: (input) => {
1194
+ const value = input;
1195
+ return { action: "cancel_order", symbol: value.symbol, orderId: value.orderId, newClientOrderId: value.newClientOrderId ?? null };
1196
+ }
1197
+ }
1198
+ ];
1199
+
1200
+ // ../core/src/tools/sub-account-tools.ts
1201
+ function fields(input, allowed, required = []) {
1202
+ const value = strictObject(input, allowed);
1203
+ const result2 = {};
1204
+ for (const name of required) result2[name] = requiredString(value, name);
1205
+ for (const name of allowed) if (!required.includes(name) && optionalString(value, name)) result2[name] = optionalString(value, name);
1206
+ return result2;
1207
+ }
1208
+ function pages(value) {
1209
+ return { page: optionalPositiveInteger(value, "page", 1), pageSize: optionalPositiveInteger(value, "pageSize", 20, 100) };
1210
+ }
1211
+ function transfer(value, requireSubUid) {
1212
+ const result2 = { coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount") };
1213
+ if (requireSubUid) result2.subUid = requiredString(value, "subUid");
1214
+ return result2;
1215
+ }
1216
+ var subAccountTools = [
1217
+ {
1218
+ name: "sub_account_list",
1219
+ title: "List Sub-accounts",
1220
+ description: "Get enabled sub-accounts under the main account.",
1221
+ cliPath: ["sub-account", "list"],
1222
+ module: "spot-sub-account",
1223
+ access: "signed",
1224
+ operation: "read",
1225
+ riskLevel: "low",
1226
+ inputSchema: { type: "object", additionalProperties: false },
1227
+ errorCodes: signedReadErrors,
1228
+ validate: (input) => {
1229
+ strictObject(input, []);
1230
+ return {};
1231
+ },
1232
+ handler: (_input, context) => context.api.signedPost("/sapi/v1/sub_user/get_sub_user_List", {}, signed(context))
1233
+ },
1234
+ {
1235
+ name: "sub_account_create",
1236
+ title: "Create Sub-account",
1237
+ description: "Create a virtual sub-account after confirmation.",
1238
+ cliPath: ["sub-account", "create"],
1239
+ module: "spot-sub-account",
1240
+ access: "signed",
1241
+ operation: "write",
1242
+ riskLevel: "high",
1243
+ inputSchema: { type: "object", properties: { subUserEmail: { type: "string", maxLength: 5 } }, required: ["subUserEmail"], additionalProperties: false },
1244
+ errorCodes: writeErrors,
1245
+ validate: (input) => {
1246
+ const value = fields(input, ["subUserEmail"], ["subUserEmail"]);
1247
+ if (String(value.subUserEmail).length > 5) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "subUserEmail must be at most 5 characters.");
1248
+ return value;
1249
+ },
1250
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/create_sub_user", input, signed(context)),
1251
+ writeSummary: (input) => ({ action: "create_sub_account", subUserEmail: input.subUserEmail })
1252
+ },
1253
+ {
1254
+ name: "sub_account_update_trading_status",
1255
+ title: "Update Sub-account Trading Status",
1256
+ description: "Enable or disable a sub-account capability after confirmation.",
1257
+ cliPath: ["sub-account", "set-trading-status"],
1258
+ module: "spot-sub-account",
1259
+ access: "signed",
1260
+ operation: "write",
1261
+ riskLevel: "high",
1262
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, type: { type: "string", enum: ["lever", "etf", "deposit"] }, status: { type: "string", enum: ["0", "1"] } }, required: ["subUid", "type", "status"], additionalProperties: false },
1263
+ errorCodes: writeErrors,
1264
+ validate: (input) => {
1265
+ const value = fields(input, ["subUid", "type", "status"], ["subUid", "type", "status"]);
1266
+ if (!["lever", "etf", "deposit"].includes(String(value.type)) || !["0", "1"].includes(String(value.status))) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "type or status is invalid.");
1267
+ return value;
1268
+ },
1269
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/update_trade_status", input, signed(context)),
1270
+ writeSummary: (input) => ({ action: "update_sub_account_trading_status", ...input })
1271
+ },
1272
+ {
1273
+ name: "sub_account_get_api_key_ips",
1274
+ title: "Get Sub-account API Key IPs",
1275
+ description: "Get a sub-account API key whitelist.",
1276
+ cliPath: ["sub-account", "api-key", "list"],
1277
+ module: "spot-sub-account",
1278
+ access: "signed",
1279
+ operation: "read",
1280
+ riskLevel: "low",
1281
+ inputSchema: { type: "object", properties: { subUid: { type: "string" } }, required: ["subUid"], additionalProperties: false },
1282
+ errorCodes: signedReadErrors,
1283
+ validate: (input) => fields(input, ["subUid"], ["subUid"]),
1284
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/sub_account_api/list", input, signed(context))
1285
+ },
1286
+ {
1287
+ name: "sub_account_update_api_key_ips",
1288
+ title: "Update Sub-account API Key IPs",
1289
+ description: "Update a sub-account API key IP whitelist after confirmation.",
1290
+ cliPath: ["sub-account", "api-key", "set-ip"],
1291
+ module: "spot-sub-account",
1292
+ access: "signed",
1293
+ operation: "write",
1294
+ riskLevel: "high",
1295
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, subAccountApiKey: { type: "string" }, status: { type: "string", enum: ["1", "2"] }, ipAddress: { type: "string" } }, required: ["subUid", "subAccountApiKey", "status"], additionalProperties: false },
1296
+ errorCodes: writeErrors,
1297
+ validate: (input) => {
1298
+ const value = fields(input, ["subUid", "subAccountApiKey", "status", "ipAddress"], ["subUid", "subAccountApiKey", "status"]);
1299
+ if (!["1", "2"].includes(String(value.status))) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "status must be 1 or 2.");
1300
+ if (value.status === "2" && !value.ipAddress) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "ipAddress is required when status is 2.");
1301
+ return value;
1302
+ },
1303
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/sub_account_api/update_ip", input, signed(context)),
1304
+ writeSummary: (input) => ({ action: "update_sub_account_api_key_ips", ...input })
1305
+ },
1306
+ {
1307
+ name: "sub_account_delete_api_key",
1308
+ title: "Delete Sub-account API Key",
1309
+ description: "Delete a sub-account API key after confirmation.",
1310
+ cliPath: ["sub-account", "api-key", "delete"],
1311
+ module: "spot-sub-account",
1312
+ access: "signed",
1313
+ operation: "write",
1314
+ riskLevel: "high",
1315
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, subAccountApiKey: { type: "string" } }, required: ["subUid", "subAccountApiKey"], additionalProperties: false },
1316
+ errorCodes: writeErrors,
1317
+ validate: (input) => fields(input, ["subUid", "subAccountApiKey"], ["subUid", "subAccountApiKey"]),
1318
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/sub_account_api/delete", input, signed(context)),
1319
+ writeSummary: (input) => ({ action: "delete_sub_account_api_key", ...input })
1320
+ },
1321
+ {
1322
+ name: "sub_account_get_assets",
1323
+ title: "Get Sub-account Assets",
1324
+ description: "Query sub-account assets by account type.",
1325
+ cliPath: ["sub-account", "assets"],
1326
+ module: "spot-sub-account",
1327
+ access: "signed",
1328
+ operation: "read",
1329
+ riskLevel: "low",
1330
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, accountType: { type: "string" }, type: { type: "string" } }, required: ["subUid", "accountType"], additionalProperties: false },
1331
+ errorCodes: signedReadErrors,
1332
+ validate: (input) => fields(input, ["subUid", "accountType", "type"], ["subUid", "accountType"]),
1333
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/asset/account", input, signed(context))
1334
+ },
1335
+ {
1336
+ name: "sub_account_root_transfer",
1337
+ title: "Transfer Between Root and Sub-account",
1338
+ description: "Transfer assets between a root and sub-account after confirmation.",
1339
+ cliPath: ["sub-account", "root-transfer"],
1340
+ module: "spot-sub-account",
1341
+ access: "signed",
1342
+ operation: "write",
1343
+ riskLevel: "high",
1344
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, coinSymbol: { type: "string" }, amount: { type: "string" }, type: { type: "string" } }, required: ["subUid", "coinSymbol", "amount", "type"], additionalProperties: false },
1345
+ errorCodes: writeErrors,
1346
+ validate: (input) => {
1347
+ const value = strictObject(input, ["subUid", "coinSymbol", "amount", "type"]);
1348
+ return { ...transfer(value, true), type: requiredString(value, "type") };
1349
+ },
1350
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/asset/root_transfer", input, signed(context)),
1351
+ writeSummary: (input) => ({ action: "sub_account_root_transfer", ...input })
1352
+ },
1353
+ {
1354
+ name: "sub_account_get_root_transfer_history",
1355
+ title: "Get Root/Sub Transfer History",
1356
+ description: "Query root and sub-account transfer history.",
1357
+ cliPath: ["sub-account", "root-transfer-history"],
1358
+ module: "spot-sub-account",
1359
+ access: "signed",
1360
+ operation: "read",
1361
+ riskLevel: "low",
1362
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["subUid", "coinSymbol"], additionalProperties: false },
1363
+ errorCodes: signedReadErrors,
1364
+ validate: (input) => {
1365
+ const value = strictObject(input, ["subUid", "coinSymbol", "page", "pageSize"]);
1366
+ return { subUid: requiredString(value, "subUid"), coinSymbol: requiredString(value, "coinSymbol"), ...pages(value) };
1367
+ },
1368
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/asset/root_transfer_query", input, signed(context))
1369
+ },
1370
+ {
1371
+ name: "sub_account_internal_transfer",
1372
+ title: "Transfer Within Sub-account",
1373
+ description: "Transfer assets between a sub-account's internal account types after confirmation.",
1374
+ cliPath: ["sub-account", "internal-transfer"],
1375
+ module: "spot-sub-account",
1376
+ access: "signed",
1377
+ operation: "write",
1378
+ riskLevel: "high",
1379
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, coinSymbol: { type: "string" }, amount: { type: "string" }, type: { type: "string" }, accountType: { type: "string" }, symbol: { type: "string" } }, required: ["subUid", "coinSymbol", "amount", "type", "accountType"], additionalProperties: false },
1380
+ errorCodes: writeErrors,
1381
+ validate: (input) => {
1382
+ const value = strictObject(input, ["subUid", "coinSymbol", "amount", "type", "accountType", "symbol"]);
1383
+ return { ...transfer(value, true), type: requiredString(value, "type"), accountType: requiredString(value, "accountType"), ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {} };
1384
+ },
1385
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/asset/transfer", input, signed(context)),
1386
+ writeSummary: (input) => ({ action: "sub_account_internal_transfer", ...input })
1387
+ },
1388
+ {
1389
+ name: "sub_account_get_internal_transfer_history",
1390
+ title: "Get Sub-account Internal Transfer History",
1391
+ description: "Query internal sub-account transfer history.",
1392
+ cliPath: ["sub-account", "internal-transfer-history"],
1393
+ module: "spot-sub-account",
1394
+ access: "signed",
1395
+ operation: "read",
1396
+ riskLevel: "low",
1397
+ inputSchema: { type: "object", properties: { subUid: { type: "string" }, type: { type: "string" }, accountType: { type: "string" }, coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["subUid", "type", "accountType", "coinSymbol"], additionalProperties: false },
1398
+ errorCodes: signedReadErrors,
1399
+ validate: (input) => {
1400
+ const value = strictObject(input, ["subUid", "type", "accountType", "coinSymbol", "page", "pageSize"]);
1401
+ return { subUid: requiredString(value, "subUid"), type: requiredString(value, "type"), accountType: requiredString(value, "accountType"), coinSymbol: requiredString(value, "coinSymbol"), ...pages(value) };
1402
+ },
1403
+ handler: (input, context) => context.api.signedPost("/sapi/v1/sub_user/asset/transfer_query", input, signed(context))
1404
+ },
1405
+ {
1406
+ name: "sub_account_transfer_to_parent",
1407
+ title: "Transfer From Sub-account to Parent",
1408
+ description: "Transfer assets to the parent account after confirmation.",
1409
+ cliPath: ["sub-account", "transfer-to-parent"],
1410
+ module: "spot-sub-account",
1411
+ access: "signed",
1412
+ operation: "write",
1413
+ riskLevel: "high",
1414
+ inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, amount: { type: "string" } }, required: ["coinSymbol", "amount"], additionalProperties: false },
1415
+ errorCodes: writeErrors,
1416
+ validate: (input) => {
1417
+ const value = strictObject(input, ["coinSymbol", "amount"]);
1418
+ return transfer(value, false);
1419
+ },
1420
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/subaccount/transfer", input, signed(context)),
1421
+ writeSummary: (input) => ({ action: "sub_account_transfer_to_parent", ...input })
1422
+ },
1423
+ {
1424
+ name: "sub_account_get_parent_transfer_history",
1425
+ title: "Get Parent Transfer History",
1426
+ description: "Query transfers between this sub-account and its parent.",
1427
+ cliPath: ["sub-account", "parent-transfer-history"],
1428
+ module: "spot-sub-account",
1429
+ access: "signed",
1430
+ operation: "read",
1431
+ riskLevel: "low",
1432
+ inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["coinSymbol"], additionalProperties: false },
1433
+ errorCodes: signedReadErrors,
1434
+ validate: (input) => {
1435
+ const value = strictObject(input, ["coinSymbol", "page", "pageSize"]);
1436
+ return { coinSymbol: requiredString(value, "coinSymbol"), ...pages(value) };
1437
+ },
1438
+ handler: (input, context) => context.api.signedPost("/sapi/v1/asset/subaccount/transfer_query", input, signed(context))
1439
+ }
1440
+ ];
1441
+
1442
+ // ../core/src/tools/tool-registry.ts
1443
+ var allTools = [...marketTools, ...accountTools, ...orderTools, ...marginTools, ...assetTools, ...subAccountTools];
1444
+ var ToolRegistry = class {
1445
+ toolsByName = /* @__PURE__ */ new Map();
1446
+ toolsByCliPath = /* @__PURE__ */ new Map();
1447
+ constructor(tools = allTools) {
1448
+ for (const tool of tools) {
1449
+ const path = tool.cliPath.join(" ");
1450
+ if (this.toolsByName.has(tool.name) || this.toolsByCliPath.has(path)) {
1451
+ throw new AiHubError("AI_HUB_TOOL_DUPLICATE", `Duplicate tool registration: ${tool.name}.`);
1452
+ }
1453
+ this.toolsByName.set(tool.name, tool);
1454
+ this.toolsByCliPath.set(path, tool);
1455
+ }
1456
+ }
1457
+ list(options = {}) {
1458
+ return [...this.toolsByName.values()].filter((tool) => !options.readOnly || tool.operation === "read");
1459
+ }
1460
+ byName(name, options = {}) {
1461
+ const tool = this.toolsByName.get(name);
1462
+ if (!tool || options.readOnly && tool.operation !== "read") {
1463
+ throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${name}" is not available in this server session.`);
1464
+ }
1465
+ return tool;
1466
+ }
1467
+ byCliPath(path, options = {}) {
1468
+ const tool = this.toolsByCliPath.get(path.join(" "));
1469
+ if (!tool || options.readOnly && tool.operation !== "read") {
1470
+ throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Command "${path.join(" ")}" is not available.`);
1471
+ }
1472
+ return tool;
1473
+ }
1474
+ async execute(name, input, context, options = {}) {
1475
+ const tool = this.byName(name, options);
1476
+ if (tool.operation === "write") throw new AiHubError("AI_HUB_WRITE_CONFIRMATION_REQUIRED", `Tool "${name}" must be executed through confirmation.`);
1477
+ const validInput = tool.validate(input);
1478
+ return tool.handler(validInput, context);
1479
+ }
1480
+ prepareWrite(name, input, context, confirmations) {
1481
+ const tool = this.byName(name);
1482
+ if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
1483
+ const validInput = tool.validate(input);
1484
+ return confirmations.prepare({
1485
+ action: tool.name,
1486
+ payload: validInput,
1487
+ context: confirmationContext(context),
1488
+ summary: tool.writeSummary(validInput)
1489
+ });
1490
+ }
1491
+ async executeConfirmed(action, payload, context) {
1492
+ const tool = this.byName(action);
1493
+ if (tool.operation !== "write") throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${action}" is not a write Tool.`);
1494
+ const validInput = tool.validate(payload);
1495
+ return tool.handler(validInput, context);
1496
+ }
1497
+ capabilities(context, options = {}) {
1498
+ return this.list(options).map((tool) => ({
1499
+ name: tool.name,
1500
+ module: tool.module,
1501
+ operation: tool.operation,
1502
+ riskLevel: tool.riskLevel,
1503
+ status: tool.access === "signed" && !context.credentials ? "requires_auth" : "enabled"
1504
+ }));
1505
+ }
1506
+ };
1507
+ function createToolRegistry() {
1508
+ return new ToolRegistry();
1509
+ }
1510
+
1511
+ // ../core/src/tools/write-executor.ts
1512
+ var ToolWriteExecutor = class {
1513
+ constructor(registry, confirmations = new ConfirmationService()) {
1514
+ this.registry = registry;
1515
+ this.confirmations = confirmations;
1516
+ }
1517
+ registry;
1518
+ confirmations;
1519
+ prepare(name, input, context) {
1520
+ return this.registry.prepareWrite(name, input, context, this.confirmations);
1521
+ }
1522
+ async confirm(confirmationId, userConfirmation, context) {
1523
+ const pending = this.confirmations.confirm(confirmationId, userConfirmation, confirmationContext(context));
1524
+ try {
1525
+ return await this.registry.executeConfirmed(pending.action, pending.payload, context);
1526
+ } catch (error) {
1527
+ if (error instanceof AiHubError && error.code === "AI_HUB_OPENAPI_NETWORK_ERROR") {
1528
+ throw new AiHubError("AI_HUB_WRITE_RESULT_UNKNOWN", "The request may have reached OpenAPI. Do not retry automatically; query by client order ID before taking another action.");
1529
+ }
1530
+ throw error;
1531
+ }
1532
+ }
1533
+ };
1534
+
1535
+ // src/server.ts
1536
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1537
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1538
+ var CAPABILITIES_TOOL = "system_get_capabilities";
1539
+ var CONFIRM_ACTION_TOOL = "confirm_action";
1540
+ function result(data) {
1541
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1542
+ }
1543
+ function errorResult(error) {
1544
+ const payload = error instanceof AiHubError ? { code: error.code, message: error.message } : { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
1545
+ return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, ...payload }) }] };
1546
+ }
1547
+ function toMcpTool(tool) {
1548
+ return {
1549
+ name: tool.name,
1550
+ title: tool.title,
1551
+ description: tool.description,
1552
+ inputSchema: tool.inputSchema,
1553
+ annotations: {
1554
+ readOnlyHint: tool.operation === "read",
1555
+ destructiveHint: tool.riskLevel === "high",
1556
+ idempotentHint: tool.operation === "read",
1557
+ openWorldHint: true
1558
+ }
1559
+ };
1560
+ }
1561
+ function prepareToolName(tool) {
1562
+ const [prefix, ...rest] = tool.name.split("_");
1563
+ return `${prefix ?? "spot"}_prepare_${rest.join("_")}`;
1564
+ }
1565
+ function toPrepareMcpTool(tool) {
1566
+ return {
1567
+ ...toMcpTool(tool),
1568
+ name: prepareToolName(tool),
1569
+ title: `Prepare ${tool.title}`,
1570
+ description: `Validate and preview this state-changing request. It does not call OpenAPI. Stop after this call and wait for a NEW explicit user confirmation message before calling ${CONFIRM_ACTION_TOOL}.`,
1571
+ annotations: { readOnlyHint: false, destructiveHint: tool.riskLevel === "high", idempotentHint: true, openWorldHint: false }
1572
+ };
1573
+ }
1574
+ function createServer(profileName, readOnly) {
1575
+ const registry = createToolRegistry();
1576
+ const writeExecutor = new ToolWriteExecutor(registry);
1577
+ const server = new Server(
1578
+ { name: "ai-hub-agent-trade", version: "0.1.0" },
1579
+ {
1580
+ capabilities: { tools: {} },
1581
+ 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."
1582
+ }
1583
+ );
1584
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1585
+ tools: [
1586
+ {
1587
+ name: CAPABILITIES_TOOL,
1588
+ title: "Server Capabilities Snapshot",
1589
+ description: "Return the available local profile and Tool Registry capability snapshot.",
1590
+ inputSchema: { type: "object", additionalProperties: false },
1591
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1592
+ },
1593
+ ...registry.list({ readOnly }).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
1594
+ ...readOnly ? [] : [{
1595
+ name: CONFIRM_ACTION_TOOL,
1596
+ title: "Confirm Prepared Action",
1597
+ description: "Execute one previously prepared state-changing action exactly once, only after a new explicit user confirmation message received after the preview.",
1598
+ inputSchema: { type: "object", properties: { confirmationId: { type: "string" }, userConfirmation: { type: "string", minLength: 1, description: "The new explicit user confirmation message received after the prepare preview. Do not generate or infer this text." } }, required: ["confirmationId", "userConfirmation"], additionalProperties: false },
1599
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
1600
+ }]
1601
+ ]
1602
+ }));
1603
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1604
+ try {
1605
+ const context = await createToolExecutionContext(profileName);
1606
+ if (request.params.name === CAPABILITIES_TOOL) {
1607
+ return result({
1608
+ ok: true,
1609
+ data: {
1610
+ profile: { name: context.profile.name, host: new URL(context.profile.openApiBaseUrl).host, configVersion: context.profile.configVersion },
1611
+ readOnly,
1612
+ capabilities: registry.capabilities(context, { readOnly })
1613
+ }
1614
+ });
1615
+ }
1616
+ if (request.params.name === CONFIRM_ACTION_TOOL) {
1617
+ const input = request.params.arguments ?? {};
1618
+ if (typeof input.confirmationId !== "string" || typeof input.userConfirmation !== "string" || !input.userConfirmation.trim()) {
1619
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "confirmationId and a non-empty userConfirmation are required.");
1620
+ }
1621
+ return result({ ok: true, data: await writeExecutor.confirm(input.confirmationId, input.userConfirmation, context) });
1622
+ }
1623
+ const preparedTool = registry.list({ readOnly: false }).find((tool) => tool.operation === "write" && prepareToolName(tool) === request.params.name);
1624
+ if (preparedTool) {
1625
+ return result({ ok: true, data: writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
1626
+ }
1627
+ const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
1628
+ return result({ ok: true, data });
1629
+ } catch (error) {
1630
+ return errorResult(error);
1631
+ }
1632
+ });
1633
+ return server;
1634
+ }
1635
+
1636
+ // src/index.ts
1637
+ function readOption(args, option) {
1638
+ const index = args.indexOf(option);
1639
+ if (index < 0) return void 0;
1640
+ const value = args[index + 1];
1641
+ if (!value || value.startsWith("--")) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${option} requires a value.`);
1642
+ return value;
1643
+ }
1644
+ async function main(argv) {
1645
+ const profileName = readOption(argv, "--profile");
1646
+ const readOnly = argv.includes("--read-only");
1647
+ const server = createServer(profileName, readOnly);
1648
+ await server.connect(new StdioServerTransport());
1649
+ }
1650
+ main(process.argv.slice(2)).catch((error) => {
1651
+ const payload = error instanceof AiHubError ? { code: error.code, message: error.message } : { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
1652
+ process.stderr.write(`${JSON.stringify(payload)}
1653
+ `);
1654
+ process.exitCode = 1;
1655
+ });
1656
+ export {
1657
+ main
1658
+ };