@aihubspot/agent-trade-mcp 0.1.9 → 0.1.10
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 +65 -49
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,9 @@ function loadStoredCredentials(apiKey, secretKey) {
|
|
|
56
56
|
|
|
57
57
|
// ../core/src/confirmation.ts
|
|
58
58
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
59
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
60
|
+
import { homedir } from "os";
|
|
61
|
+
import { join } from "path";
|
|
59
62
|
function stableJson(value) {
|
|
60
63
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
61
64
|
if (value && typeof value === "object") {
|
|
@@ -66,6 +69,37 @@ function stableJson(value) {
|
|
|
66
69
|
function hash(input) {
|
|
67
70
|
return createHash2("sha256").update(stableJson({ action: input.action, payload: input.payload, context: input.context })).digest("hex");
|
|
68
71
|
}
|
|
72
|
+
function createPending(input, ttlMs, now) {
|
|
73
|
+
return {
|
|
74
|
+
...input,
|
|
75
|
+
confirmationId: randomUUID(),
|
|
76
|
+
requestHash: hash(input),
|
|
77
|
+
expiresAtMs: now() + ttlMs
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function preparedAction(pending) {
|
|
81
|
+
return {
|
|
82
|
+
confirmationId: pending.confirmationId,
|
|
83
|
+
expiresAt: new Date(pending.expiresAtMs).toISOString(),
|
|
84
|
+
requestHash: pending.requestHash,
|
|
85
|
+
action: pending.action,
|
|
86
|
+
summary: pending.summary,
|
|
87
|
+
requiresNewUserConfirmation: true,
|
|
88
|
+
nextStep: "Stop and wait for a new explicit user confirmation message. Do not confirm from the same user instruction that prepared this request."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function confirmPending(pending, userConfirmation, context, now) {
|
|
92
|
+
if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
|
|
93
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_REQUIRED", "A non-empty new explicit user confirmation message is required before a state-changing request can execute.");
|
|
94
|
+
}
|
|
95
|
+
if (pending.expiresAtMs < now()) {
|
|
96
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
|
|
97
|
+
}
|
|
98
|
+
if (stableJson(pending.context) !== stableJson(context)) {
|
|
99
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
|
|
100
|
+
}
|
|
101
|
+
return { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
|
|
102
|
+
}
|
|
69
103
|
var ConfirmationService = class {
|
|
70
104
|
constructor(ttlMs = 5 * 60 * 1e3, now = () => Date.now()) {
|
|
71
105
|
this.ttlMs = ttlMs;
|
|
@@ -75,19 +109,9 @@ var ConfirmationService = class {
|
|
|
75
109
|
now;
|
|
76
110
|
actions = /* @__PURE__ */ new Map();
|
|
77
111
|
prepare(input) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.actions.set(confirmationId, { ...input, confirmationId, requestHash, expiresAtMs, consumed: false });
|
|
82
|
-
return {
|
|
83
|
-
confirmationId,
|
|
84
|
-
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
85
|
-
requestHash,
|
|
86
|
-
action: input.action,
|
|
87
|
-
summary: input.summary,
|
|
88
|
-
requiresNewUserConfirmation: true,
|
|
89
|
-
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."
|
|
90
|
-
};
|
|
112
|
+
const pending = createPending(input, this.ttlMs, this.now);
|
|
113
|
+
this.actions.set(pending.confirmationId, { ...pending, consumed: false });
|
|
114
|
+
return preparedAction(pending);
|
|
91
115
|
}
|
|
92
116
|
confirm(confirmationId, userConfirmation, context) {
|
|
93
117
|
if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
|
|
@@ -96,17 +120,9 @@ var ConfirmationService = class {
|
|
|
96
120
|
const pending = this.actions.get(confirmationId);
|
|
97
121
|
if (!pending) throw new AiHubError("AI_HUB_CONFIRMATION_NOT_FOUND", "Confirmation was not found or has already been consumed.");
|
|
98
122
|
if (pending.consumed) throw new AiHubError("AI_HUB_CONFIRMATION_CONSUMED", "Confirmation has already been consumed.");
|
|
99
|
-
if (pending.expiresAtMs < this.now()) {
|
|
100
|
-
this.actions.delete(confirmationId);
|
|
101
|
-
throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
|
|
102
|
-
}
|
|
103
|
-
if (stableJson(pending.context) !== stableJson(context)) {
|
|
104
|
-
this.actions.delete(confirmationId);
|
|
105
|
-
throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
|
|
106
|
-
}
|
|
107
123
|
pending.consumed = true;
|
|
108
124
|
this.actions.delete(confirmationId);
|
|
109
|
-
return
|
|
125
|
+
return confirmPending(pending, userConfirmation, context, this.now);
|
|
110
126
|
}
|
|
111
127
|
};
|
|
112
128
|
|
|
@@ -394,16 +410,16 @@ var AiHubSpotApi = class {
|
|
|
394
410
|
// ../core/src/config.ts
|
|
395
411
|
import { createHash as createHash3 } from "crypto";
|
|
396
412
|
import { lookup } from "dns/promises";
|
|
397
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
398
|
-
import { homedir } from "os";
|
|
399
|
-
import { dirname, join } from "path";
|
|
413
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
414
|
+
import { homedir as homedir2 } from "os";
|
|
415
|
+
import { dirname, join as join2 } from "path";
|
|
400
416
|
import { isIP } from "net";
|
|
401
417
|
import { parse, stringify } from "smol-toml";
|
|
402
418
|
var CONFIG_DIRECTORY = ".ai-hub";
|
|
403
419
|
var CONFIG_FILENAME = "config.toml";
|
|
404
420
|
var PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
405
|
-
function configFilePath(home =
|
|
406
|
-
return
|
|
421
|
+
function configFilePath(home = homedir2()) {
|
|
422
|
+
return join2(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
|
|
407
423
|
}
|
|
408
424
|
function validateProfileName(name) {
|
|
409
425
|
if (!PROFILE_NAME.test(name)) {
|
|
@@ -467,7 +483,7 @@ var ConfigStore = class {
|
|
|
467
483
|
filePath;
|
|
468
484
|
async read() {
|
|
469
485
|
try {
|
|
470
|
-
return parseConfig(await
|
|
486
|
+
return parseConfig(await readFile2(this.filePath, "utf8"));
|
|
471
487
|
} catch (error) {
|
|
472
488
|
if (error.code === "ENOENT") return defaultConfig();
|
|
473
489
|
throw error;
|
|
@@ -538,13 +554,13 @@ var ConfigStore = class {
|
|
|
538
554
|
}
|
|
539
555
|
async write(config) {
|
|
540
556
|
const directory = dirname(this.filePath);
|
|
541
|
-
await
|
|
542
|
-
await
|
|
557
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
558
|
+
await chmod2(directory, 448);
|
|
543
559
|
const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
|
|
544
|
-
await
|
|
545
|
-
await
|
|
546
|
-
await
|
|
547
|
-
await
|
|
560
|
+
await writeFile2(temporaryPath, stringify(config), { mode: 384 });
|
|
561
|
+
await chmod2(temporaryPath, 384);
|
|
562
|
+
await rename2(temporaryPath, this.filePath);
|
|
563
|
+
await chmod2(this.filePath, 384);
|
|
548
564
|
}
|
|
549
565
|
};
|
|
550
566
|
|
|
@@ -1235,9 +1251,9 @@ function summarizeKlines(value, symbol, interval) {
|
|
|
1235
1251
|
|
|
1236
1252
|
// ../core/src/tools/symbol-rules.ts
|
|
1237
1253
|
import { createHash as createHash4 } from "crypto";
|
|
1238
|
-
import { chmod as
|
|
1239
|
-
import { homedir as
|
|
1240
|
-
import { join as
|
|
1254
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
1255
|
+
import { homedir as homedir3 } from "os";
|
|
1256
|
+
import { join as join3 } from "path";
|
|
1241
1257
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1242
1258
|
var cache = /* @__PURE__ */ new Map();
|
|
1243
1259
|
var pendingLoads = /* @__PURE__ */ new Map();
|
|
@@ -1248,16 +1264,16 @@ function persistentCacheEnabled() {
|
|
|
1248
1264
|
return process.env.AI_HUB_DISABLE_PERSISTENT_CACHE !== "1";
|
|
1249
1265
|
}
|
|
1250
1266
|
function persistentCacheDirectory() {
|
|
1251
|
-
return process.env.AI_HUB_CACHE_DIR ??
|
|
1267
|
+
return process.env.AI_HUB_CACHE_DIR ?? join3(homedir3(), ".ai-hub", "cache");
|
|
1252
1268
|
}
|
|
1253
1269
|
function persistentCachePath(key) {
|
|
1254
1270
|
const hash2 = createHash4("sha256").update(key).digest("hex");
|
|
1255
|
-
return
|
|
1271
|
+
return join3(persistentCacheDirectory(), `symbols-${hash2}.json`);
|
|
1256
1272
|
}
|
|
1257
1273
|
async function loadPersistentSnapshot(key) {
|
|
1258
1274
|
if (!persistentCacheEnabled()) return void 0;
|
|
1259
1275
|
try {
|
|
1260
|
-
const parsed = JSON.parse(await
|
|
1276
|
+
const parsed = JSON.parse(await readFile3(persistentCachePath(key), "utf8"));
|
|
1261
1277
|
if (typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt) || parsed.expiresAt <= Date.now() || !("response" in parsed)) return void 0;
|
|
1262
1278
|
return { expiresAt: parsed.expiresAt, response: parsed.response, rules: parseRules(parsed.response) };
|
|
1263
1279
|
} catch {
|
|
@@ -1270,14 +1286,14 @@ async function persistSnapshot(key, entry) {
|
|
|
1270
1286
|
const filePath = persistentCachePath(key);
|
|
1271
1287
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1272
1288
|
try {
|
|
1273
|
-
await
|
|
1274
|
-
await
|
|
1275
|
-
await
|
|
1276
|
-
await
|
|
1277
|
-
await
|
|
1278
|
-
await
|
|
1289
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
1290
|
+
await chmod3(directory, 448);
|
|
1291
|
+
await writeFile3(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
|
|
1292
|
+
await chmod3(temporaryPath, 384);
|
|
1293
|
+
await rename3(temporaryPath, filePath);
|
|
1294
|
+
await chmod3(filePath, 384);
|
|
1279
1295
|
} catch {
|
|
1280
|
-
await
|
|
1296
|
+
await rm2(temporaryPath, { force: true }).catch(() => void 0);
|
|
1281
1297
|
}
|
|
1282
1298
|
}
|
|
1283
1299
|
function normalizedSymbol2(symbol) {
|
|
@@ -2511,7 +2527,7 @@ var ToolRegistry = class {
|
|
|
2511
2527
|
if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
|
|
2512
2528
|
const validInput = tool.validate(input);
|
|
2513
2529
|
const preflightInput = tool.preflight ? await tool.preflight(validInput, context) : validInput;
|
|
2514
|
-
return confirmations.prepare({
|
|
2530
|
+
return await confirmations.prepare({
|
|
2515
2531
|
action: tool.name,
|
|
2516
2532
|
payload: preflightInput,
|
|
2517
2533
|
context: confirmationContext(context),
|
|
@@ -2811,7 +2827,7 @@ function createServer(profileName, readOnly) {
|
|
|
2811
2827
|
const registry = createToolRegistry();
|
|
2812
2828
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
2813
2829
|
const server = new Server(
|
|
2814
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
2830
|
+
{ name: "ai-hub-agent-trade", version: "0.1.10" },
|
|
2815
2831
|
{
|
|
2816
2832
|
capabilities: { tools: {} },
|
|
2817
2833
|
instructions: "Every successful read-tool response provides both JSON text and MCP structuredContent in the envelope { ok: true, data: ... }. All read-tool data is normalized: dataType=array means use data.items and data.count; dataType=object or scalar means use data.value; dataType=null means no value. Inspect data.dataType before formatting and never assume undocumented nested keys. For a generic trading-pair list, call market_get_symbol_overview first; it returns only counts and a small sample. For paged or quote-asset browsing, use market_list_symbols. Use market_search_symbols only when the user gave a keyword; query is required. Use market_get_symbol_info only for exact precision and minimum rules. Do not use a symbol search as a generic all-symbol list. The complete raw symbols payload is intentionally unavailable in MCP. For broad market questions, call market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. Account types are fixed: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Never call account type 2 Derivatives. For Spot to Derivatives, use account_prepare_transfer with fromAccount=EXCHANGE and toAccount=FUTURE. Use wallet_prepare_universal_transfer for margin or C2C; account type 2 requires its isolated-margin trading pair in symbol. Prepare/confirm tools return their documented action payload directly in data. 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."
|