@vtxmacro/cli 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.
Files changed (4) hide show
  1. package/README.md +35 -0
  2. package/bin/vtx-mcp.js +1109 -0
  3. package/bin/vtx.js +15375 -0
  4. package/package.json +22 -0
package/bin/vtx-mcp.js ADDED
@@ -0,0 +1,1109 @@
1
+ #!/usr/bin/env node
2
+
3
+ // lib/agent-core/client.ts
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ // lib/agent-core/types.ts
7
+ var AgentClientError = class extends Error {
8
+ constructor(payload) {
9
+ super(payload.message);
10
+ this.name = "AgentClientError";
11
+ this.status = payload.status;
12
+ this.code = payload.code;
13
+ this.detail = payload.detail;
14
+ }
15
+ };
16
+
17
+ // lib/agent-core/client.ts
18
+ var SECRET_RESPONSE_FIELDS = /* @__PURE__ */ new Set([
19
+ "hyperliquid_signing_key",
20
+ "local_ai_api_key",
21
+ "openai_compatible_api_key",
22
+ "openai_api_key",
23
+ "anthropic_api_key",
24
+ "gemini_api_key",
25
+ "grok_api_key",
26
+ "groq_api_key",
27
+ "openrouter_api_key",
28
+ "venice_api_key",
29
+ "deepseek_api_key",
30
+ "mistral_api_key",
31
+ "together_api_key",
32
+ "fireworks_api_key",
33
+ "cerebras_api_key",
34
+ "moonshot_api_key",
35
+ "alibaba_cloud_model_studio_api_key",
36
+ "amazon_bedrock_api_key",
37
+ "lightning_api_key"
38
+ ]);
39
+ function normalizeApiUrl(value) {
40
+ const parsed = String(value || "").trim();
41
+ if (!parsed) {
42
+ throw new Error("VTX API URL is required");
43
+ }
44
+ return parsed.replace(/\/+$/, "");
45
+ }
46
+ async function parseError(response, fallback) {
47
+ let detail = null;
48
+ try {
49
+ detail = await response.json();
50
+ } catch {
51
+ detail = null;
52
+ }
53
+ const rawDetail = detail && typeof detail === "object" && "detail" in detail ? detail.detail : detail;
54
+ const message = typeof rawDetail === "string" ? rawDetail : fallback;
55
+ const code = rawDetail && typeof rawDetail === "object" && "code" in rawDetail ? String(rawDetail.code || "") : void 0;
56
+ return new AgentClientError({
57
+ status: response.status,
58
+ code,
59
+ message,
60
+ detail: rawDetail
61
+ });
62
+ }
63
+ function redactSecretResponse(value) {
64
+ if (Array.isArray(value)) {
65
+ return value.map((item) => redactSecretResponse(item));
66
+ }
67
+ if (!value || typeof value !== "object") {
68
+ return value;
69
+ }
70
+ const redacted = {};
71
+ for (const [key, nested] of Object.entries(value)) {
72
+ if (SECRET_RESPONSE_FIELDS.has(key)) {
73
+ redacted[key] = nested ? "[redacted]" : null;
74
+ continue;
75
+ }
76
+ redacted[key] = redactSecretResponse(nested);
77
+ }
78
+ return redacted;
79
+ }
80
+ var VtxAgentClient = class _VtxAgentClient {
81
+ constructor(config) {
82
+ this.apiUrl = normalizeApiUrl(config.apiUrl);
83
+ this.token = String(config.token || "").trim() || null;
84
+ this.activeProfileId = config.activeProfileId ?? null;
85
+ this.fetchImpl = config.fetchImpl ?? fetch;
86
+ }
87
+ async request(path, options = {}) {
88
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
89
+ const headers = {
90
+ Accept: "application/json"
91
+ };
92
+ if (this.token) {
93
+ headers.Authorization = `Bearer ${this.token}`;
94
+ }
95
+ const profileId = options.profileId ?? this.activeProfileId;
96
+ if (profileId != null) {
97
+ headers["x-profile-id"] = String(profileId);
98
+ }
99
+ if (options.idempotencyKey) {
100
+ headers["x-idempotency-key"] = options.idempotencyKey;
101
+ }
102
+ if (options.headers) {
103
+ for (const [key, value] of Object.entries(options.headers)) {
104
+ if (value) {
105
+ headers[key] = value;
106
+ }
107
+ }
108
+ }
109
+ let body;
110
+ if (options.body !== void 0) {
111
+ headers["Content-Type"] = "application/json";
112
+ body = JSON.stringify(options.body);
113
+ }
114
+ const response = await this.fetchImpl(`${this.apiUrl}${normalizedPath}`, {
115
+ method: options.method ?? "GET",
116
+ headers,
117
+ body
118
+ });
119
+ if (!response.ok) {
120
+ throw await parseError(response, `VTX API request failed: ${response.status}`);
121
+ }
122
+ if (response.status === 204) {
123
+ return void 0;
124
+ }
125
+ return await response.json();
126
+ }
127
+ whoami() {
128
+ return this.request("/auth/agent-tokens/whoami");
129
+ }
130
+ listTokens() {
131
+ return this.request("/auth/agent-tokens");
132
+ }
133
+ createToken(payload) {
134
+ return this.request("/auth/agent-tokens", { method: "POST", body: payload });
135
+ }
136
+ startBrowserLogin(payload) {
137
+ return this.request("/auth/agent-tokens/login/start", {
138
+ method: "POST",
139
+ body: payload
140
+ });
141
+ }
142
+ pollBrowserLogin(loginCode) {
143
+ return this.request("/auth/agent-tokens/login/poll", {
144
+ method: "POST",
145
+ body: { login_code: loginCode }
146
+ });
147
+ }
148
+ revokeToken(tokenId, reason = "cli_revoked") {
149
+ return this.request(`/auth/agent-tokens/${tokenId}`, {
150
+ method: "DELETE",
151
+ body: { reason }
152
+ });
153
+ }
154
+ listProfiles() {
155
+ return this.request("/auth/me/profiles");
156
+ }
157
+ createProfile(profileId) {
158
+ return this.request("/auth/me/profiles", {
159
+ method: "POST",
160
+ profileId
161
+ });
162
+ }
163
+ updateProfile(profileId, payload) {
164
+ return this.request("/auth/me", {
165
+ method: "PATCH",
166
+ profileId,
167
+ body: payload
168
+ });
169
+ }
170
+ deleteProfile(profileId) {
171
+ return this.request(`/auth/me/profiles/${profileId}`, {
172
+ method: "DELETE"
173
+ });
174
+ }
175
+ setProfileExecutionMode(profileId, executionMode) {
176
+ return this.request("/preferences", {
177
+ method: "PUT",
178
+ profileId,
179
+ body: { preferences: { execution_mode: executionMode } }
180
+ });
181
+ }
182
+ updatePreferences(profileId, preferences) {
183
+ return this.request("/preferences", {
184
+ method: "PUT",
185
+ profileId,
186
+ body: { preferences }
187
+ });
188
+ }
189
+ getBillingStatus() {
190
+ return this.request("/billing/balance");
191
+ }
192
+ getBillingPrices(profileId) {
193
+ return this.request("/billing/prices", { profileId });
194
+ }
195
+ getRuntimeStatus(profileId) {
196
+ return this.request("/trading/ai/status", { profileId });
197
+ }
198
+ getRuntimeEvents(profileId, options = {}) {
199
+ const params = new URLSearchParams();
200
+ if (options.limit !== void 0) {
201
+ params.set("limit", String(options.limit));
202
+ }
203
+ if (options.eventType) {
204
+ params.set("event_type", options.eventType);
205
+ }
206
+ if (options.clientEventType) {
207
+ params.set("client_event_type", options.clientEventType);
208
+ }
209
+ const query = params.toString();
210
+ return this.request(`/trading/ai/runtime/events${query ? `?${query}` : ""}`, { profileId });
211
+ }
212
+ getRuntimeBootstrap(profileId, leaseToken) {
213
+ return this.request("/trading/ai/runtime/bootstrap", {
214
+ profileId,
215
+ headers: { "x-client-runtime-lease": leaseToken }
216
+ });
217
+ }
218
+ getRuntimeSecrets(profileId, leaseToken, options = {}) {
219
+ const params = new URLSearchParams();
220
+ if (options.activeProvider) {
221
+ params.set("active_provider", options.activeProvider);
222
+ }
223
+ if (typeof options.includeAllProviderKeys === "boolean") {
224
+ params.set("include_all_provider_keys", String(options.includeAllProviderKeys));
225
+ }
226
+ const query = params.toString();
227
+ return this.request(`/auth/me/client-runtime-secrets${query ? `?${query}` : ""}`, {
228
+ profileId,
229
+ headers: { "x-client-runtime-lease": leaseToken }
230
+ });
231
+ }
232
+ getRuntimePromptContractMetadata(profileId, payload, leaseToken) {
233
+ return this.request("/trading/ai/runtime/prompt-contract/metadata", {
234
+ method: "POST",
235
+ profileId,
236
+ headers: { "x-client-runtime-lease": leaseToken },
237
+ body: payload
238
+ });
239
+ }
240
+ getRuntimePromptContractDerivedContext(profileId, payload, leaseToken) {
241
+ return this.request("/trading/ai/runtime/prompt-contract/derived-context", {
242
+ method: "POST",
243
+ profileId,
244
+ headers: { "x-client-runtime-lease": leaseToken },
245
+ body: payload
246
+ });
247
+ }
248
+ getRuntimePrompt(profileId, payload, leaseToken) {
249
+ return this.request("/trading/ai/runtime/prompt", {
250
+ method: "POST",
251
+ profileId,
252
+ headers: { "x-client-runtime-lease": leaseToken },
253
+ body: payload
254
+ });
255
+ }
256
+ getLlmConfig(profileId) {
257
+ return this.request("/llm/config", { profileId });
258
+ }
259
+ heartbeatRuntime(profileId, payload, leaseToken) {
260
+ return this.request("/trading/ai/runtime/heartbeat", {
261
+ method: "POST",
262
+ profileId,
263
+ headers: { "x-client-runtime-lease": leaseToken },
264
+ body: payload
265
+ });
266
+ }
267
+ reportRuntimeDecision(profileId, payload, leaseToken) {
268
+ return this.request("/trading/ai/runtime/decision", {
269
+ method: "POST",
270
+ profileId,
271
+ idempotencyKey: randomUUID(),
272
+ headers: { "x-client-runtime-lease": leaseToken },
273
+ body: payload
274
+ });
275
+ }
276
+ reportRuntimeTradeSync(profileId, payload, leaseToken) {
277
+ return this.request("/trading/ai/runtime/trade-sync", {
278
+ method: "POST",
279
+ profileId,
280
+ idempotencyKey: randomUUID(),
281
+ headers: { "x-client-runtime-lease": leaseToken },
282
+ body: payload
283
+ });
284
+ }
285
+ reportRuntimeError(profileId, payload, leaseToken) {
286
+ return this.request("/trading/ai/runtime/error", {
287
+ method: "POST",
288
+ profileId,
289
+ idempotencyKey: randomUUID(),
290
+ headers: { "x-client-runtime-lease": leaseToken },
291
+ body: payload
292
+ });
293
+ }
294
+ getTradeHistory(profileId) {
295
+ return this.request("/trading/ai/history", { profileId });
296
+ }
297
+ placeMarketOrder(profileId, payload) {
298
+ return this.request("/trading/market-order", {
299
+ method: "POST",
300
+ profileId,
301
+ idempotencyKey: randomUUID(),
302
+ body: payload
303
+ });
304
+ }
305
+ placeLimitOrder(profileId, payload) {
306
+ return this.request("/trading/limit-order", {
307
+ method: "POST",
308
+ profileId,
309
+ idempotencyKey: randomUUID(),
310
+ body: payload
311
+ });
312
+ }
313
+ cancelOrder(profileId, payload) {
314
+ return this.request("/trading/cancel-order", {
315
+ method: "POST",
316
+ profileId,
317
+ idempotencyKey: randomUUID(),
318
+ body: payload
319
+ });
320
+ }
321
+ getSecretStatus(profileId) {
322
+ return this.request("/auth/me", { profileId });
323
+ }
324
+ async setRuntimeSecret(profileId, fieldName, value) {
325
+ const response = await this.request("/auth/me/client-runtime-secrets", {
326
+ method: "PUT",
327
+ profileId,
328
+ body: { [fieldName]: value }
329
+ });
330
+ return redactSecretResponse(response);
331
+ }
332
+ startBot(profileId, payload = {}) {
333
+ return this.request("/trading/ai/start", {
334
+ method: "POST",
335
+ profileId,
336
+ idempotencyKey: randomUUID(),
337
+ body: payload
338
+ });
339
+ }
340
+ stopBot(profileId) {
341
+ return this.request("/trading/ai/stop", {
342
+ method: "POST",
343
+ profileId,
344
+ idempotencyKey: randomUUID(),
345
+ body: {}
346
+ });
347
+ }
348
+ startAssistant(profileId) {
349
+ return this.request("/trading/ai/assistant/start", {
350
+ method: "POST",
351
+ profileId,
352
+ idempotencyKey: randomUUID(),
353
+ body: {}
354
+ });
355
+ }
356
+ stopAssistant(profileId) {
357
+ return this.request("/trading/ai/assistant/stop", {
358
+ method: "POST",
359
+ profileId,
360
+ idempotencyKey: randomUUID(),
361
+ body: {}
362
+ });
363
+ }
364
+ startRuntime(profileId, payload) {
365
+ return this.request("/trading/ai/runtime/session/start", {
366
+ method: "POST",
367
+ profileId,
368
+ idempotencyKey: randomUUID(),
369
+ body: payload
370
+ });
371
+ }
372
+ stopRuntime(profileId, payload, leaseToken) {
373
+ const headersLease = String(leaseToken || "").trim();
374
+ const previousFetch = this.fetchImpl;
375
+ if (!headersLease) {
376
+ return this.request("/trading/ai/runtime/session/stop", {
377
+ method: "POST",
378
+ profileId,
379
+ idempotencyKey: randomUUID(),
380
+ body: payload
381
+ });
382
+ }
383
+ const withLeaseFetch = async (input, init) => {
384
+ const headers = new Headers(init?.headers);
385
+ headers.set("x-client-runtime-lease", headersLease);
386
+ return previousFetch(input, { ...init, headers });
387
+ };
388
+ return new _VtxAgentClient({
389
+ apiUrl: this.apiUrl,
390
+ token: this.token,
391
+ activeProfileId: this.activeProfileId,
392
+ fetchImpl: withLeaseFetch
393
+ }).request("/trading/ai/runtime/session/stop", {
394
+ method: "POST",
395
+ profileId,
396
+ idempotencyKey: randomUUID(),
397
+ body: payload
398
+ });
399
+ }
400
+ };
401
+ function createAgentClient(config) {
402
+ return new VtxAgentClient(config);
403
+ }
404
+
405
+ // lib/agent-core/config.ts
406
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
407
+ import { dirname, join } from "node:path";
408
+ import { homedir } from "node:os";
409
+ function defaultBaseDir() {
410
+ return join(homedir(), ".vtx");
411
+ }
412
+ function resolveAgentCliConfig(env = process.env) {
413
+ const baseDir = String(env.VTX_HOME || "").trim() || defaultBaseDir();
414
+ const rawProfile = String(env.VTX_PROFILE_ID || "").trim();
415
+ const parsedProfile = rawProfile ? Number(rawProfile) : NaN;
416
+ return {
417
+ apiUrl: String(env.VTX_API_URL || "http://localhost:8000").replace(/\/+$/, ""),
418
+ tokenPath: String(env.VTX_TOKEN_PATH || "").trim() || join(baseDir, "token.json"),
419
+ statePath: String(env.VTX_RUNTIME_STATE_PATH || "").trim() || join(baseDir, "runtime-state.json"),
420
+ runtimeDeviceId: String(env.VTX_RUNTIME_DEVICE_ID || "").trim() || null,
421
+ activeProfileId: Number.isFinite(parsedProfile) && parsedProfile > 0 ? parsedProfile : null,
422
+ outputJson: String(env.VTX_OUTPUT || "").trim().toLowerCase() === "json"
423
+ };
424
+ }
425
+ async function readStoredAgentAuth(path) {
426
+ try {
427
+ const raw = await readFile(path, "utf8");
428
+ if (!raw.trim()) {
429
+ throw new Error(`VTX token file is empty at ${path}. Run "vtx auth login" or remove the file and retry.`);
430
+ }
431
+ const parsed = JSON.parse(raw);
432
+ const token = String(parsed.token || "").trim();
433
+ return token ? { token } : null;
434
+ } catch (error) {
435
+ if (error.code === "ENOENT") {
436
+ return null;
437
+ }
438
+ if (error instanceof SyntaxError) {
439
+ throw new Error(`VTX token file is not valid JSON at ${path}. Run "vtx auth login" or remove the file and retry.`);
440
+ }
441
+ throw error;
442
+ }
443
+ }
444
+
445
+ // lib/runtime/hyperliquid-market-symbol.ts
446
+ var configuredCaseSensitiveSymbols = [];
447
+ var canonicalBaseSymbol = (base) => {
448
+ const trimmed = base.trim();
449
+ if (!trimmed) return "";
450
+ const upper = trimmed.toUpperCase();
451
+ for (const candidate of configuredCaseSensitiveSymbols) {
452
+ if (candidate.toUpperCase() === upper) {
453
+ return candidate;
454
+ }
455
+ }
456
+ return upper;
457
+ };
458
+ var normalizeHyperliquidMarketSymbol = (value) => {
459
+ const raw = String(value ?? "").trim();
460
+ if (!raw) return "";
461
+ if (!raw.includes(":")) return canonicalBaseSymbol(raw);
462
+ const [dex, ...rest] = raw.split(":");
463
+ const base = rest.join(":").trim();
464
+ const normalizedDex = dex.trim().toLowerCase();
465
+ if (!normalizedDex || !base) return "";
466
+ return `${normalizedDex}:${canonicalBaseSymbol(base)}`;
467
+ };
468
+
469
+ // lib/agent-mcp/server.ts
470
+ var VTX_MCP_TOOLS = [
471
+ {
472
+ name: "vtx_whoami",
473
+ description: "Inspect the authenticated VTX user and agent-token scopes.",
474
+ inputSchema: { type: "object", properties: {} }
475
+ },
476
+ {
477
+ name: "vtx_profiles_list",
478
+ description: "List profiles owned by the authenticated VTX user.",
479
+ inputSchema: { type: "object", properties: {} }
480
+ },
481
+ {
482
+ name: "vtx_profiles_create",
483
+ description: "Create a profile for the authenticated VTX user. Requires profile:write scope.",
484
+ inputSchema: {
485
+ type: "object",
486
+ properties: { sourceProfileId: { type: "number" } }
487
+ }
488
+ },
489
+ {
490
+ name: "vtx_profiles_update",
491
+ description: "Update the selected profile handle for the authenticated VTX user. Requires profile:write scope.",
492
+ inputSchema: {
493
+ type: "object",
494
+ required: ["profileId", "name"],
495
+ properties: { profileId: { type: "number" }, name: { type: "string" } }
496
+ }
497
+ },
498
+ {
499
+ name: "vtx_profiles_delete",
500
+ description: "Delete a profile owned by the authenticated VTX user. Requires profile:write scope.",
501
+ inputSchema: {
502
+ type: "object",
503
+ required: ["profileId"],
504
+ properties: { profileId: { type: "number" } }
505
+ }
506
+ },
507
+ {
508
+ name: "vtx_profiles_set_mode",
509
+ description: "Set a profile execution mode. Client Mode runs locally with no VTX platform fee; Server Mode uses normal VTX infrastructure and fees.",
510
+ inputSchema: {
511
+ type: "object",
512
+ required: ["profileId", "mode"],
513
+ properties: { profileId: { type: "number" }, mode: { type: "string", enum: ["client", "server"] } }
514
+ }
515
+ },
516
+ {
517
+ name: "vtx_bots_start",
518
+ description: "Start the Server Mode AI Trader for a profile through the normal backend path. Requires bot:control scope and normal Server Mode billing/caps.",
519
+ inputSchema: {
520
+ type: "object",
521
+ required: ["profileId"],
522
+ properties: {
523
+ profileId: { type: "number" },
524
+ symbol: { type: "string" },
525
+ timeframe: { type: "string" },
526
+ size: { type: "string" },
527
+ leverage: { type: "number" },
528
+ interval: { type: "number" }
529
+ }
530
+ }
531
+ },
532
+ {
533
+ name: "vtx_bots_configure",
534
+ description: "Configure AI Trader profile preferences through the normal preferences API before starting a bot. Requires profile:write scope.",
535
+ inputSchema: {
536
+ type: "object",
537
+ required: ["profileId"],
538
+ properties: {
539
+ profileId: { type: "number" },
540
+ symbol: { type: "string" },
541
+ timeframe: { type: "string" },
542
+ size: { type: "string" },
543
+ leverage: { type: "number" },
544
+ interval: { type: "number" }
545
+ }
546
+ }
547
+ },
548
+ {
549
+ name: "vtx_bots_stop",
550
+ description: "Stop the Server Mode AI Trader for a profile through the normal backend path. Requires bot:control scope.",
551
+ inputSchema: {
552
+ type: "object",
553
+ required: ["profileId"],
554
+ properties: { profileId: { type: "number" } }
555
+ }
556
+ },
557
+ {
558
+ name: "vtx_bots_status",
559
+ description: "Read bot/runtime status for a profile.",
560
+ inputSchema: {
561
+ type: "object",
562
+ required: ["profileId"],
563
+ properties: { profileId: { type: "number" } }
564
+ }
565
+ },
566
+ {
567
+ name: "vtx_bots_logs",
568
+ description: "Read AI Trader decision history/logs for a profile without executing trades.",
569
+ inputSchema: {
570
+ type: "object",
571
+ required: ["profileId"],
572
+ properties: { profileId: { type: "number" } }
573
+ }
574
+ },
575
+ {
576
+ name: "vtx_runtime_status",
577
+ description: "Read Client Mode runtime status for a profile. Client Mode has no VTX platform fee; Server Mode bots use normal fees.",
578
+ inputSchema: {
579
+ type: "object",
580
+ required: ["profileId"],
581
+ properties: { profileId: { type: "number" } }
582
+ }
583
+ },
584
+ {
585
+ name: "vtx_runtime_events",
586
+ description: "Read Client Mode runtime events for a profile. This is status/history visibility and does not execute local runtime work.",
587
+ inputSchema: {
588
+ type: "object",
589
+ required: ["profileId"],
590
+ properties: {
591
+ profileId: { type: "number" },
592
+ limit: { type: "number" },
593
+ eventType: { type: "string" },
594
+ clientEventType: { type: "string" }
595
+ }
596
+ }
597
+ },
598
+ {
599
+ name: "vtx_trade_market_order",
600
+ description: "Submit a market order for a profile through the normal Trade page backend path. Requires trading:execute scope and uses the same risk checks, rate limits, and idempotency behavior as a user-submitted order.",
601
+ inputSchema: {
602
+ type: "object",
603
+ required: ["profileId", "symbol", "side", "size"],
604
+ properties: {
605
+ profileId: { type: "number" },
606
+ symbol: { type: "string" },
607
+ side: { type: "string", enum: ["buy", "sell"] },
608
+ size: { type: "number" },
609
+ slippage: { type: "number" },
610
+ estimatedPrice: { type: "number" },
611
+ estimatedPriceTimestampMs: { type: "number" }
612
+ }
613
+ }
614
+ },
615
+ {
616
+ name: "vtx_trade_limit_order",
617
+ description: "Submit a limit order for a profile through the normal Trade page backend path. Requires trading:execute scope and uses the same risk checks, rate limits, and idempotency behavior as a user-submitted order.",
618
+ inputSchema: {
619
+ type: "object",
620
+ required: ["profileId", "symbol", "side", "size", "limitPrice"],
621
+ properties: {
622
+ profileId: { type: "number" },
623
+ symbol: { type: "string" },
624
+ side: { type: "string", enum: ["buy", "sell"] },
625
+ size: { type: "number" },
626
+ limitPrice: { type: "number" },
627
+ reduceOnly: { type: "boolean" }
628
+ }
629
+ }
630
+ },
631
+ {
632
+ name: "vtx_trade_cancel_order",
633
+ description: "Cancel an open order for a profile through the normal Trade page backend path. Requires trading:execute scope.",
634
+ inputSchema: {
635
+ type: "object",
636
+ required: ["profileId", "symbol", "oid"],
637
+ properties: {
638
+ profileId: { type: "number" },
639
+ symbol: { type: "string" },
640
+ oid: { type: "number" }
641
+ }
642
+ }
643
+ },
644
+ {
645
+ name: "vtx_tokens_list",
646
+ description: "List agent tokens without returning raw token values.",
647
+ inputSchema: { type: "object", properties: {} }
648
+ },
649
+ {
650
+ name: "vtx_billing_status",
651
+ description: "Read VTX billing balance for the authenticated user.",
652
+ inputSchema: { type: "object", properties: {} }
653
+ },
654
+ {
655
+ name: "vtx_billing_prices",
656
+ description: "Read VTX pricing visibility, including Server Mode trader call fee and model pricing metadata.",
657
+ inputSchema: {
658
+ type: "object",
659
+ properties: { profileId: { type: "number" } }
660
+ }
661
+ },
662
+ {
663
+ name: "vtx_ai_config",
664
+ description: "Read the shared AI/runtime configuration contract used by the web app, CLI, MCP, and Client Mode runtime.",
665
+ inputSchema: {
666
+ type: "object",
667
+ properties: { profileId: { type: "number" } }
668
+ }
669
+ },
670
+ {
671
+ name: "vtx_secrets_status",
672
+ description: "Read secret readiness and masked previews for a profile. Raw stored secret values are not returned.",
673
+ inputSchema: {
674
+ type: "object",
675
+ required: ["profileId"],
676
+ properties: { profileId: { type: "number" } }
677
+ }
678
+ },
679
+ {
680
+ name: "vtx_secrets_set",
681
+ description: "Write or rotate one runtime secret for a profile. The response redacts raw secret values.",
682
+ inputSchema: {
683
+ type: "object",
684
+ required: ["profileId", "field", "value"],
685
+ properties: {
686
+ profileId: { type: "number" },
687
+ field: { type: "string" },
688
+ value: { type: "string" }
689
+ }
690
+ }
691
+ }
692
+ ];
693
+ var WRITABLE_SECRET_FIELDS = /* @__PURE__ */ new Set([
694
+ "hyperliquid_signing_key",
695
+ "local_ai_api_key",
696
+ "openai_compatible_api_key",
697
+ "openai_api_key",
698
+ "anthropic_api_key",
699
+ "gemini_api_key",
700
+ "grok_api_key",
701
+ "groq_api_key",
702
+ "openrouter_api_key",
703
+ "venice_api_key",
704
+ "deepseek_api_key",
705
+ "mistral_api_key",
706
+ "together_api_key",
707
+ "fireworks_api_key",
708
+ "cerebras_api_key",
709
+ "moonshot_api_key",
710
+ "alibaba_cloud_model_studio_api_key",
711
+ "lightning_api_key"
712
+ ]);
713
+ function requirePositiveNumber(input, key) {
714
+ const value = Number(input[key]);
715
+ if (!Number.isFinite(value) || value <= 0) {
716
+ throw new Error(`${key} must be a positive number`);
717
+ }
718
+ return value;
719
+ }
720
+ function optionalPositiveNumber(input, key) {
721
+ if (input[key] === void 0 || input[key] === null || input[key] === "") {
722
+ return void 0;
723
+ }
724
+ return requirePositiveNumber(input, key);
725
+ }
726
+ function optionalString(input, key) {
727
+ const value = String(input[key] || "").trim();
728
+ return value || null;
729
+ }
730
+ function botStartPayload(input) {
731
+ const payload = {};
732
+ for (const key of ["symbol", "timeframe", "size"]) {
733
+ const value = optionalString(input, key);
734
+ if (value) {
735
+ payload[key] = value;
736
+ }
737
+ }
738
+ const leverage = optionalPositiveNumber(input, "leverage");
739
+ if (leverage !== void 0) {
740
+ payload.leverage = leverage;
741
+ }
742
+ const interval = optionalPositiveNumber(input, "interval");
743
+ if (interval !== void 0) {
744
+ payload.interval = interval;
745
+ }
746
+ return payload;
747
+ }
748
+ function botConfigurePreferences(input) {
749
+ const preferences = {};
750
+ const trade = {};
751
+ const symbol = optionalString(input, "symbol");
752
+ if (symbol) {
753
+ const normalizedSymbol = normalizeHyperliquidMarketSymbol(symbol);
754
+ preferences.trading_symbols = [normalizedSymbol];
755
+ trade.tradingSymbol = normalizedSymbol;
756
+ }
757
+ const timeframe = optionalString(input, "timeframe");
758
+ if (timeframe) {
759
+ preferences.trading_timeframes = [timeframe.trim()];
760
+ trade.chartInterval = timeframe.trim();
761
+ }
762
+ const size = optionalString(input, "size");
763
+ if (size) {
764
+ preferences.ai_trade_amount = Number(size);
765
+ trade.orderSizeUSDC = size;
766
+ }
767
+ const leverage = optionalPositiveNumber(input, "leverage");
768
+ if (leverage !== void 0) {
769
+ trade.leverage = leverage;
770
+ }
771
+ const interval = optionalPositiveNumber(input, "interval");
772
+ if (interval !== void 0) {
773
+ preferences.trading_interval = interval;
774
+ }
775
+ if (Object.keys(trade).length > 0) {
776
+ preferences.trade = trade;
777
+ }
778
+ if (Object.keys(preferences).length === 0) {
779
+ throw new Error("At least one bot configuration field is required");
780
+ }
781
+ return preferences;
782
+ }
783
+ function requireOrderSide(input) {
784
+ const side = optionalString(input, "side");
785
+ if (side !== "buy" && side !== "sell") {
786
+ throw new Error("side must be buy or sell");
787
+ }
788
+ return side;
789
+ }
790
+ function requireSymbol(input) {
791
+ const symbol = optionalString(input, "symbol");
792
+ if (!symbol) {
793
+ throw new Error("symbol is required");
794
+ }
795
+ return normalizeHyperliquidMarketSymbol(symbol);
796
+ }
797
+ function marketOrderPayload(input) {
798
+ const payload = {
799
+ symbol: requireSymbol(input),
800
+ side: requireOrderSide(input),
801
+ size: requirePositiveNumber(input, "size")
802
+ };
803
+ const slippage = optionalPositiveNumber(input, "slippage");
804
+ if (slippage !== void 0) {
805
+ payload.slippage = slippage;
806
+ }
807
+ const estimatedPrice = optionalPositiveNumber(input, "estimatedPrice");
808
+ if (estimatedPrice !== void 0) {
809
+ payload.estimated_price = estimatedPrice;
810
+ }
811
+ const estimatedPriceTimestampMs = optionalPositiveNumber(input, "estimatedPriceTimestampMs");
812
+ if (estimatedPriceTimestampMs !== void 0) {
813
+ payload.estimated_price_timestamp_ms = estimatedPriceTimestampMs;
814
+ }
815
+ return payload;
816
+ }
817
+ function limitOrderPayload(input) {
818
+ return {
819
+ symbol: requireSymbol(input),
820
+ side: requireOrderSide(input),
821
+ size: requirePositiveNumber(input, "size"),
822
+ limit_price: requirePositiveNumber(input, "limitPrice"),
823
+ reduce_only: Boolean(input.reduceOnly)
824
+ };
825
+ }
826
+ function cancelOrderPayload(input) {
827
+ return {
828
+ symbol: requireSymbol(input),
829
+ oid: requirePositiveNumber(input, "oid")
830
+ };
831
+ }
832
+ function requireSecretField(input) {
833
+ const field = optionalString(input, "field");
834
+ if (!field || !WRITABLE_SECRET_FIELDS.has(field)) {
835
+ throw new Error(`Unsupported secret field: ${field || "(none)"}`);
836
+ }
837
+ return field;
838
+ }
839
+ async function createDefaultMcpClient(env = process.env) {
840
+ const config = resolveAgentCliConfig(env);
841
+ const auth = await readStoredAgentAuth(config.tokenPath);
842
+ return createAgentClient({
843
+ apiUrl: config.apiUrl,
844
+ token: auth?.token,
845
+ activeProfileId: config.activeProfileId
846
+ });
847
+ }
848
+ async function callVtxMcpTool(client, name, input = {}) {
849
+ if (name === "vtx_whoami") {
850
+ return client.whoami();
851
+ }
852
+ if (name === "vtx_profiles_list") {
853
+ return client.listProfiles();
854
+ }
855
+ if (name === "vtx_profiles_create") {
856
+ const sourceProfileId = optionalPositiveNumber(input, "sourceProfileId") ?? null;
857
+ return client.createProfile(sourceProfileId);
858
+ }
859
+ if (name === "vtx_profiles_update") {
860
+ const profileId = requirePositiveNumber(input, "profileId");
861
+ const username = optionalString(input, "name");
862
+ if (!username) {
863
+ throw new Error("name is required");
864
+ }
865
+ return client.updateProfile(profileId, { username });
866
+ }
867
+ if (name === "vtx_profiles_delete") {
868
+ return client.deleteProfile(requirePositiveNumber(input, "profileId"));
869
+ }
870
+ if (name === "vtx_profiles_set_mode") {
871
+ const profileId = requirePositiveNumber(input, "profileId");
872
+ const mode = optionalString(input, "mode");
873
+ if (mode !== "client" && mode !== "server") {
874
+ throw new Error("mode must be client or server");
875
+ }
876
+ return client.setProfileExecutionMode(profileId, mode);
877
+ }
878
+ if (name === "vtx_tokens_list") {
879
+ return client.listTokens();
880
+ }
881
+ if (name === "vtx_bots_start") {
882
+ return client.startBot(requirePositiveNumber(input, "profileId"), botStartPayload(input));
883
+ }
884
+ if (name === "vtx_bots_configure") {
885
+ return client.updatePreferences(requirePositiveNumber(input, "profileId"), botConfigurePreferences(input));
886
+ }
887
+ if (name === "vtx_bots_stop") {
888
+ return client.stopBot(requirePositiveNumber(input, "profileId"));
889
+ }
890
+ if (name === "vtx_bots_status") {
891
+ return client.getRuntimeStatus(requirePositiveNumber(input, "profileId"));
892
+ }
893
+ if (name === "vtx_bots_logs") {
894
+ return client.getTradeHistory(requirePositiveNumber(input, "profileId"));
895
+ }
896
+ if (name === "vtx_runtime_status") {
897
+ return client.getRuntimeStatus(requirePositiveNumber(input, "profileId"));
898
+ }
899
+ if (name === "vtx_runtime_events") {
900
+ return client.getRuntimeEvents(requirePositiveNumber(input, "profileId"), {
901
+ limit: optionalPositiveNumber(input, "limit"),
902
+ eventType: optionalString(input, "eventType"),
903
+ clientEventType: optionalString(input, "clientEventType")
904
+ });
905
+ }
906
+ if (name === "vtx_trade_market_order") {
907
+ return client.placeMarketOrder(requirePositiveNumber(input, "profileId"), marketOrderPayload(input));
908
+ }
909
+ if (name === "vtx_trade_limit_order") {
910
+ return client.placeLimitOrder(requirePositiveNumber(input, "profileId"), limitOrderPayload(input));
911
+ }
912
+ if (name === "vtx_trade_cancel_order") {
913
+ return client.cancelOrder(requirePositiveNumber(input, "profileId"), cancelOrderPayload(input));
914
+ }
915
+ if (name === "vtx_billing_status") {
916
+ return client.getBillingStatus();
917
+ }
918
+ if (name === "vtx_billing_prices") {
919
+ return client.getBillingPrices(optionalPositiveNumber(input, "profileId") ?? null);
920
+ }
921
+ if (name === "vtx_ai_config") {
922
+ return client.getLlmConfig(optionalPositiveNumber(input, "profileId") ?? null);
923
+ }
924
+ if (name === "vtx_secrets_status") {
925
+ return client.getSecretStatus(requirePositiveNumber(input, "profileId"));
926
+ }
927
+ if (name === "vtx_secrets_set") {
928
+ const value = optionalString(input, "value");
929
+ if (value === null) {
930
+ throw new Error("value is required");
931
+ }
932
+ return client.setRuntimeSecret(requirePositiveNumber(input, "profileId"), requireSecretField(input), value);
933
+ }
934
+ throw new Error(`Unknown VTX MCP tool: ${name}`);
935
+ }
936
+
937
+ // lib/agent-mcp/stdio.ts
938
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
939
+ function jsonRpcResult(id, result) {
940
+ return { jsonrpc: "2.0", id, result };
941
+ }
942
+ function jsonRpcError(id, code, message, data) {
943
+ return { jsonrpc: "2.0", id, error: { code, message, data } };
944
+ }
945
+ function requestId(request) {
946
+ return request.id === void 0 ? null : request.id;
947
+ }
948
+ function isNotification(request) {
949
+ return request.id === void 0;
950
+ }
951
+ function stringifyToolResult(result) {
952
+ if (typeof result === "string") {
953
+ return result;
954
+ }
955
+ return JSON.stringify(result, null, 2);
956
+ }
957
+ async function handleMcpJsonRpcRequest(request, clientFactory = createDefaultMcpClient) {
958
+ const id = requestId(request);
959
+ const method = String(request.method || "").trim();
960
+ if (!method) {
961
+ return jsonRpcError(id, -32600, "Invalid JSON-RPC request.");
962
+ }
963
+ if (method === "initialize") {
964
+ return jsonRpcResult(id, {
965
+ protocolVersion: MCP_PROTOCOL_VERSION,
966
+ capabilities: {
967
+ tools: {}
968
+ },
969
+ serverInfo: {
970
+ name: "vtx-macro",
971
+ version: "0.1.0"
972
+ }
973
+ });
974
+ }
975
+ if (method === "notifications/initialized") {
976
+ return null;
977
+ }
978
+ if (method === "ping") {
979
+ return jsonRpcResult(id, {});
980
+ }
981
+ if (method === "tools/list") {
982
+ return jsonRpcResult(id, { tools: VTX_MCP_TOOLS });
983
+ }
984
+ if (method === "tools/call") {
985
+ const params = request.params || {};
986
+ const name = String(params.name || "").trim();
987
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
988
+ if (!name) {
989
+ return jsonRpcError(id, -32602, "Tool name is required.");
990
+ }
991
+ try {
992
+ const client = await clientFactory();
993
+ const result = await callVtxMcpTool(client, name, args);
994
+ return jsonRpcResult(id, {
995
+ content: [
996
+ {
997
+ type: "text",
998
+ text: stringifyToolResult(result)
999
+ }
1000
+ ],
1001
+ isError: false
1002
+ });
1003
+ } catch (error) {
1004
+ const message = error instanceof Error ? error.message : "VTX MCP tool failed.";
1005
+ return jsonRpcResult(id, {
1006
+ content: [
1007
+ {
1008
+ type: "text",
1009
+ text: message
1010
+ }
1011
+ ],
1012
+ isError: true
1013
+ });
1014
+ }
1015
+ }
1016
+ if (isNotification(request)) {
1017
+ return null;
1018
+ }
1019
+ return jsonRpcError(id, -32601, `Unsupported MCP method: ${method}`);
1020
+ }
1021
+ function encodeMcpFrame(message) {
1022
+ const body = JSON.stringify(message);
1023
+ return Buffer.from(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r
1024
+ \r
1025
+ ${body}`, "utf8");
1026
+ }
1027
+ function encodeMcpLine(message) {
1028
+ return Buffer.from(`${JSON.stringify(message)}
1029
+ `, "utf8");
1030
+ }
1031
+ function encodeMcpResponse(message, format) {
1032
+ return format === "json-line" ? encodeMcpLine(message) : encodeMcpFrame(message);
1033
+ }
1034
+ function parseJsonLineFrame(buffer, newlineEnd) {
1035
+ const line = buffer.subarray(0, newlineEnd).toString("utf8").trim();
1036
+ if (!line) {
1037
+ return {
1038
+ message: null,
1039
+ remaining: buffer.subarray(newlineEnd + 1),
1040
+ format: "json-line"
1041
+ };
1042
+ }
1043
+ if (!line.startsWith("{") && !line.startsWith("[")) {
1044
+ return null;
1045
+ }
1046
+ return {
1047
+ message: JSON.parse(line),
1048
+ remaining: buffer.subarray(newlineEnd + 1),
1049
+ format: "json-line"
1050
+ };
1051
+ }
1052
+ function extractFrame(buffer) {
1053
+ const headerEnd = buffer.indexOf("\r\n\r\n");
1054
+ const delimiterLength = 4;
1055
+ const normalizedHeaderEnd = headerEnd >= 0 ? headerEnd : buffer.indexOf("\n\n");
1056
+ const normalizedDelimiterLength = headerEnd >= 0 ? delimiterLength : 2;
1057
+ if (normalizedHeaderEnd < 0) {
1058
+ const newlineEnd = buffer.indexOf("\n");
1059
+ if (newlineEnd < 0) {
1060
+ return null;
1061
+ }
1062
+ return parseJsonLineFrame(buffer, newlineEnd);
1063
+ }
1064
+ const headers = buffer.subarray(0, normalizedHeaderEnd).toString("utf8");
1065
+ const lengthLine = headers.split(/\r?\n/).find((line) => /^content-length:/i.test(line));
1066
+ if (!lengthLine) {
1067
+ const newlineEnd = buffer.indexOf("\n");
1068
+ if (newlineEnd >= 0 && normalizedHeaderEnd > newlineEnd) {
1069
+ return parseJsonLineFrame(buffer, newlineEnd);
1070
+ }
1071
+ return null;
1072
+ }
1073
+ const rawLength = lengthLine.split(":")[1]?.trim();
1074
+ const contentLength = Number(rawLength);
1075
+ if (!Number.isInteger(contentLength) || contentLength < 0) {
1076
+ throw new Error("MCP frame has invalid Content-Length header.");
1077
+ }
1078
+ const bodyStart = normalizedHeaderEnd + normalizedDelimiterLength;
1079
+ const bodyEnd = bodyStart + contentLength;
1080
+ if (buffer.length < bodyEnd) {
1081
+ return null;
1082
+ }
1083
+ const body = buffer.subarray(bodyStart, bodyEnd).toString("utf8");
1084
+ return {
1085
+ message: JSON.parse(body),
1086
+ remaining: buffer.subarray(bodyEnd),
1087
+ format: "content-length"
1088
+ };
1089
+ }
1090
+ async function runVtxMcpStdioServer(input = process.stdin, output = process.stdout, clientFactory = createDefaultMcpClient) {
1091
+ let buffer = Buffer.alloc(0);
1092
+ for await (const chunk of input) {
1093
+ buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))]);
1094
+ let frame = extractFrame(buffer);
1095
+ while (frame) {
1096
+ buffer = frame.remaining;
1097
+ if (frame.message) {
1098
+ const response = await handleMcpJsonRpcRequest(frame.message, clientFactory);
1099
+ if (response) {
1100
+ output.write(encodeMcpResponse(response, frame.format));
1101
+ }
1102
+ }
1103
+ frame = extractFrame(buffer);
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ // bin/vtx-mcp.ts
1109
+ await runVtxMcpStdioServer();