@exagent/agent 0.1.1 → 0.1.2
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/chunk-5PQNJMLK.mjs +1816 -0
- package/dist/chunk-7WZIS3RL.mjs +1841 -0
- package/dist/chunk-HXDTXMNM.mjs +1828 -0
- package/dist/chunk-JKVX2ZE2.mjs +2593 -0
- package/dist/chunk-KJJGA46E.mjs +2666 -0
- package/dist/chunk-S43QPE3R.mjs +1615 -0
- package/dist/chunk-WDT62ZH4.mjs +1615 -0
- package/dist/chunk-YNSV3HXM.mjs +2667 -0
- package/dist/cli.js +1257 -47
- package/dist/cli.mjs +110 -6
- package/dist/index.d.mts +301 -7
- package/dist/index.d.ts +301 -7
- package/dist/index.js +1159 -33
- package/dist/index.mjs +21 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -34,14 +34,21 @@ __export(index_exports, {
|
|
|
34
34
|
AgentRuntime: () => AgentRuntime,
|
|
35
35
|
AnthropicAdapter: () => AnthropicAdapter,
|
|
36
36
|
BaseLLMAdapter: () => BaseLLMAdapter,
|
|
37
|
+
DeepSeekAdapter: () => DeepSeekAdapter,
|
|
38
|
+
GoogleAdapter: () => GoogleAdapter,
|
|
39
|
+
GroqAdapter: () => GroqAdapter,
|
|
37
40
|
LLMConfigSchema: () => LLMConfigSchema,
|
|
38
41
|
LLMProviderSchema: () => LLMProviderSchema,
|
|
39
42
|
MarketDataService: () => MarketDataService,
|
|
43
|
+
MistralAdapter: () => MistralAdapter,
|
|
40
44
|
OllamaAdapter: () => OllamaAdapter,
|
|
41
45
|
OpenAIAdapter: () => OpenAIAdapter,
|
|
46
|
+
RelayClient: () => RelayClient,
|
|
47
|
+
RelayConfigSchema: () => RelayConfigSchema,
|
|
42
48
|
RiskManager: () => RiskManager,
|
|
43
49
|
RiskUniverseSchema: () => RiskUniverseSchema,
|
|
44
50
|
STRATEGY_TEMPLATES: () => STRATEGY_TEMPLATES,
|
|
51
|
+
TogetherAdapter: () => TogetherAdapter,
|
|
45
52
|
TradeExecutor: () => TradeExecutor,
|
|
46
53
|
TradingConfigSchema: () => TradingConfigSchema,
|
|
47
54
|
VaultConfigSchema: () => VaultConfigSchema,
|
|
@@ -49,9 +56,12 @@ __export(index_exports, {
|
|
|
49
56
|
VaultPolicySchema: () => VaultPolicySchema,
|
|
50
57
|
createLLMAdapter: () => createLLMAdapter,
|
|
51
58
|
createSampleConfig: () => createSampleConfig,
|
|
59
|
+
decryptEnvFile: () => decryptEnvFile,
|
|
60
|
+
encryptEnvFile: () => encryptEnvFile,
|
|
52
61
|
getAllStrategyTemplates: () => getAllStrategyTemplates,
|
|
53
62
|
getStrategyTemplate: () => getStrategyTemplate,
|
|
54
63
|
loadConfig: () => loadConfig,
|
|
64
|
+
loadSecureEnv: () => loadSecureEnv,
|
|
55
65
|
loadStrategy: () => loadStrategy,
|
|
56
66
|
validateConfig: () => validateConfig,
|
|
57
67
|
validateStrategy: () => validateStrategy
|
|
@@ -60,6 +70,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
60
70
|
|
|
61
71
|
// src/runtime.ts
|
|
62
72
|
var import_sdk = require("@exagent/sdk");
|
|
73
|
+
var import_viem2 = require("viem");
|
|
74
|
+
var import_chains2 = require("viem/chains");
|
|
63
75
|
|
|
64
76
|
// src/llm/openai.ts
|
|
65
77
|
var import_openai = __toESM(require("openai"));
|
|
@@ -104,7 +116,7 @@ var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
|
104
116
|
async chat(messages) {
|
|
105
117
|
try {
|
|
106
118
|
const response = await this.client.chat.completions.create({
|
|
107
|
-
model: this.config.model || "gpt-4
|
|
119
|
+
model: this.config.model || "gpt-4.1",
|
|
108
120
|
messages: messages.map((m) => ({
|
|
109
121
|
role: m.role,
|
|
110
122
|
content: m.content
|
|
@@ -149,7 +161,7 @@ var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
|
149
161
|
const systemMessage = messages.find((m) => m.role === "system");
|
|
150
162
|
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
151
163
|
const body = {
|
|
152
|
-
model: this.config.model || "claude-
|
|
164
|
+
model: this.config.model || "claude-opus-4-5-20251101",
|
|
153
165
|
max_tokens: this.config.maxTokens || 4096,
|
|
154
166
|
temperature: this.config.temperature,
|
|
155
167
|
system: systemMessage?.content,
|
|
@@ -186,6 +198,256 @@ var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
|
186
198
|
}
|
|
187
199
|
};
|
|
188
200
|
|
|
201
|
+
// src/llm/google.ts
|
|
202
|
+
var GoogleAdapter = class extends BaseLLMAdapter {
|
|
203
|
+
apiKey;
|
|
204
|
+
baseUrl;
|
|
205
|
+
constructor(config) {
|
|
206
|
+
super(config);
|
|
207
|
+
if (!config.apiKey) {
|
|
208
|
+
throw new Error("Google AI API key required");
|
|
209
|
+
}
|
|
210
|
+
this.apiKey = config.apiKey;
|
|
211
|
+
this.baseUrl = config.endpoint || "https://generativelanguage.googleapis.com/v1beta";
|
|
212
|
+
}
|
|
213
|
+
async chat(messages) {
|
|
214
|
+
const model = this.config.model || "gemini-2.5-flash";
|
|
215
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
216
|
+
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
217
|
+
const contents = chatMessages.map((m) => ({
|
|
218
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
219
|
+
parts: [{ text: m.content }]
|
|
220
|
+
}));
|
|
221
|
+
const body = {
|
|
222
|
+
contents,
|
|
223
|
+
generationConfig: {
|
|
224
|
+
temperature: this.config.temperature,
|
|
225
|
+
maxOutputTokens: this.config.maxTokens || 4096
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
if (systemMessage) {
|
|
229
|
+
body.systemInstruction = {
|
|
230
|
+
parts: [{ text: systemMessage.content }]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const url = `${this.baseUrl}/models/${model}:generateContent?key=${this.apiKey}`;
|
|
234
|
+
const response = await fetch(url, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
"Content-Type": "application/json"
|
|
238
|
+
},
|
|
239
|
+
body: JSON.stringify(body)
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
const error = await response.text();
|
|
243
|
+
throw new Error(`Google AI API error: ${response.status} - ${error}`);
|
|
244
|
+
}
|
|
245
|
+
const data = await response.json();
|
|
246
|
+
const candidate = data.candidates?.[0];
|
|
247
|
+
if (!candidate?.content?.parts) {
|
|
248
|
+
throw new Error("No response from Google AI");
|
|
249
|
+
}
|
|
250
|
+
const content = candidate.content.parts.map((part) => part.text || "").join("");
|
|
251
|
+
const usageMetadata = data.usageMetadata;
|
|
252
|
+
return {
|
|
253
|
+
content,
|
|
254
|
+
usage: usageMetadata ? {
|
|
255
|
+
promptTokens: usageMetadata.promptTokenCount || 0,
|
|
256
|
+
completionTokens: usageMetadata.candidatesTokenCount || 0,
|
|
257
|
+
totalTokens: usageMetadata.totalTokenCount || 0
|
|
258
|
+
} : void 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/llm/deepseek.ts
|
|
264
|
+
var import_openai2 = __toESM(require("openai"));
|
|
265
|
+
var DeepSeekAdapter = class extends BaseLLMAdapter {
|
|
266
|
+
client;
|
|
267
|
+
constructor(config) {
|
|
268
|
+
super(config);
|
|
269
|
+
if (!config.apiKey) {
|
|
270
|
+
throw new Error("DeepSeek API key required");
|
|
271
|
+
}
|
|
272
|
+
this.client = new import_openai2.default({
|
|
273
|
+
apiKey: config.apiKey,
|
|
274
|
+
baseURL: config.endpoint || "https://api.deepseek.com/v1"
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async chat(messages) {
|
|
278
|
+
try {
|
|
279
|
+
const response = await this.client.chat.completions.create({
|
|
280
|
+
model: this.config.model || "deepseek-chat",
|
|
281
|
+
messages: messages.map((m) => ({
|
|
282
|
+
role: m.role,
|
|
283
|
+
content: m.content
|
|
284
|
+
})),
|
|
285
|
+
temperature: this.config.temperature,
|
|
286
|
+
max_tokens: this.config.maxTokens
|
|
287
|
+
});
|
|
288
|
+
const choice = response.choices[0];
|
|
289
|
+
if (!choice || !choice.message) {
|
|
290
|
+
throw new Error("No response from DeepSeek");
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
content: choice.message.content || "",
|
|
294
|
+
usage: response.usage ? {
|
|
295
|
+
promptTokens: response.usage.prompt_tokens,
|
|
296
|
+
completionTokens: response.usage.completion_tokens,
|
|
297
|
+
totalTokens: response.usage.total_tokens
|
|
298
|
+
} : void 0
|
|
299
|
+
};
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof import_openai2.default.APIError) {
|
|
302
|
+
throw new Error(`DeepSeek API error: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/llm/mistral.ts
|
|
310
|
+
var MistralAdapter = class extends BaseLLMAdapter {
|
|
311
|
+
apiKey;
|
|
312
|
+
baseUrl;
|
|
313
|
+
constructor(config) {
|
|
314
|
+
super(config);
|
|
315
|
+
if (!config.apiKey) {
|
|
316
|
+
throw new Error("Mistral API key required");
|
|
317
|
+
}
|
|
318
|
+
this.apiKey = config.apiKey;
|
|
319
|
+
this.baseUrl = config.endpoint || "https://api.mistral.ai/v1";
|
|
320
|
+
}
|
|
321
|
+
async chat(messages) {
|
|
322
|
+
const body = {
|
|
323
|
+
model: this.config.model || "mistral-large-latest",
|
|
324
|
+
messages: messages.map((m) => ({
|
|
325
|
+
role: m.role,
|
|
326
|
+
content: m.content
|
|
327
|
+
})),
|
|
328
|
+
temperature: this.config.temperature,
|
|
329
|
+
max_tokens: this.config.maxTokens
|
|
330
|
+
};
|
|
331
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: {
|
|
334
|
+
"Content-Type": "application/json",
|
|
335
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify(body)
|
|
338
|
+
});
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
const error = await response.text();
|
|
341
|
+
throw new Error(`Mistral API error: ${response.status} - ${error}`);
|
|
342
|
+
}
|
|
343
|
+
const data = await response.json();
|
|
344
|
+
const choice = data.choices?.[0];
|
|
345
|
+
if (!choice || !choice.message) {
|
|
346
|
+
throw new Error("No response from Mistral");
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
content: choice.message.content || "",
|
|
350
|
+
usage: data.usage ? {
|
|
351
|
+
promptTokens: data.usage.prompt_tokens,
|
|
352
|
+
completionTokens: data.usage.completion_tokens,
|
|
353
|
+
totalTokens: data.usage.total_tokens
|
|
354
|
+
} : void 0
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/llm/groq.ts
|
|
360
|
+
var import_openai3 = __toESM(require("openai"));
|
|
361
|
+
var GroqAdapter = class extends BaseLLMAdapter {
|
|
362
|
+
client;
|
|
363
|
+
constructor(config) {
|
|
364
|
+
super(config);
|
|
365
|
+
if (!config.apiKey) {
|
|
366
|
+
throw new Error("Groq API key required");
|
|
367
|
+
}
|
|
368
|
+
this.client = new import_openai3.default({
|
|
369
|
+
apiKey: config.apiKey,
|
|
370
|
+
baseURL: config.endpoint || "https://api.groq.com/openai/v1"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async chat(messages) {
|
|
374
|
+
try {
|
|
375
|
+
const response = await this.client.chat.completions.create({
|
|
376
|
+
model: this.config.model || "llama-3.1-70b-versatile",
|
|
377
|
+
messages: messages.map((m) => ({
|
|
378
|
+
role: m.role,
|
|
379
|
+
content: m.content
|
|
380
|
+
})),
|
|
381
|
+
temperature: this.config.temperature,
|
|
382
|
+
max_tokens: this.config.maxTokens
|
|
383
|
+
});
|
|
384
|
+
const choice = response.choices[0];
|
|
385
|
+
if (!choice || !choice.message) {
|
|
386
|
+
throw new Error("No response from Groq");
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
content: choice.message.content || "",
|
|
390
|
+
usage: response.usage ? {
|
|
391
|
+
promptTokens: response.usage.prompt_tokens,
|
|
392
|
+
completionTokens: response.usage.completion_tokens,
|
|
393
|
+
totalTokens: response.usage.total_tokens
|
|
394
|
+
} : void 0
|
|
395
|
+
};
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof import_openai3.default.APIError) {
|
|
398
|
+
throw new Error(`Groq API error: ${error.message}`);
|
|
399
|
+
}
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// src/llm/together.ts
|
|
406
|
+
var import_openai4 = __toESM(require("openai"));
|
|
407
|
+
var TogetherAdapter = class extends BaseLLMAdapter {
|
|
408
|
+
client;
|
|
409
|
+
constructor(config) {
|
|
410
|
+
super(config);
|
|
411
|
+
if (!config.apiKey) {
|
|
412
|
+
throw new Error("Together AI API key required");
|
|
413
|
+
}
|
|
414
|
+
this.client = new import_openai4.default({
|
|
415
|
+
apiKey: config.apiKey,
|
|
416
|
+
baseURL: config.endpoint || "https://api.together.xyz/v1"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async chat(messages) {
|
|
420
|
+
try {
|
|
421
|
+
const response = await this.client.chat.completions.create({
|
|
422
|
+
model: this.config.model || "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
|
|
423
|
+
messages: messages.map((m) => ({
|
|
424
|
+
role: m.role,
|
|
425
|
+
content: m.content
|
|
426
|
+
})),
|
|
427
|
+
temperature: this.config.temperature,
|
|
428
|
+
max_tokens: this.config.maxTokens
|
|
429
|
+
});
|
|
430
|
+
const choice = response.choices[0];
|
|
431
|
+
if (!choice || !choice.message) {
|
|
432
|
+
throw new Error("No response from Together AI");
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
content: choice.message.content || "",
|
|
436
|
+
usage: response.usage ? {
|
|
437
|
+
promptTokens: response.usage.prompt_tokens,
|
|
438
|
+
completionTokens: response.usage.completion_tokens,
|
|
439
|
+
totalTokens: response.usage.total_tokens
|
|
440
|
+
} : void 0
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error instanceof import_openai4.default.APIError) {
|
|
444
|
+
throw new Error(`Together AI API error: ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
189
451
|
// src/llm/ollama.ts
|
|
190
452
|
var OllamaAdapter = class extends BaseLLMAdapter {
|
|
191
453
|
baseUrl;
|
|
@@ -218,7 +480,7 @@ var OllamaAdapter = class extends BaseLLMAdapter {
|
|
|
218
480
|
}
|
|
219
481
|
async chat(messages) {
|
|
220
482
|
const body = {
|
|
221
|
-
model: this.config.model || "
|
|
483
|
+
model: this.config.model || "llama3.2",
|
|
222
484
|
messages: messages.map((m) => ({
|
|
223
485
|
role: m.role,
|
|
224
486
|
content: m.content
|
|
@@ -253,7 +515,7 @@ var OllamaAdapter = class extends BaseLLMAdapter {
|
|
|
253
515
|
getMetadata() {
|
|
254
516
|
return {
|
|
255
517
|
provider: "ollama",
|
|
256
|
-
model: this.config.model || "
|
|
518
|
+
model: this.config.model || "llama3.2",
|
|
257
519
|
isLocal: true
|
|
258
520
|
};
|
|
259
521
|
}
|
|
@@ -266,6 +528,16 @@ async function createLLMAdapter(config) {
|
|
|
266
528
|
return new OpenAIAdapter(config);
|
|
267
529
|
case "anthropic":
|
|
268
530
|
return new AnthropicAdapter(config);
|
|
531
|
+
case "google":
|
|
532
|
+
return new GoogleAdapter(config);
|
|
533
|
+
case "deepseek":
|
|
534
|
+
return new DeepSeekAdapter(config);
|
|
535
|
+
case "mistral":
|
|
536
|
+
return new MistralAdapter(config);
|
|
537
|
+
case "groq":
|
|
538
|
+
return new GroqAdapter(config);
|
|
539
|
+
case "together":
|
|
540
|
+
return new TogetherAdapter(config);
|
|
269
541
|
case "ollama":
|
|
270
542
|
const adapter = new OllamaAdapter(config);
|
|
271
543
|
await adapter.healthCheck();
|
|
@@ -319,7 +591,7 @@ async function loadStrategy(strategyPath) {
|
|
|
319
591
|
console.log("No custom strategy found, using default (hold) strategy");
|
|
320
592
|
return defaultStrategy;
|
|
321
593
|
}
|
|
322
|
-
async function loadTypeScriptModule(
|
|
594
|
+
async function loadTypeScriptModule(path2) {
|
|
323
595
|
try {
|
|
324
596
|
const tsxPath = require.resolve("tsx");
|
|
325
597
|
const { pathToFileURL } = await import("url");
|
|
@@ -330,7 +602,7 @@ async function loadTypeScriptModule(path) {
|
|
|
330
602
|
"--import",
|
|
331
603
|
"tsx/esm",
|
|
332
604
|
"-e",
|
|
333
|
-
`import('${pathToFileURL(
|
|
605
|
+
`import('${pathToFileURL(path2).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
|
|
334
606
|
],
|
|
335
607
|
{
|
|
336
608
|
cwd: process.cwd(),
|
|
@@ -353,7 +625,7 @@ async function loadTypeScriptModule(path) {
|
|
|
353
625
|
const tsx = await import("tsx/esm/api");
|
|
354
626
|
const unregister = tsx.register();
|
|
355
627
|
try {
|
|
356
|
-
const module2 = await import(
|
|
628
|
+
const module2 = await import(path2);
|
|
357
629
|
return module2;
|
|
358
630
|
} finally {
|
|
359
631
|
unregister();
|
|
@@ -1153,7 +1425,268 @@ var VaultManager = class {
|
|
|
1153
1425
|
}
|
|
1154
1426
|
};
|
|
1155
1427
|
|
|
1428
|
+
// src/relay.ts
|
|
1429
|
+
var import_ws = __toESM(require("ws"));
|
|
1430
|
+
var import_accounts2 = require("viem/accounts");
|
|
1431
|
+
var RelayClient = class {
|
|
1432
|
+
config;
|
|
1433
|
+
ws = null;
|
|
1434
|
+
authenticated = false;
|
|
1435
|
+
reconnectAttempts = 0;
|
|
1436
|
+
maxReconnectAttempts = 50;
|
|
1437
|
+
reconnectTimer = null;
|
|
1438
|
+
heartbeatTimer = null;
|
|
1439
|
+
stopped = false;
|
|
1440
|
+
constructor(config) {
|
|
1441
|
+
this.config = config;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Connect to the relay server
|
|
1445
|
+
*/
|
|
1446
|
+
async connect() {
|
|
1447
|
+
if (this.stopped) return;
|
|
1448
|
+
const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
|
|
1449
|
+
return new Promise((resolve, reject) => {
|
|
1450
|
+
try {
|
|
1451
|
+
this.ws = new import_ws.default(wsUrl);
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
console.error("Relay: Failed to create WebSocket:", error);
|
|
1454
|
+
this.scheduleReconnect();
|
|
1455
|
+
reject(error);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const connectTimeout = setTimeout(() => {
|
|
1459
|
+
if (!this.authenticated) {
|
|
1460
|
+
console.error("Relay: Connection timeout");
|
|
1461
|
+
this.ws?.close();
|
|
1462
|
+
this.scheduleReconnect();
|
|
1463
|
+
reject(new Error("Connection timeout"));
|
|
1464
|
+
}
|
|
1465
|
+
}, 15e3);
|
|
1466
|
+
this.ws.on("open", async () => {
|
|
1467
|
+
console.log("Relay: Connected, authenticating...");
|
|
1468
|
+
this.reconnectAttempts = 0;
|
|
1469
|
+
try {
|
|
1470
|
+
await this.authenticate();
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
console.error("Relay: Authentication failed:", error);
|
|
1473
|
+
this.ws?.close();
|
|
1474
|
+
clearTimeout(connectTimeout);
|
|
1475
|
+
reject(error);
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
this.ws.on("message", (raw) => {
|
|
1479
|
+
try {
|
|
1480
|
+
const data = JSON.parse(raw.toString());
|
|
1481
|
+
this.handleMessage(data);
|
|
1482
|
+
if (data.type === "auth_success") {
|
|
1483
|
+
clearTimeout(connectTimeout);
|
|
1484
|
+
this.authenticated = true;
|
|
1485
|
+
this.startHeartbeat();
|
|
1486
|
+
console.log("Relay: Authenticated successfully");
|
|
1487
|
+
resolve();
|
|
1488
|
+
} else if (data.type === "auth_error") {
|
|
1489
|
+
clearTimeout(connectTimeout);
|
|
1490
|
+
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
1491
|
+
reject(new Error(data.message));
|
|
1492
|
+
}
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
this.ws.on("close", (code, reason) => {
|
|
1497
|
+
clearTimeout(connectTimeout);
|
|
1498
|
+
this.authenticated = false;
|
|
1499
|
+
this.stopHeartbeat();
|
|
1500
|
+
if (!this.stopped) {
|
|
1501
|
+
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
1502
|
+
this.scheduleReconnect();
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
this.ws.on("error", (error) => {
|
|
1506
|
+
if (!this.stopped) {
|
|
1507
|
+
console.error("Relay: WebSocket error:", error.message);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Authenticate with the relay server using wallet signature
|
|
1514
|
+
*/
|
|
1515
|
+
async authenticate() {
|
|
1516
|
+
const account = (0, import_accounts2.privateKeyToAccount)(this.config.privateKey);
|
|
1517
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
1518
|
+
const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
|
|
1519
|
+
const signature = await (0, import_accounts2.signMessage)({
|
|
1520
|
+
message,
|
|
1521
|
+
privateKey: this.config.privateKey
|
|
1522
|
+
});
|
|
1523
|
+
this.send({
|
|
1524
|
+
type: "auth",
|
|
1525
|
+
agentId: this.config.agentId,
|
|
1526
|
+
wallet: account.address,
|
|
1527
|
+
timestamp,
|
|
1528
|
+
signature
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Handle incoming messages from the relay server
|
|
1533
|
+
*/
|
|
1534
|
+
handleMessage(data) {
|
|
1535
|
+
switch (data.type) {
|
|
1536
|
+
case "command":
|
|
1537
|
+
if (data.command && this.config.onCommand) {
|
|
1538
|
+
this.config.onCommand(data.command);
|
|
1539
|
+
}
|
|
1540
|
+
break;
|
|
1541
|
+
case "auth_success":
|
|
1542
|
+
case "auth_error":
|
|
1543
|
+
break;
|
|
1544
|
+
case "error":
|
|
1545
|
+
console.error(`Relay: Server error: ${data.message}`);
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Send a status heartbeat
|
|
1551
|
+
*/
|
|
1552
|
+
sendHeartbeat(status) {
|
|
1553
|
+
if (!this.authenticated) return;
|
|
1554
|
+
this.send({
|
|
1555
|
+
type: "heartbeat",
|
|
1556
|
+
agentId: this.config.agentId,
|
|
1557
|
+
status
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Send a status update (outside of regular heartbeat)
|
|
1562
|
+
*/
|
|
1563
|
+
sendStatusUpdate(status) {
|
|
1564
|
+
if (!this.authenticated) return;
|
|
1565
|
+
this.send({
|
|
1566
|
+
type: "status_update",
|
|
1567
|
+
agentId: this.config.agentId,
|
|
1568
|
+
status
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Send a message to the command center
|
|
1573
|
+
*/
|
|
1574
|
+
sendMessage(messageType, level, title, body, data) {
|
|
1575
|
+
if (!this.authenticated) return;
|
|
1576
|
+
this.send({
|
|
1577
|
+
type: "message",
|
|
1578
|
+
agentId: this.config.agentId,
|
|
1579
|
+
messageType,
|
|
1580
|
+
level,
|
|
1581
|
+
title,
|
|
1582
|
+
body,
|
|
1583
|
+
data
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Send a command execution result
|
|
1588
|
+
*/
|
|
1589
|
+
sendCommandResult(commandId, success, result) {
|
|
1590
|
+
if (!this.authenticated) return;
|
|
1591
|
+
this.send({
|
|
1592
|
+
type: "command_result",
|
|
1593
|
+
agentId: this.config.agentId,
|
|
1594
|
+
commandId,
|
|
1595
|
+
success,
|
|
1596
|
+
result
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Start the heartbeat timer
|
|
1601
|
+
*/
|
|
1602
|
+
startHeartbeat() {
|
|
1603
|
+
this.stopHeartbeat();
|
|
1604
|
+
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
1605
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1606
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1607
|
+
this.ws.ping();
|
|
1608
|
+
}
|
|
1609
|
+
}, interval);
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Stop the heartbeat timer
|
|
1613
|
+
*/
|
|
1614
|
+
stopHeartbeat() {
|
|
1615
|
+
if (this.heartbeatTimer) {
|
|
1616
|
+
clearInterval(this.heartbeatTimer);
|
|
1617
|
+
this.heartbeatTimer = null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Schedule a reconnection with exponential backoff
|
|
1622
|
+
*/
|
|
1623
|
+
scheduleReconnect() {
|
|
1624
|
+
if (this.stopped) return;
|
|
1625
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1626
|
+
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
1630
|
+
this.reconnectAttempts++;
|
|
1631
|
+
console.log(
|
|
1632
|
+
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
1633
|
+
);
|
|
1634
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1635
|
+
this.connect().catch(() => {
|
|
1636
|
+
});
|
|
1637
|
+
}, delay);
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Send a JSON message to the WebSocket
|
|
1641
|
+
*/
|
|
1642
|
+
send(data) {
|
|
1643
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1644
|
+
this.ws.send(JSON.stringify(data));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Check if connected and authenticated
|
|
1649
|
+
*/
|
|
1650
|
+
get isConnected() {
|
|
1651
|
+
return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Disconnect and stop reconnecting
|
|
1655
|
+
*/
|
|
1656
|
+
disconnect() {
|
|
1657
|
+
this.stopped = true;
|
|
1658
|
+
this.stopHeartbeat();
|
|
1659
|
+
if (this.reconnectTimer) {
|
|
1660
|
+
clearTimeout(this.reconnectTimer);
|
|
1661
|
+
this.reconnectTimer = null;
|
|
1662
|
+
}
|
|
1663
|
+
if (this.ws) {
|
|
1664
|
+
this.ws.close(1e3, "Agent shutting down");
|
|
1665
|
+
this.ws = null;
|
|
1666
|
+
}
|
|
1667
|
+
this.authenticated = false;
|
|
1668
|
+
console.log("Relay: Disconnected");
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// src/browser-open.ts
|
|
1673
|
+
var import_child_process2 = require("child_process");
|
|
1674
|
+
function openBrowser(url) {
|
|
1675
|
+
const platform = process.platform;
|
|
1676
|
+
try {
|
|
1677
|
+
if (platform === "darwin") {
|
|
1678
|
+
(0, import_child_process2.exec)(`open "${url}"`);
|
|
1679
|
+
} else if (platform === "win32") {
|
|
1680
|
+
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
1681
|
+
} else {
|
|
1682
|
+
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
1683
|
+
}
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1156
1688
|
// src/runtime.ts
|
|
1689
|
+
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
1157
1690
|
var AgentRuntime = class {
|
|
1158
1691
|
config;
|
|
1159
1692
|
client;
|
|
@@ -1163,9 +1696,14 @@ var AgentRuntime = class {
|
|
|
1163
1696
|
riskManager;
|
|
1164
1697
|
marketData;
|
|
1165
1698
|
vaultManager;
|
|
1699
|
+
relay = null;
|
|
1166
1700
|
isRunning = false;
|
|
1701
|
+
mode = "idle";
|
|
1167
1702
|
configHash;
|
|
1168
1703
|
lastVaultCheck = 0;
|
|
1704
|
+
cycleCount = 0;
|
|
1705
|
+
lastCycleAt = 0;
|
|
1706
|
+
processAlive = true;
|
|
1169
1707
|
VAULT_CHECK_INTERVAL = 3e5;
|
|
1170
1708
|
// Check vault status every 5 minutes
|
|
1171
1709
|
constructor(config) {
|
|
@@ -1197,8 +1735,44 @@ var AgentRuntime = class {
|
|
|
1197
1735
|
this.riskManager = new RiskManager(this.config.trading);
|
|
1198
1736
|
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
1199
1737
|
await this.initializeVaultManager();
|
|
1738
|
+
await this.initializeRelay();
|
|
1200
1739
|
console.log("Agent initialized successfully");
|
|
1201
1740
|
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Initialize the relay client for command center connectivity
|
|
1743
|
+
*/
|
|
1744
|
+
async initializeRelay() {
|
|
1745
|
+
const relayConfig = this.config.relay;
|
|
1746
|
+
const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
|
|
1747
|
+
if (!relayConfig?.enabled || !relayEnabled) {
|
|
1748
|
+
console.log("Relay: Disabled");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
|
|
1752
|
+
if (!apiUrl) {
|
|
1753
|
+
console.log("Relay: No API URL configured, skipping");
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
this.relay = new RelayClient({
|
|
1757
|
+
agentId: String(this.config.agentId),
|
|
1758
|
+
privateKey: this.config.privateKey,
|
|
1759
|
+
relay: {
|
|
1760
|
+
...relayConfig,
|
|
1761
|
+
apiUrl
|
|
1762
|
+
},
|
|
1763
|
+
onCommand: (cmd) => this.handleCommand(cmd)
|
|
1764
|
+
});
|
|
1765
|
+
try {
|
|
1766
|
+
await this.relay.connect();
|
|
1767
|
+
console.log("Relay: Connected to command center");
|
|
1768
|
+
this.sendRelayStatus();
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
console.warn(
|
|
1771
|
+
"Relay: Failed to connect (agent will work locally):",
|
|
1772
|
+
error instanceof Error ? error.message : error
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1202
1776
|
/**
|
|
1203
1777
|
* Initialize the vault manager based on config
|
|
1204
1778
|
*/
|
|
@@ -1228,7 +1802,9 @@ var AgentRuntime = class {
|
|
|
1228
1802
|
}
|
|
1229
1803
|
}
|
|
1230
1804
|
/**
|
|
1231
|
-
* Ensure the current wallet is linked to the agent
|
|
1805
|
+
* Ensure the current wallet is linked to the agent.
|
|
1806
|
+
* If the trading wallet differs from the owner, enters a recovery loop
|
|
1807
|
+
* that waits for the owner to link it from the website.
|
|
1232
1808
|
*/
|
|
1233
1809
|
async ensureWalletLinked() {
|
|
1234
1810
|
const agentId = BigInt(this.config.agentId);
|
|
@@ -1238,9 +1814,31 @@ var AgentRuntime = class {
|
|
|
1238
1814
|
console.log("Wallet not linked, linking now...");
|
|
1239
1815
|
const agent = await this.client.registry.getAgent(agentId);
|
|
1240
1816
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
);
|
|
1817
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
1818
|
+
console.log("");
|
|
1819
|
+
console.log("=== WALLET LINKING REQUIRED ===");
|
|
1820
|
+
console.log("");
|
|
1821
|
+
console.log(` Agent owner: ${agent?.owner}`);
|
|
1822
|
+
console.log(` Trading wallet: ${address}`);
|
|
1823
|
+
console.log("");
|
|
1824
|
+
console.log(" Your trading wallet needs to be linked to your agent.");
|
|
1825
|
+
console.log(" Opening the command center in your browser...");
|
|
1826
|
+
console.log(` ${ccUrl}`);
|
|
1827
|
+
console.log("");
|
|
1828
|
+
openBrowser(ccUrl);
|
|
1829
|
+
console.log(" Waiting for wallet to be linked... (checking every 15s)");
|
|
1830
|
+
console.log(" Press Ctrl+C to exit.");
|
|
1831
|
+
console.log("");
|
|
1832
|
+
while (true) {
|
|
1833
|
+
await this.sleep(15e3);
|
|
1834
|
+
const linked = await this.client.registry.isLinkedWallet(agentId, address);
|
|
1835
|
+
if (linked) {
|
|
1836
|
+
console.log(" Wallet linked! Continuing setup...");
|
|
1837
|
+
console.log("");
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
process.stdout.write(".");
|
|
1841
|
+
}
|
|
1244
1842
|
}
|
|
1245
1843
|
await this.client.registry.linkOwnWallet(agentId);
|
|
1246
1844
|
console.log("Wallet linked successfully");
|
|
@@ -1249,8 +1847,9 @@ var AgentRuntime = class {
|
|
|
1249
1847
|
}
|
|
1250
1848
|
}
|
|
1251
1849
|
/**
|
|
1252
|
-
* Sync the LLM config hash to chain for epoch tracking
|
|
1253
|
-
*
|
|
1850
|
+
* Sync the LLM config hash to chain for epoch tracking.
|
|
1851
|
+
* If the wallet has insufficient gas, enters a recovery loop
|
|
1852
|
+
* that waits for the user to fund the wallet.
|
|
1254
1853
|
*/
|
|
1255
1854
|
async syncConfigHash() {
|
|
1256
1855
|
const agentId = BigInt(this.config.agentId);
|
|
@@ -1260,9 +1859,50 @@ var AgentRuntime = class {
|
|
|
1260
1859
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
1261
1860
|
if (onChainHash !== this.configHash) {
|
|
1262
1861
|
console.log("Config changed, updating on-chain...");
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1862
|
+
try {
|
|
1863
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
1864
|
+
const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
1865
|
+
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1868
|
+
if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
1869
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
1870
|
+
const chain = this.config.network === "mainnet" ? import_chains2.base : import_chains2.baseSepolia;
|
|
1871
|
+
const publicClientInstance = (0, import_viem2.createPublicClient)({
|
|
1872
|
+
chain,
|
|
1873
|
+
transport: (0, import_viem2.http)(this.getRpcUrl())
|
|
1874
|
+
});
|
|
1875
|
+
console.log("");
|
|
1876
|
+
console.log("=== ETH NEEDED FOR GAS ===");
|
|
1877
|
+
console.log("");
|
|
1878
|
+
console.log(` Wallet: ${this.client.address}`);
|
|
1879
|
+
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
1880
|
+
console.log(" Opening the command center to fund your wallet...");
|
|
1881
|
+
console.log(` ${ccUrl}`);
|
|
1882
|
+
console.log("");
|
|
1883
|
+
openBrowser(ccUrl);
|
|
1884
|
+
console.log(" Waiting for ETH... (checking every 15s)");
|
|
1885
|
+
console.log(" Press Ctrl+C to exit.");
|
|
1886
|
+
console.log("");
|
|
1887
|
+
while (true) {
|
|
1888
|
+
await this.sleep(15e3);
|
|
1889
|
+
const balance = await publicClientInstance.getBalance({
|
|
1890
|
+
address: this.client.address
|
|
1891
|
+
});
|
|
1892
|
+
if (balance > BigInt(0)) {
|
|
1893
|
+
console.log(" ETH detected! Retrying config update...");
|
|
1894
|
+
console.log("");
|
|
1895
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
1896
|
+
const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
1897
|
+
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
process.stdout.write(".");
|
|
1901
|
+
}
|
|
1902
|
+
} else {
|
|
1903
|
+
throw error;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1266
1906
|
} else {
|
|
1267
1907
|
const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
1268
1908
|
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
@@ -1275,48 +1915,301 @@ var AgentRuntime = class {
|
|
|
1275
1915
|
return this.configHash;
|
|
1276
1916
|
}
|
|
1277
1917
|
/**
|
|
1278
|
-
* Start the
|
|
1918
|
+
* Start the agent in daemon mode.
|
|
1919
|
+
* The agent enters idle mode and waits for commands from the command center.
|
|
1920
|
+
* Trading begins only when a start_trading command is received.
|
|
1921
|
+
*
|
|
1922
|
+
* If relay is not configured, falls back to immediate trading mode.
|
|
1279
1923
|
*/
|
|
1280
1924
|
async run() {
|
|
1281
|
-
|
|
1282
|
-
|
|
1925
|
+
this.processAlive = true;
|
|
1926
|
+
if (this.relay) {
|
|
1927
|
+
console.log("");
|
|
1928
|
+
console.log("Agent is in IDLE mode. Waiting for commands from command center.");
|
|
1929
|
+
console.log("Visit https://exagent.io to start trading from the dashboard.");
|
|
1930
|
+
console.log("");
|
|
1931
|
+
this.mode = "idle";
|
|
1932
|
+
this.sendRelayStatus();
|
|
1933
|
+
this.relay.sendMessage(
|
|
1934
|
+
"system",
|
|
1935
|
+
"success",
|
|
1936
|
+
"Agent Connected",
|
|
1937
|
+
`${this.config.name} is online and waiting for commands.`,
|
|
1938
|
+
{ wallet: this.client.address }
|
|
1939
|
+
);
|
|
1940
|
+
while (this.processAlive) {
|
|
1941
|
+
if (this.mode === "trading" && this.isRunning) {
|
|
1942
|
+
try {
|
|
1943
|
+
await this.runCycle();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1946
|
+
console.error("Error in trading cycle:", message);
|
|
1947
|
+
this.relay?.sendMessage(
|
|
1948
|
+
"system",
|
|
1949
|
+
"error",
|
|
1950
|
+
"Cycle Error",
|
|
1951
|
+
message
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
await this.sleep(this.config.trading.tradingIntervalMs);
|
|
1955
|
+
} else {
|
|
1956
|
+
this.sendRelayStatus();
|
|
1957
|
+
await this.sleep(3e4);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
} else {
|
|
1961
|
+
if (this.isRunning) {
|
|
1962
|
+
throw new Error("Agent is already running");
|
|
1963
|
+
}
|
|
1964
|
+
this.isRunning = true;
|
|
1965
|
+
this.mode = "trading";
|
|
1966
|
+
console.log("Starting trading loop...");
|
|
1967
|
+
console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
|
|
1968
|
+
while (this.isRunning) {
|
|
1969
|
+
try {
|
|
1970
|
+
await this.runCycle();
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
console.error("Error in trading cycle:", error);
|
|
1973
|
+
}
|
|
1974
|
+
await this.sleep(this.config.trading.tradingIntervalMs);
|
|
1975
|
+
}
|
|
1283
1976
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Handle a command from the command center
|
|
1980
|
+
*/
|
|
1981
|
+
async handleCommand(cmd) {
|
|
1982
|
+
console.log(`Command received: ${cmd.type}`);
|
|
1983
|
+
try {
|
|
1984
|
+
switch (cmd.type) {
|
|
1985
|
+
case "start_trading":
|
|
1986
|
+
if (this.mode === "trading") {
|
|
1987
|
+
this.relay?.sendCommandResult(cmd.id, true, "Already trading");
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
this.mode = "trading";
|
|
1991
|
+
this.isRunning = true;
|
|
1992
|
+
console.log("Trading started via command center");
|
|
1993
|
+
this.relay?.sendCommandResult(cmd.id, true, "Trading started");
|
|
1994
|
+
this.relay?.sendMessage(
|
|
1995
|
+
"system",
|
|
1996
|
+
"success",
|
|
1997
|
+
"Trading Started",
|
|
1998
|
+
"Agent is now actively trading."
|
|
1999
|
+
);
|
|
2000
|
+
this.sendRelayStatus();
|
|
2001
|
+
break;
|
|
2002
|
+
case "stop_trading":
|
|
2003
|
+
if (this.mode === "idle") {
|
|
2004
|
+
this.relay?.sendCommandResult(cmd.id, true, "Already idle");
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
this.mode = "idle";
|
|
2008
|
+
this.isRunning = false;
|
|
2009
|
+
console.log("Trading stopped via command center");
|
|
2010
|
+
this.relay?.sendCommandResult(cmd.id, true, "Trading stopped");
|
|
2011
|
+
this.relay?.sendMessage(
|
|
2012
|
+
"system",
|
|
2013
|
+
"info",
|
|
2014
|
+
"Trading Stopped",
|
|
2015
|
+
"Agent is now idle. Send start_trading to resume."
|
|
2016
|
+
);
|
|
2017
|
+
this.sendRelayStatus();
|
|
2018
|
+
break;
|
|
2019
|
+
case "update_risk_params": {
|
|
2020
|
+
const params = cmd.params || {};
|
|
2021
|
+
if (params.maxPositionSizeBps !== void 0) {
|
|
2022
|
+
this.config.trading.maxPositionSizeBps = Number(params.maxPositionSizeBps);
|
|
2023
|
+
}
|
|
2024
|
+
if (params.maxDailyLossBps !== void 0) {
|
|
2025
|
+
this.config.trading.maxDailyLossBps = Number(params.maxDailyLossBps);
|
|
2026
|
+
}
|
|
2027
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
2028
|
+
console.log("Risk params updated via command center");
|
|
2029
|
+
this.relay?.sendCommandResult(cmd.id, true, "Risk params updated");
|
|
2030
|
+
this.relay?.sendMessage(
|
|
2031
|
+
"config_updated",
|
|
2032
|
+
"info",
|
|
2033
|
+
"Risk Parameters Updated",
|
|
2034
|
+
`Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
|
|
2035
|
+
);
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
case "update_trading_interval": {
|
|
2039
|
+
const intervalMs = Number(cmd.params?.intervalMs);
|
|
2040
|
+
if (intervalMs && intervalMs >= 1e3) {
|
|
2041
|
+
this.config.trading.tradingIntervalMs = intervalMs;
|
|
2042
|
+
console.log(`Trading interval updated to ${intervalMs}ms`);
|
|
2043
|
+
this.relay?.sendCommandResult(cmd.id, true, `Interval set to ${intervalMs}ms`);
|
|
2044
|
+
} else {
|
|
2045
|
+
this.relay?.sendCommandResult(cmd.id, false, "Invalid interval (minimum 1000ms)");
|
|
2046
|
+
}
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
case "create_vault": {
|
|
2050
|
+
const result = await this.createVault();
|
|
2051
|
+
this.relay?.sendCommandResult(
|
|
2052
|
+
cmd.id,
|
|
2053
|
+
result.success,
|
|
2054
|
+
result.success ? `Vault created: ${result.vaultAddress}` : result.error
|
|
2055
|
+
);
|
|
2056
|
+
if (result.success) {
|
|
2057
|
+
this.relay?.sendMessage(
|
|
2058
|
+
"vault_created",
|
|
2059
|
+
"success",
|
|
2060
|
+
"Vault Created",
|
|
2061
|
+
`Vault deployed at ${result.vaultAddress}`,
|
|
2062
|
+
{ vaultAddress: result.vaultAddress }
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
case "refresh_status":
|
|
2068
|
+
this.sendRelayStatus();
|
|
2069
|
+
this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
|
|
2070
|
+
break;
|
|
2071
|
+
case "shutdown":
|
|
2072
|
+
console.log("Shutdown requested via command center");
|
|
2073
|
+
this.relay?.sendCommandResult(cmd.id, true, "Shutting down");
|
|
2074
|
+
this.relay?.sendMessage(
|
|
2075
|
+
"system",
|
|
2076
|
+
"info",
|
|
2077
|
+
"Shutting Down",
|
|
2078
|
+
"Agent is shutting down. Restart manually to reconnect."
|
|
2079
|
+
);
|
|
2080
|
+
await this.sleep(1e3);
|
|
2081
|
+
this.stop();
|
|
2082
|
+
break;
|
|
2083
|
+
default:
|
|
2084
|
+
console.warn(`Unknown command: ${cmd.type}`);
|
|
2085
|
+
this.relay?.sendCommandResult(cmd.id, false, `Unknown command: ${cmd.type}`);
|
|
1292
2086
|
}
|
|
1293
|
-
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2089
|
+
console.error(`Command ${cmd.type} failed:`, message);
|
|
2090
|
+
this.relay?.sendCommandResult(cmd.id, false, message);
|
|
1294
2091
|
}
|
|
1295
2092
|
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Send current status to the relay
|
|
2095
|
+
*/
|
|
2096
|
+
sendRelayStatus() {
|
|
2097
|
+
if (!this.relay) return;
|
|
2098
|
+
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
2099
|
+
const status = {
|
|
2100
|
+
mode: this.mode,
|
|
2101
|
+
agentId: String(this.config.agentId),
|
|
2102
|
+
wallet: this.client?.address,
|
|
2103
|
+
cycleCount: this.cycleCount,
|
|
2104
|
+
lastCycleAt: this.lastCycleAt,
|
|
2105
|
+
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
2106
|
+
llm: {
|
|
2107
|
+
provider: this.config.llm.provider,
|
|
2108
|
+
model: this.config.llm.model || "default"
|
|
2109
|
+
},
|
|
2110
|
+
risk: this.riskManager?.getStatus() || {
|
|
2111
|
+
dailyPnL: 0,
|
|
2112
|
+
dailyLossLimit: 0,
|
|
2113
|
+
isLimitHit: false
|
|
2114
|
+
},
|
|
2115
|
+
vault: {
|
|
2116
|
+
policy: vaultConfig.policy,
|
|
2117
|
+
hasVault: false,
|
|
2118
|
+
vaultAddress: null
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
this.relay.sendHeartbeat(status);
|
|
2122
|
+
}
|
|
1296
2123
|
/**
|
|
1297
2124
|
* Run a single trading cycle
|
|
1298
2125
|
*/
|
|
1299
2126
|
async runCycle() {
|
|
1300
2127
|
console.log(`
|
|
1301
2128
|
--- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
|
|
2129
|
+
this.cycleCount++;
|
|
2130
|
+
this.lastCycleAt = Date.now();
|
|
1302
2131
|
await this.checkVaultAutoCreation();
|
|
1303
2132
|
const tokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
1304
2133
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
1305
2134
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
1306
|
-
|
|
2135
|
+
this.checkFundsLow(marketData);
|
|
2136
|
+
let signals;
|
|
2137
|
+
try {
|
|
2138
|
+
signals = await this.strategy(marketData, this.llm, this.config);
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2141
|
+
console.error("LLM/strategy error:", message);
|
|
2142
|
+
this.relay?.sendMessage(
|
|
2143
|
+
"llm_error",
|
|
2144
|
+
"error",
|
|
2145
|
+
"Strategy Error",
|
|
2146
|
+
message
|
|
2147
|
+
);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
1307
2150
|
console.log(`Strategy generated ${signals.length} signals`);
|
|
1308
2151
|
const filteredSignals = this.riskManager.filterSignals(signals, marketData);
|
|
1309
2152
|
console.log(`${filteredSignals.length} signals passed risk checks`);
|
|
2153
|
+
if (this.riskManager.getStatus().isLimitHit) {
|
|
2154
|
+
this.relay?.sendMessage(
|
|
2155
|
+
"risk_limit_hit",
|
|
2156
|
+
"warning",
|
|
2157
|
+
"Risk Limit Hit",
|
|
2158
|
+
`Daily loss limit reached: ${this.riskManager.getStatus().dailyPnL.toFixed(2)}`
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
1310
2161
|
if (filteredSignals.length > 0) {
|
|
1311
2162
|
const results = await this.executor.executeAll(filteredSignals);
|
|
1312
2163
|
for (const result of results) {
|
|
1313
2164
|
if (result.success) {
|
|
1314
2165
|
console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
|
|
2166
|
+
this.relay?.sendMessage(
|
|
2167
|
+
"trade_executed",
|
|
2168
|
+
"success",
|
|
2169
|
+
"Trade Executed",
|
|
2170
|
+
`${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
|
|
2171
|
+
{
|
|
2172
|
+
action: result.signal.action,
|
|
2173
|
+
txHash: result.txHash,
|
|
2174
|
+
tokenIn: result.signal.tokenIn,
|
|
2175
|
+
tokenOut: result.signal.tokenOut
|
|
2176
|
+
}
|
|
2177
|
+
);
|
|
1315
2178
|
} else {
|
|
1316
2179
|
console.warn(`Trade failed: ${result.error}`);
|
|
2180
|
+
this.relay?.sendMessage(
|
|
2181
|
+
"trade_failed",
|
|
2182
|
+
"error",
|
|
2183
|
+
"Trade Failed",
|
|
2184
|
+
result.error || "Unknown error",
|
|
2185
|
+
{ action: result.signal.action }
|
|
2186
|
+
);
|
|
1317
2187
|
}
|
|
1318
2188
|
}
|
|
1319
2189
|
}
|
|
2190
|
+
this.sendRelayStatus();
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Check if ETH balance is below threshold and notify
|
|
2194
|
+
*/
|
|
2195
|
+
checkFundsLow(marketData) {
|
|
2196
|
+
if (!this.relay) return;
|
|
2197
|
+
const wethAddress = "0x4200000000000000000000000000000000000006";
|
|
2198
|
+
const ethBalance = marketData.balances[wethAddress] || BigInt(0);
|
|
2199
|
+
const ethAmount = Number(ethBalance) / 1e18;
|
|
2200
|
+
if (ethAmount < FUNDS_LOW_THRESHOLD) {
|
|
2201
|
+
this.relay.sendMessage(
|
|
2202
|
+
"funds_low",
|
|
2203
|
+
"warning",
|
|
2204
|
+
"Low Funds",
|
|
2205
|
+
`ETH balance is ${ethAmount.toFixed(6)} ETH. Fund your trading wallet to continue trading.`,
|
|
2206
|
+
{
|
|
2207
|
+
ethBalance: ethAmount.toFixed(6),
|
|
2208
|
+
wallet: this.client.address,
|
|
2209
|
+
threshold: FUNDS_LOW_THRESHOLD
|
|
2210
|
+
}
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
1320
2213
|
}
|
|
1321
2214
|
/**
|
|
1322
2215
|
* Check for vault auto-creation based on policy
|
|
@@ -1330,7 +2223,14 @@ var AgentRuntime = class {
|
|
|
1330
2223
|
const result = await this.vaultManager.checkAndAutoCreateVault();
|
|
1331
2224
|
switch (result.action) {
|
|
1332
2225
|
case "created":
|
|
1333
|
-
console.log(
|
|
2226
|
+
console.log(`Vault created automatically: ${result.vaultAddress}`);
|
|
2227
|
+
this.relay?.sendMessage(
|
|
2228
|
+
"vault_created",
|
|
2229
|
+
"success",
|
|
2230
|
+
"Vault Auto-Created",
|
|
2231
|
+
`Vault deployed at ${result.vaultAddress}`,
|
|
2232
|
+
{ vaultAddress: result.vaultAddress }
|
|
2233
|
+
);
|
|
1334
2234
|
break;
|
|
1335
2235
|
case "already_exists":
|
|
1336
2236
|
break;
|
|
@@ -1342,11 +2242,16 @@ var AgentRuntime = class {
|
|
|
1342
2242
|
}
|
|
1343
2243
|
}
|
|
1344
2244
|
/**
|
|
1345
|
-
* Stop the
|
|
2245
|
+
* Stop the agent process completely
|
|
1346
2246
|
*/
|
|
1347
2247
|
stop() {
|
|
1348
2248
|
console.log("Stopping agent...");
|
|
1349
2249
|
this.isRunning = false;
|
|
2250
|
+
this.processAlive = false;
|
|
2251
|
+
this.mode = "idle";
|
|
2252
|
+
if (this.relay) {
|
|
2253
|
+
this.relay.disconnect();
|
|
2254
|
+
}
|
|
1350
2255
|
}
|
|
1351
2256
|
/**
|
|
1352
2257
|
* Get RPC URL based on network
|
|
@@ -1384,6 +2289,7 @@ var AgentRuntime = class {
|
|
|
1384
2289
|
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
1385
2290
|
return {
|
|
1386
2291
|
isRunning: this.isRunning,
|
|
2292
|
+
mode: this.mode,
|
|
1387
2293
|
agentId: Number(this.config.agentId),
|
|
1388
2294
|
wallet: this.client?.address || "not initialized",
|
|
1389
2295
|
llm: {
|
|
@@ -1397,7 +2303,11 @@ var AgentRuntime = class {
|
|
|
1397
2303
|
hasVault: false,
|
|
1398
2304
|
// Updated async via getVaultStatus
|
|
1399
2305
|
vaultAddress: null
|
|
1400
|
-
}
|
|
2306
|
+
},
|
|
2307
|
+
relay: {
|
|
2308
|
+
connected: this.relay?.isConnected || false
|
|
2309
|
+
},
|
|
2310
|
+
cycleCount: this.cycleCount
|
|
1401
2311
|
};
|
|
1402
2312
|
}
|
|
1403
2313
|
/**
|
|
@@ -1476,6 +2386,11 @@ var VaultConfigSchema = import_zod.z.object({
|
|
|
1476
2386
|
var WalletConfigSchema = import_zod.z.object({
|
|
1477
2387
|
setup: WalletSetupSchema.default("provide")
|
|
1478
2388
|
}).optional();
|
|
2389
|
+
var RelayConfigSchema = import_zod.z.object({
|
|
2390
|
+
enabled: import_zod.z.boolean().default(false),
|
|
2391
|
+
apiUrl: import_zod.z.string().url(),
|
|
2392
|
+
heartbeatIntervalMs: import_zod.z.number().min(5e3).default(3e4)
|
|
2393
|
+
}).optional();
|
|
1479
2394
|
var AgentConfigSchema = import_zod.z.object({
|
|
1480
2395
|
// Identity (from on-chain registration)
|
|
1481
2396
|
agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
|
|
@@ -1491,6 +2406,8 @@ var AgentConfigSchema = import_zod.z.object({
|
|
|
1491
2406
|
trading: TradingConfigSchema.default({}),
|
|
1492
2407
|
// Vault configuration (copy trading)
|
|
1493
2408
|
vault: VaultConfigSchema.default({}),
|
|
2409
|
+
// Relay configuration (command center)
|
|
2410
|
+
relay: RelayConfigSchema,
|
|
1494
2411
|
// Allowed tokens (addresses)
|
|
1495
2412
|
allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
|
|
1496
2413
|
});
|
|
@@ -1519,6 +2436,21 @@ function loadConfig(configPath) {
|
|
|
1519
2436
|
if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
|
|
1520
2437
|
llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1521
2438
|
}
|
|
2439
|
+
if (process.env.GOOGLE_AI_API_KEY && config.llm.provider === "google") {
|
|
2440
|
+
llmConfig.apiKey = process.env.GOOGLE_AI_API_KEY;
|
|
2441
|
+
}
|
|
2442
|
+
if (process.env.DEEPSEEK_API_KEY && config.llm.provider === "deepseek") {
|
|
2443
|
+
llmConfig.apiKey = process.env.DEEPSEEK_API_KEY;
|
|
2444
|
+
}
|
|
2445
|
+
if (process.env.MISTRAL_API_KEY && config.llm.provider === "mistral") {
|
|
2446
|
+
llmConfig.apiKey = process.env.MISTRAL_API_KEY;
|
|
2447
|
+
}
|
|
2448
|
+
if (process.env.GROQ_API_KEY && config.llm.provider === "groq") {
|
|
2449
|
+
llmConfig.apiKey = process.env.GROQ_API_KEY;
|
|
2450
|
+
}
|
|
2451
|
+
if (process.env.TOGETHER_API_KEY && config.llm.provider === "together") {
|
|
2452
|
+
llmConfig.apiKey = process.env.TOGETHER_API_KEY;
|
|
2453
|
+
}
|
|
1522
2454
|
if (process.env.EXAGENT_LLM_URL) {
|
|
1523
2455
|
llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
|
|
1524
2456
|
}
|
|
@@ -1557,7 +2489,7 @@ function createSampleConfig(agentId, name) {
|
|
|
1557
2489
|
network: "testnet",
|
|
1558
2490
|
llm: {
|
|
1559
2491
|
provider: "openai",
|
|
1560
|
-
model: "gpt-4
|
|
2492
|
+
model: "gpt-4.1",
|
|
1561
2493
|
temperature: 0.7,
|
|
1562
2494
|
maxTokens: 4096
|
|
1563
2495
|
},
|
|
@@ -1577,20 +2509,211 @@ function createSampleConfig(agentId, name) {
|
|
|
1577
2509
|
}
|
|
1578
2510
|
};
|
|
1579
2511
|
}
|
|
2512
|
+
|
|
2513
|
+
// src/secure-env.ts
|
|
2514
|
+
var crypto = __toESM(require("crypto"));
|
|
2515
|
+
var fs = __toESM(require("fs"));
|
|
2516
|
+
var path = __toESM(require("path"));
|
|
2517
|
+
var ALGORITHM = "aes-256-gcm";
|
|
2518
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
2519
|
+
var SALT_LENGTH = 32;
|
|
2520
|
+
var IV_LENGTH = 16;
|
|
2521
|
+
var KEY_LENGTH = 32;
|
|
2522
|
+
var SENSITIVE_PATTERNS = [
|
|
2523
|
+
/PRIVATE_KEY$/i,
|
|
2524
|
+
/_API_KEY$/i,
|
|
2525
|
+
/API_KEY$/i,
|
|
2526
|
+
/_SECRET$/i,
|
|
2527
|
+
/^OPENAI_API_KEY$/i,
|
|
2528
|
+
/^ANTHROPIC_API_KEY$/i,
|
|
2529
|
+
/^GOOGLE_AI_API_KEY$/i,
|
|
2530
|
+
/^DEEPSEEK_API_KEY$/i,
|
|
2531
|
+
/^MISTRAL_API_KEY$/i,
|
|
2532
|
+
/^GROQ_API_KEY$/i,
|
|
2533
|
+
/^TOGETHER_API_KEY$/i
|
|
2534
|
+
];
|
|
2535
|
+
function isSensitiveKey(key) {
|
|
2536
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
2537
|
+
}
|
|
2538
|
+
function deriveKey(passphrase, salt) {
|
|
2539
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
2540
|
+
}
|
|
2541
|
+
function encryptValue(value, key) {
|
|
2542
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
2543
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
2544
|
+
let encrypted = cipher.update(value, "utf8", "hex");
|
|
2545
|
+
encrypted += cipher.final("hex");
|
|
2546
|
+
const tag = cipher.getAuthTag();
|
|
2547
|
+
return {
|
|
2548
|
+
iv: iv.toString("hex"),
|
|
2549
|
+
encrypted,
|
|
2550
|
+
tag: tag.toString("hex")
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function decryptValue(encrypted, key, iv, tag) {
|
|
2554
|
+
const decipher = crypto.createDecipheriv(
|
|
2555
|
+
ALGORITHM,
|
|
2556
|
+
key,
|
|
2557
|
+
Buffer.from(iv, "hex")
|
|
2558
|
+
);
|
|
2559
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
2560
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
2561
|
+
decrypted += decipher.final("utf8");
|
|
2562
|
+
return decrypted;
|
|
2563
|
+
}
|
|
2564
|
+
function parseEnvFile(content) {
|
|
2565
|
+
const entries = [];
|
|
2566
|
+
for (const line of content.split("\n")) {
|
|
2567
|
+
const trimmed = line.trim();
|
|
2568
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2569
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2570
|
+
if (eqIndex === -1) continue;
|
|
2571
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
2572
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2573
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2574
|
+
value = value.slice(1, -1);
|
|
2575
|
+
}
|
|
2576
|
+
if (key && value) {
|
|
2577
|
+
entries.push({ key, value });
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
return entries;
|
|
2581
|
+
}
|
|
2582
|
+
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
2583
|
+
if (!fs.existsSync(envPath)) {
|
|
2584
|
+
throw new Error(`File not found: ${envPath}`);
|
|
2585
|
+
}
|
|
2586
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
2587
|
+
const entries = parseEnvFile(content);
|
|
2588
|
+
if (entries.length === 0) {
|
|
2589
|
+
throw new Error("No environment variables found in file");
|
|
2590
|
+
}
|
|
2591
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
2592
|
+
const key = deriveKey(passphrase, salt);
|
|
2593
|
+
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
2594
|
+
if (isSensitiveKey(envKey)) {
|
|
2595
|
+
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
2596
|
+
return {
|
|
2597
|
+
key: envKey,
|
|
2598
|
+
value: encrypted,
|
|
2599
|
+
encrypted: true,
|
|
2600
|
+
iv,
|
|
2601
|
+
tag
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
return {
|
|
2605
|
+
key: envKey,
|
|
2606
|
+
value,
|
|
2607
|
+
encrypted: false
|
|
2608
|
+
};
|
|
2609
|
+
});
|
|
2610
|
+
const encryptedEnv = {
|
|
2611
|
+
version: 1,
|
|
2612
|
+
salt: salt.toString("hex"),
|
|
2613
|
+
entries: encryptedEntries
|
|
2614
|
+
};
|
|
2615
|
+
const encPath = envPath + ".enc";
|
|
2616
|
+
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
2617
|
+
if (deleteOriginal) {
|
|
2618
|
+
fs.unlinkSync(envPath);
|
|
2619
|
+
}
|
|
2620
|
+
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
2621
|
+
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
2622
|
+
console.log(
|
|
2623
|
+
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
2624
|
+
);
|
|
2625
|
+
return encPath;
|
|
2626
|
+
}
|
|
2627
|
+
function decryptEnvFile(encPath, passphrase) {
|
|
2628
|
+
if (!fs.existsSync(encPath)) {
|
|
2629
|
+
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
2630
|
+
}
|
|
2631
|
+
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
2632
|
+
if (content.version !== 1) {
|
|
2633
|
+
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
2634
|
+
}
|
|
2635
|
+
const salt = Buffer.from(content.salt, "hex");
|
|
2636
|
+
const key = deriveKey(passphrase, salt);
|
|
2637
|
+
const result = {};
|
|
2638
|
+
for (const entry of content.entries) {
|
|
2639
|
+
if (entry.encrypted) {
|
|
2640
|
+
if (!entry.iv || !entry.tag) {
|
|
2641
|
+
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
2642
|
+
}
|
|
2643
|
+
try {
|
|
2644
|
+
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
2645
|
+
} catch {
|
|
2646
|
+
throw new Error(
|
|
2647
|
+
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
} else {
|
|
2651
|
+
result[entry.key] = entry.value;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
return result;
|
|
2655
|
+
}
|
|
2656
|
+
function loadSecureEnv(basePath, passphrase) {
|
|
2657
|
+
const encPath = path.join(basePath, ".env.enc");
|
|
2658
|
+
const envPath = path.join(basePath, ".env");
|
|
2659
|
+
if (fs.existsSync(encPath)) {
|
|
2660
|
+
if (!passphrase) {
|
|
2661
|
+
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
2662
|
+
}
|
|
2663
|
+
if (!passphrase) {
|
|
2664
|
+
console.warn("");
|
|
2665
|
+
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
2666
|
+
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
2667
|
+
console.warn(" pass --passphrase when running the agent.");
|
|
2668
|
+
console.warn(" Falling back to plaintext .env file.");
|
|
2669
|
+
console.warn("");
|
|
2670
|
+
} else {
|
|
2671
|
+
const vars = decryptEnvFile(encPath, passphrase);
|
|
2672
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
2673
|
+
process.env[key] = value;
|
|
2674
|
+
}
|
|
2675
|
+
return true;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (fs.existsSync(envPath)) {
|
|
2679
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
2680
|
+
const entries = parseEnvFile(content);
|
|
2681
|
+
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
2682
|
+
if (sensitiveKeys.length > 0) {
|
|
2683
|
+
console.warn("");
|
|
2684
|
+
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
2685
|
+
for (const key of sensitiveKeys) {
|
|
2686
|
+
console.warn(` - ${key}`);
|
|
2687
|
+
}
|
|
2688
|
+
console.warn("");
|
|
2689
|
+
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
2690
|
+
console.warn("");
|
|
2691
|
+
}
|
|
2692
|
+
return false;
|
|
2693
|
+
}
|
|
2694
|
+
return false;
|
|
2695
|
+
}
|
|
1580
2696
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1581
2697
|
0 && (module.exports = {
|
|
1582
2698
|
AgentConfigSchema,
|
|
1583
2699
|
AgentRuntime,
|
|
1584
2700
|
AnthropicAdapter,
|
|
1585
2701
|
BaseLLMAdapter,
|
|
2702
|
+
DeepSeekAdapter,
|
|
2703
|
+
GoogleAdapter,
|
|
2704
|
+
GroqAdapter,
|
|
1586
2705
|
LLMConfigSchema,
|
|
1587
2706
|
LLMProviderSchema,
|
|
1588
2707
|
MarketDataService,
|
|
2708
|
+
MistralAdapter,
|
|
1589
2709
|
OllamaAdapter,
|
|
1590
2710
|
OpenAIAdapter,
|
|
2711
|
+
RelayClient,
|
|
2712
|
+
RelayConfigSchema,
|
|
1591
2713
|
RiskManager,
|
|
1592
2714
|
RiskUniverseSchema,
|
|
1593
2715
|
STRATEGY_TEMPLATES,
|
|
2716
|
+
TogetherAdapter,
|
|
1594
2717
|
TradeExecutor,
|
|
1595
2718
|
TradingConfigSchema,
|
|
1596
2719
|
VaultConfigSchema,
|
|
@@ -1598,9 +2721,12 @@ function createSampleConfig(agentId, name) {
|
|
|
1598
2721
|
VaultPolicySchema,
|
|
1599
2722
|
createLLMAdapter,
|
|
1600
2723
|
createSampleConfig,
|
|
2724
|
+
decryptEnvFile,
|
|
2725
|
+
encryptEnvFile,
|
|
1601
2726
|
getAllStrategyTemplates,
|
|
1602
2727
|
getStrategyTemplate,
|
|
1603
2728
|
loadConfig,
|
|
2729
|
+
loadSecureEnv,
|
|
1604
2730
|
loadStrategy,
|
|
1605
2731
|
validateConfig,
|
|
1606
2732
|
validateStrategy
|