@exagent/agent 0.1.0 → 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 +5 -2
package/dist/cli.js
CHANGED
|
@@ -27,11 +27,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
var import_dotenv2 = require("dotenv");
|
|
29
29
|
var readline = __toESM(require("readline"));
|
|
30
|
-
var
|
|
31
|
-
var
|
|
30
|
+
var fs2 = __toESM(require("fs"));
|
|
31
|
+
var path2 = __toESM(require("path"));
|
|
32
32
|
|
|
33
33
|
// src/runtime.ts
|
|
34
34
|
var import_sdk = require("@exagent/sdk");
|
|
35
|
+
var import_viem2 = require("viem");
|
|
36
|
+
var import_chains2 = require("viem/chains");
|
|
35
37
|
|
|
36
38
|
// src/llm/openai.ts
|
|
37
39
|
var import_openai = __toESM(require("openai"));
|
|
@@ -76,7 +78,7 @@ var OpenAIAdapter = class extends BaseLLMAdapter {
|
|
|
76
78
|
async chat(messages) {
|
|
77
79
|
try {
|
|
78
80
|
const response = await this.client.chat.completions.create({
|
|
79
|
-
model: this.config.model || "gpt-4
|
|
81
|
+
model: this.config.model || "gpt-4.1",
|
|
80
82
|
messages: messages.map((m) => ({
|
|
81
83
|
role: m.role,
|
|
82
84
|
content: m.content
|
|
@@ -121,7 +123,7 @@ var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
|
121
123
|
const systemMessage = messages.find((m) => m.role === "system");
|
|
122
124
|
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
123
125
|
const body = {
|
|
124
|
-
model: this.config.model || "claude-
|
|
126
|
+
model: this.config.model || "claude-opus-4-5-20251101",
|
|
125
127
|
max_tokens: this.config.maxTokens || 4096,
|
|
126
128
|
temperature: this.config.temperature,
|
|
127
129
|
system: systemMessage?.content,
|
|
@@ -158,6 +160,256 @@ var AnthropicAdapter = class extends BaseLLMAdapter {
|
|
|
158
160
|
}
|
|
159
161
|
};
|
|
160
162
|
|
|
163
|
+
// src/llm/google.ts
|
|
164
|
+
var GoogleAdapter = class extends BaseLLMAdapter {
|
|
165
|
+
apiKey;
|
|
166
|
+
baseUrl;
|
|
167
|
+
constructor(config) {
|
|
168
|
+
super(config);
|
|
169
|
+
if (!config.apiKey) {
|
|
170
|
+
throw new Error("Google AI API key required");
|
|
171
|
+
}
|
|
172
|
+
this.apiKey = config.apiKey;
|
|
173
|
+
this.baseUrl = config.endpoint || "https://generativelanguage.googleapis.com/v1beta";
|
|
174
|
+
}
|
|
175
|
+
async chat(messages) {
|
|
176
|
+
const model = this.config.model || "gemini-2.5-flash";
|
|
177
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
178
|
+
const chatMessages = messages.filter((m) => m.role !== "system");
|
|
179
|
+
const contents = chatMessages.map((m) => ({
|
|
180
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
181
|
+
parts: [{ text: m.content }]
|
|
182
|
+
}));
|
|
183
|
+
const body = {
|
|
184
|
+
contents,
|
|
185
|
+
generationConfig: {
|
|
186
|
+
temperature: this.config.temperature,
|
|
187
|
+
maxOutputTokens: this.config.maxTokens || 4096
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
if (systemMessage) {
|
|
191
|
+
body.systemInstruction = {
|
|
192
|
+
parts: [{ text: systemMessage.content }]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const url = `${this.baseUrl}/models/${model}:generateContent?key=${this.apiKey}`;
|
|
196
|
+
const response = await fetch(url, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json"
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify(body)
|
|
202
|
+
});
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
const error = await response.text();
|
|
205
|
+
throw new Error(`Google AI API error: ${response.status} - ${error}`);
|
|
206
|
+
}
|
|
207
|
+
const data = await response.json();
|
|
208
|
+
const candidate = data.candidates?.[0];
|
|
209
|
+
if (!candidate?.content?.parts) {
|
|
210
|
+
throw new Error("No response from Google AI");
|
|
211
|
+
}
|
|
212
|
+
const content = candidate.content.parts.map((part) => part.text || "").join("");
|
|
213
|
+
const usageMetadata = data.usageMetadata;
|
|
214
|
+
return {
|
|
215
|
+
content,
|
|
216
|
+
usage: usageMetadata ? {
|
|
217
|
+
promptTokens: usageMetadata.promptTokenCount || 0,
|
|
218
|
+
completionTokens: usageMetadata.candidatesTokenCount || 0,
|
|
219
|
+
totalTokens: usageMetadata.totalTokenCount || 0
|
|
220
|
+
} : void 0
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/llm/deepseek.ts
|
|
226
|
+
var import_openai2 = __toESM(require("openai"));
|
|
227
|
+
var DeepSeekAdapter = class extends BaseLLMAdapter {
|
|
228
|
+
client;
|
|
229
|
+
constructor(config) {
|
|
230
|
+
super(config);
|
|
231
|
+
if (!config.apiKey) {
|
|
232
|
+
throw new Error("DeepSeek API key required");
|
|
233
|
+
}
|
|
234
|
+
this.client = new import_openai2.default({
|
|
235
|
+
apiKey: config.apiKey,
|
|
236
|
+
baseURL: config.endpoint || "https://api.deepseek.com/v1"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async chat(messages) {
|
|
240
|
+
try {
|
|
241
|
+
const response = await this.client.chat.completions.create({
|
|
242
|
+
model: this.config.model || "deepseek-chat",
|
|
243
|
+
messages: messages.map((m) => ({
|
|
244
|
+
role: m.role,
|
|
245
|
+
content: m.content
|
|
246
|
+
})),
|
|
247
|
+
temperature: this.config.temperature,
|
|
248
|
+
max_tokens: this.config.maxTokens
|
|
249
|
+
});
|
|
250
|
+
const choice = response.choices[0];
|
|
251
|
+
if (!choice || !choice.message) {
|
|
252
|
+
throw new Error("No response from DeepSeek");
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
content: choice.message.content || "",
|
|
256
|
+
usage: response.usage ? {
|
|
257
|
+
promptTokens: response.usage.prompt_tokens,
|
|
258
|
+
completionTokens: response.usage.completion_tokens,
|
|
259
|
+
totalTokens: response.usage.total_tokens
|
|
260
|
+
} : void 0
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof import_openai2.default.APIError) {
|
|
264
|
+
throw new Error(`DeepSeek API error: ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/llm/mistral.ts
|
|
272
|
+
var MistralAdapter = class extends BaseLLMAdapter {
|
|
273
|
+
apiKey;
|
|
274
|
+
baseUrl;
|
|
275
|
+
constructor(config) {
|
|
276
|
+
super(config);
|
|
277
|
+
if (!config.apiKey) {
|
|
278
|
+
throw new Error("Mistral API key required");
|
|
279
|
+
}
|
|
280
|
+
this.apiKey = config.apiKey;
|
|
281
|
+
this.baseUrl = config.endpoint || "https://api.mistral.ai/v1";
|
|
282
|
+
}
|
|
283
|
+
async chat(messages) {
|
|
284
|
+
const body = {
|
|
285
|
+
model: this.config.model || "mistral-large-latest",
|
|
286
|
+
messages: messages.map((m) => ({
|
|
287
|
+
role: m.role,
|
|
288
|
+
content: m.content
|
|
289
|
+
})),
|
|
290
|
+
temperature: this.config.temperature,
|
|
291
|
+
max_tokens: this.config.maxTokens
|
|
292
|
+
};
|
|
293
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: {
|
|
296
|
+
"Content-Type": "application/json",
|
|
297
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify(body)
|
|
300
|
+
});
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const error = await response.text();
|
|
303
|
+
throw new Error(`Mistral API error: ${response.status} - ${error}`);
|
|
304
|
+
}
|
|
305
|
+
const data = await response.json();
|
|
306
|
+
const choice = data.choices?.[0];
|
|
307
|
+
if (!choice || !choice.message) {
|
|
308
|
+
throw new Error("No response from Mistral");
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
content: choice.message.content || "",
|
|
312
|
+
usage: data.usage ? {
|
|
313
|
+
promptTokens: data.usage.prompt_tokens,
|
|
314
|
+
completionTokens: data.usage.completion_tokens,
|
|
315
|
+
totalTokens: data.usage.total_tokens
|
|
316
|
+
} : void 0
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/llm/groq.ts
|
|
322
|
+
var import_openai3 = __toESM(require("openai"));
|
|
323
|
+
var GroqAdapter = class extends BaseLLMAdapter {
|
|
324
|
+
client;
|
|
325
|
+
constructor(config) {
|
|
326
|
+
super(config);
|
|
327
|
+
if (!config.apiKey) {
|
|
328
|
+
throw new Error("Groq API key required");
|
|
329
|
+
}
|
|
330
|
+
this.client = new import_openai3.default({
|
|
331
|
+
apiKey: config.apiKey,
|
|
332
|
+
baseURL: config.endpoint || "https://api.groq.com/openai/v1"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async chat(messages) {
|
|
336
|
+
try {
|
|
337
|
+
const response = await this.client.chat.completions.create({
|
|
338
|
+
model: this.config.model || "llama-3.1-70b-versatile",
|
|
339
|
+
messages: messages.map((m) => ({
|
|
340
|
+
role: m.role,
|
|
341
|
+
content: m.content
|
|
342
|
+
})),
|
|
343
|
+
temperature: this.config.temperature,
|
|
344
|
+
max_tokens: this.config.maxTokens
|
|
345
|
+
});
|
|
346
|
+
const choice = response.choices[0];
|
|
347
|
+
if (!choice || !choice.message) {
|
|
348
|
+
throw new Error("No response from Groq");
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
content: choice.message.content || "",
|
|
352
|
+
usage: response.usage ? {
|
|
353
|
+
promptTokens: response.usage.prompt_tokens,
|
|
354
|
+
completionTokens: response.usage.completion_tokens,
|
|
355
|
+
totalTokens: response.usage.total_tokens
|
|
356
|
+
} : void 0
|
|
357
|
+
};
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof import_openai3.default.APIError) {
|
|
360
|
+
throw new Error(`Groq API error: ${error.message}`);
|
|
361
|
+
}
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/llm/together.ts
|
|
368
|
+
var import_openai4 = __toESM(require("openai"));
|
|
369
|
+
var TogetherAdapter = class extends BaseLLMAdapter {
|
|
370
|
+
client;
|
|
371
|
+
constructor(config) {
|
|
372
|
+
super(config);
|
|
373
|
+
if (!config.apiKey) {
|
|
374
|
+
throw new Error("Together AI API key required");
|
|
375
|
+
}
|
|
376
|
+
this.client = new import_openai4.default({
|
|
377
|
+
apiKey: config.apiKey,
|
|
378
|
+
baseURL: config.endpoint || "https://api.together.xyz/v1"
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
async chat(messages) {
|
|
382
|
+
try {
|
|
383
|
+
const response = await this.client.chat.completions.create({
|
|
384
|
+
model: this.config.model || "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
|
|
385
|
+
messages: messages.map((m) => ({
|
|
386
|
+
role: m.role,
|
|
387
|
+
content: m.content
|
|
388
|
+
})),
|
|
389
|
+
temperature: this.config.temperature,
|
|
390
|
+
max_tokens: this.config.maxTokens
|
|
391
|
+
});
|
|
392
|
+
const choice = response.choices[0];
|
|
393
|
+
if (!choice || !choice.message) {
|
|
394
|
+
throw new Error("No response from Together AI");
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
content: choice.message.content || "",
|
|
398
|
+
usage: response.usage ? {
|
|
399
|
+
promptTokens: response.usage.prompt_tokens,
|
|
400
|
+
completionTokens: response.usage.completion_tokens,
|
|
401
|
+
totalTokens: response.usage.total_tokens
|
|
402
|
+
} : void 0
|
|
403
|
+
};
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error instanceof import_openai4.default.APIError) {
|
|
406
|
+
throw new Error(`Together AI API error: ${error.message}`);
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
161
413
|
// src/llm/ollama.ts
|
|
162
414
|
var OllamaAdapter = class extends BaseLLMAdapter {
|
|
163
415
|
baseUrl;
|
|
@@ -190,7 +442,7 @@ var OllamaAdapter = class extends BaseLLMAdapter {
|
|
|
190
442
|
}
|
|
191
443
|
async chat(messages) {
|
|
192
444
|
const body = {
|
|
193
|
-
model: this.config.model || "
|
|
445
|
+
model: this.config.model || "llama3.2",
|
|
194
446
|
messages: messages.map((m) => ({
|
|
195
447
|
role: m.role,
|
|
196
448
|
content: m.content
|
|
@@ -225,7 +477,7 @@ var OllamaAdapter = class extends BaseLLMAdapter {
|
|
|
225
477
|
getMetadata() {
|
|
226
478
|
return {
|
|
227
479
|
provider: "ollama",
|
|
228
|
-
model: this.config.model || "
|
|
480
|
+
model: this.config.model || "llama3.2",
|
|
229
481
|
isLocal: true
|
|
230
482
|
};
|
|
231
483
|
}
|
|
@@ -238,6 +490,16 @@ async function createLLMAdapter(config) {
|
|
|
238
490
|
return new OpenAIAdapter(config);
|
|
239
491
|
case "anthropic":
|
|
240
492
|
return new AnthropicAdapter(config);
|
|
493
|
+
case "google":
|
|
494
|
+
return new GoogleAdapter(config);
|
|
495
|
+
case "deepseek":
|
|
496
|
+
return new DeepSeekAdapter(config);
|
|
497
|
+
case "mistral":
|
|
498
|
+
return new MistralAdapter(config);
|
|
499
|
+
case "groq":
|
|
500
|
+
return new GroqAdapter(config);
|
|
501
|
+
case "together":
|
|
502
|
+
return new TogetherAdapter(config);
|
|
241
503
|
case "ollama":
|
|
242
504
|
const adapter = new OllamaAdapter(config);
|
|
243
505
|
await adapter.healthCheck();
|
|
@@ -291,7 +553,7 @@ async function loadStrategy(strategyPath) {
|
|
|
291
553
|
console.log("No custom strategy found, using default (hold) strategy");
|
|
292
554
|
return defaultStrategy;
|
|
293
555
|
}
|
|
294
|
-
async function loadTypeScriptModule(
|
|
556
|
+
async function loadTypeScriptModule(path3) {
|
|
295
557
|
try {
|
|
296
558
|
const tsxPath = require.resolve("tsx");
|
|
297
559
|
const { pathToFileURL } = await import("url");
|
|
@@ -302,7 +564,7 @@ async function loadTypeScriptModule(path2) {
|
|
|
302
564
|
"--import",
|
|
303
565
|
"tsx/esm",
|
|
304
566
|
"-e",
|
|
305
|
-
`import('${pathToFileURL(
|
|
567
|
+
`import('${pathToFileURL(path3).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
|
|
306
568
|
],
|
|
307
569
|
{
|
|
308
570
|
cwd: process.cwd(),
|
|
@@ -325,7 +587,7 @@ async function loadTypeScriptModule(path2) {
|
|
|
325
587
|
const tsx = await import("tsx/esm/api");
|
|
326
588
|
const unregister = tsx.register();
|
|
327
589
|
try {
|
|
328
|
-
const module2 = await import(
|
|
590
|
+
const module2 = await import(path3);
|
|
329
591
|
return module2;
|
|
330
592
|
} finally {
|
|
331
593
|
unregister();
|
|
@@ -1119,7 +1381,268 @@ var VaultManager = class {
|
|
|
1119
1381
|
}
|
|
1120
1382
|
};
|
|
1121
1383
|
|
|
1384
|
+
// src/relay.ts
|
|
1385
|
+
var import_ws = __toESM(require("ws"));
|
|
1386
|
+
var import_accounts2 = require("viem/accounts");
|
|
1387
|
+
var RelayClient = class {
|
|
1388
|
+
config;
|
|
1389
|
+
ws = null;
|
|
1390
|
+
authenticated = false;
|
|
1391
|
+
reconnectAttempts = 0;
|
|
1392
|
+
maxReconnectAttempts = 50;
|
|
1393
|
+
reconnectTimer = null;
|
|
1394
|
+
heartbeatTimer = null;
|
|
1395
|
+
stopped = false;
|
|
1396
|
+
constructor(config) {
|
|
1397
|
+
this.config = config;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Connect to the relay server
|
|
1401
|
+
*/
|
|
1402
|
+
async connect() {
|
|
1403
|
+
if (this.stopped) return;
|
|
1404
|
+
const wsUrl = this.config.relay.apiUrl.replace(/^https?:\/\//, (m) => m.includes("https") ? "wss://" : "ws://").replace(/\/$/, "") + "/ws/agent";
|
|
1405
|
+
return new Promise((resolve, reject) => {
|
|
1406
|
+
try {
|
|
1407
|
+
this.ws = new import_ws.default(wsUrl);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
console.error("Relay: Failed to create WebSocket:", error);
|
|
1410
|
+
this.scheduleReconnect();
|
|
1411
|
+
reject(error);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const connectTimeout = setTimeout(() => {
|
|
1415
|
+
if (!this.authenticated) {
|
|
1416
|
+
console.error("Relay: Connection timeout");
|
|
1417
|
+
this.ws?.close();
|
|
1418
|
+
this.scheduleReconnect();
|
|
1419
|
+
reject(new Error("Connection timeout"));
|
|
1420
|
+
}
|
|
1421
|
+
}, 15e3);
|
|
1422
|
+
this.ws.on("open", async () => {
|
|
1423
|
+
console.log("Relay: Connected, authenticating...");
|
|
1424
|
+
this.reconnectAttempts = 0;
|
|
1425
|
+
try {
|
|
1426
|
+
await this.authenticate();
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
console.error("Relay: Authentication failed:", error);
|
|
1429
|
+
this.ws?.close();
|
|
1430
|
+
clearTimeout(connectTimeout);
|
|
1431
|
+
reject(error);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
this.ws.on("message", (raw) => {
|
|
1435
|
+
try {
|
|
1436
|
+
const data = JSON.parse(raw.toString());
|
|
1437
|
+
this.handleMessage(data);
|
|
1438
|
+
if (data.type === "auth_success") {
|
|
1439
|
+
clearTimeout(connectTimeout);
|
|
1440
|
+
this.authenticated = true;
|
|
1441
|
+
this.startHeartbeat();
|
|
1442
|
+
console.log("Relay: Authenticated successfully");
|
|
1443
|
+
resolve();
|
|
1444
|
+
} else if (data.type === "auth_error") {
|
|
1445
|
+
clearTimeout(connectTimeout);
|
|
1446
|
+
console.error(`Relay: Auth rejected: ${data.message}`);
|
|
1447
|
+
reject(new Error(data.message));
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
this.ws.on("close", (code, reason) => {
|
|
1453
|
+
clearTimeout(connectTimeout);
|
|
1454
|
+
this.authenticated = false;
|
|
1455
|
+
this.stopHeartbeat();
|
|
1456
|
+
if (!this.stopped) {
|
|
1457
|
+
console.log(`Relay: Disconnected (${code}: ${reason.toString() || "unknown"})`);
|
|
1458
|
+
this.scheduleReconnect();
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
this.ws.on("error", (error) => {
|
|
1462
|
+
if (!this.stopped) {
|
|
1463
|
+
console.error("Relay: WebSocket error:", error.message);
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Authenticate with the relay server using wallet signature
|
|
1470
|
+
*/
|
|
1471
|
+
async authenticate() {
|
|
1472
|
+
const account = (0, import_accounts2.privateKeyToAccount)(this.config.privateKey);
|
|
1473
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
1474
|
+
const message = `ExagentRelay:${this.config.agentId}:${timestamp}`;
|
|
1475
|
+
const signature = await (0, import_accounts2.signMessage)({
|
|
1476
|
+
message,
|
|
1477
|
+
privateKey: this.config.privateKey
|
|
1478
|
+
});
|
|
1479
|
+
this.send({
|
|
1480
|
+
type: "auth",
|
|
1481
|
+
agentId: this.config.agentId,
|
|
1482
|
+
wallet: account.address,
|
|
1483
|
+
timestamp,
|
|
1484
|
+
signature
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Handle incoming messages from the relay server
|
|
1489
|
+
*/
|
|
1490
|
+
handleMessage(data) {
|
|
1491
|
+
switch (data.type) {
|
|
1492
|
+
case "command":
|
|
1493
|
+
if (data.command && this.config.onCommand) {
|
|
1494
|
+
this.config.onCommand(data.command);
|
|
1495
|
+
}
|
|
1496
|
+
break;
|
|
1497
|
+
case "auth_success":
|
|
1498
|
+
case "auth_error":
|
|
1499
|
+
break;
|
|
1500
|
+
case "error":
|
|
1501
|
+
console.error(`Relay: Server error: ${data.message}`);
|
|
1502
|
+
break;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Send a status heartbeat
|
|
1507
|
+
*/
|
|
1508
|
+
sendHeartbeat(status) {
|
|
1509
|
+
if (!this.authenticated) return;
|
|
1510
|
+
this.send({
|
|
1511
|
+
type: "heartbeat",
|
|
1512
|
+
agentId: this.config.agentId,
|
|
1513
|
+
status
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Send a status update (outside of regular heartbeat)
|
|
1518
|
+
*/
|
|
1519
|
+
sendStatusUpdate(status) {
|
|
1520
|
+
if (!this.authenticated) return;
|
|
1521
|
+
this.send({
|
|
1522
|
+
type: "status_update",
|
|
1523
|
+
agentId: this.config.agentId,
|
|
1524
|
+
status
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Send a message to the command center
|
|
1529
|
+
*/
|
|
1530
|
+
sendMessage(messageType, level, title, body, data) {
|
|
1531
|
+
if (!this.authenticated) return;
|
|
1532
|
+
this.send({
|
|
1533
|
+
type: "message",
|
|
1534
|
+
agentId: this.config.agentId,
|
|
1535
|
+
messageType,
|
|
1536
|
+
level,
|
|
1537
|
+
title,
|
|
1538
|
+
body,
|
|
1539
|
+
data
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Send a command execution result
|
|
1544
|
+
*/
|
|
1545
|
+
sendCommandResult(commandId, success, result) {
|
|
1546
|
+
if (!this.authenticated) return;
|
|
1547
|
+
this.send({
|
|
1548
|
+
type: "command_result",
|
|
1549
|
+
agentId: this.config.agentId,
|
|
1550
|
+
commandId,
|
|
1551
|
+
success,
|
|
1552
|
+
result
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Start the heartbeat timer
|
|
1557
|
+
*/
|
|
1558
|
+
startHeartbeat() {
|
|
1559
|
+
this.stopHeartbeat();
|
|
1560
|
+
const interval = this.config.relay.heartbeatIntervalMs || 3e4;
|
|
1561
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1562
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1563
|
+
this.ws.ping();
|
|
1564
|
+
}
|
|
1565
|
+
}, interval);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Stop the heartbeat timer
|
|
1569
|
+
*/
|
|
1570
|
+
stopHeartbeat() {
|
|
1571
|
+
if (this.heartbeatTimer) {
|
|
1572
|
+
clearInterval(this.heartbeatTimer);
|
|
1573
|
+
this.heartbeatTimer = null;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Schedule a reconnection with exponential backoff
|
|
1578
|
+
*/
|
|
1579
|
+
scheduleReconnect() {
|
|
1580
|
+
if (this.stopped) return;
|
|
1581
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1582
|
+
console.error("Relay: Max reconnection attempts reached. Giving up.");
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
1586
|
+
this.reconnectAttempts++;
|
|
1587
|
+
console.log(
|
|
1588
|
+
`Relay: Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
1589
|
+
);
|
|
1590
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1591
|
+
this.connect().catch(() => {
|
|
1592
|
+
});
|
|
1593
|
+
}, delay);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Send a JSON message to the WebSocket
|
|
1597
|
+
*/
|
|
1598
|
+
send(data) {
|
|
1599
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1600
|
+
this.ws.send(JSON.stringify(data));
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Check if connected and authenticated
|
|
1605
|
+
*/
|
|
1606
|
+
get isConnected() {
|
|
1607
|
+
return this.authenticated && this.ws?.readyState === import_ws.default.OPEN;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Disconnect and stop reconnecting
|
|
1611
|
+
*/
|
|
1612
|
+
disconnect() {
|
|
1613
|
+
this.stopped = true;
|
|
1614
|
+
this.stopHeartbeat();
|
|
1615
|
+
if (this.reconnectTimer) {
|
|
1616
|
+
clearTimeout(this.reconnectTimer);
|
|
1617
|
+
this.reconnectTimer = null;
|
|
1618
|
+
}
|
|
1619
|
+
if (this.ws) {
|
|
1620
|
+
this.ws.close(1e3, "Agent shutting down");
|
|
1621
|
+
this.ws = null;
|
|
1622
|
+
}
|
|
1623
|
+
this.authenticated = false;
|
|
1624
|
+
console.log("Relay: Disconnected");
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// src/browser-open.ts
|
|
1629
|
+
var import_child_process2 = require("child_process");
|
|
1630
|
+
function openBrowser(url) {
|
|
1631
|
+
const platform = process.platform;
|
|
1632
|
+
try {
|
|
1633
|
+
if (platform === "darwin") {
|
|
1634
|
+
(0, import_child_process2.exec)(`open "${url}"`);
|
|
1635
|
+
} else if (platform === "win32") {
|
|
1636
|
+
(0, import_child_process2.exec)(`start "" "${url}"`);
|
|
1637
|
+
} else {
|
|
1638
|
+
(0, import_child_process2.exec)(`xdg-open "${url}"`);
|
|
1639
|
+
}
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1122
1644
|
// src/runtime.ts
|
|
1645
|
+
var FUNDS_LOW_THRESHOLD = 5e-3;
|
|
1123
1646
|
var AgentRuntime = class {
|
|
1124
1647
|
config;
|
|
1125
1648
|
client;
|
|
@@ -1129,9 +1652,14 @@ var AgentRuntime = class {
|
|
|
1129
1652
|
riskManager;
|
|
1130
1653
|
marketData;
|
|
1131
1654
|
vaultManager;
|
|
1655
|
+
relay = null;
|
|
1132
1656
|
isRunning = false;
|
|
1657
|
+
mode = "idle";
|
|
1133
1658
|
configHash;
|
|
1134
1659
|
lastVaultCheck = 0;
|
|
1660
|
+
cycleCount = 0;
|
|
1661
|
+
lastCycleAt = 0;
|
|
1662
|
+
processAlive = true;
|
|
1135
1663
|
VAULT_CHECK_INTERVAL = 3e5;
|
|
1136
1664
|
// Check vault status every 5 minutes
|
|
1137
1665
|
constructor(config) {
|
|
@@ -1163,8 +1691,44 @@ var AgentRuntime = class {
|
|
|
1163
1691
|
this.riskManager = new RiskManager(this.config.trading);
|
|
1164
1692
|
this.marketData = new MarketDataService(this.getRpcUrl());
|
|
1165
1693
|
await this.initializeVaultManager();
|
|
1694
|
+
await this.initializeRelay();
|
|
1166
1695
|
console.log("Agent initialized successfully");
|
|
1167
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Initialize the relay client for command center connectivity
|
|
1699
|
+
*/
|
|
1700
|
+
async initializeRelay() {
|
|
1701
|
+
const relayConfig = this.config.relay;
|
|
1702
|
+
const relayEnabled = process.env.EXAGENT_RELAY_ENABLED !== "false";
|
|
1703
|
+
if (!relayConfig?.enabled || !relayEnabled) {
|
|
1704
|
+
console.log("Relay: Disabled");
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
const apiUrl = process.env.EXAGENT_API_URL || relayConfig.apiUrl;
|
|
1708
|
+
if (!apiUrl) {
|
|
1709
|
+
console.log("Relay: No API URL configured, skipping");
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
this.relay = new RelayClient({
|
|
1713
|
+
agentId: String(this.config.agentId),
|
|
1714
|
+
privateKey: this.config.privateKey,
|
|
1715
|
+
relay: {
|
|
1716
|
+
...relayConfig,
|
|
1717
|
+
apiUrl
|
|
1718
|
+
},
|
|
1719
|
+
onCommand: (cmd) => this.handleCommand(cmd)
|
|
1720
|
+
});
|
|
1721
|
+
try {
|
|
1722
|
+
await this.relay.connect();
|
|
1723
|
+
console.log("Relay: Connected to command center");
|
|
1724
|
+
this.sendRelayStatus();
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
console.warn(
|
|
1727
|
+
"Relay: Failed to connect (agent will work locally):",
|
|
1728
|
+
error instanceof Error ? error.message : error
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1168
1732
|
/**
|
|
1169
1733
|
* Initialize the vault manager based on config
|
|
1170
1734
|
*/
|
|
@@ -1194,7 +1758,9 @@ var AgentRuntime = class {
|
|
|
1194
1758
|
}
|
|
1195
1759
|
}
|
|
1196
1760
|
/**
|
|
1197
|
-
* Ensure the current wallet is linked to the agent
|
|
1761
|
+
* Ensure the current wallet is linked to the agent.
|
|
1762
|
+
* If the trading wallet differs from the owner, enters a recovery loop
|
|
1763
|
+
* that waits for the owner to link it from the website.
|
|
1198
1764
|
*/
|
|
1199
1765
|
async ensureWalletLinked() {
|
|
1200
1766
|
const agentId = BigInt(this.config.agentId);
|
|
@@ -1204,9 +1770,31 @@ var AgentRuntime = class {
|
|
|
1204
1770
|
console.log("Wallet not linked, linking now...");
|
|
1205
1771
|
const agent = await this.client.registry.getAgent(agentId);
|
|
1206
1772
|
if (agent?.owner.toLowerCase() !== address.toLowerCase()) {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
);
|
|
1773
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
1774
|
+
console.log("");
|
|
1775
|
+
console.log("=== WALLET LINKING REQUIRED ===");
|
|
1776
|
+
console.log("");
|
|
1777
|
+
console.log(` Agent owner: ${agent?.owner}`);
|
|
1778
|
+
console.log(` Trading wallet: ${address}`);
|
|
1779
|
+
console.log("");
|
|
1780
|
+
console.log(" Your trading wallet needs to be linked to your agent.");
|
|
1781
|
+
console.log(" Opening the command center in your browser...");
|
|
1782
|
+
console.log(` ${ccUrl}`);
|
|
1783
|
+
console.log("");
|
|
1784
|
+
openBrowser(ccUrl);
|
|
1785
|
+
console.log(" Waiting for wallet to be linked... (checking every 15s)");
|
|
1786
|
+
console.log(" Press Ctrl+C to exit.");
|
|
1787
|
+
console.log("");
|
|
1788
|
+
while (true) {
|
|
1789
|
+
await this.sleep(15e3);
|
|
1790
|
+
const linked = await this.client.registry.isLinkedWallet(agentId, address);
|
|
1791
|
+
if (linked) {
|
|
1792
|
+
console.log(" Wallet linked! Continuing setup...");
|
|
1793
|
+
console.log("");
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
process.stdout.write(".");
|
|
1797
|
+
}
|
|
1210
1798
|
}
|
|
1211
1799
|
await this.client.registry.linkOwnWallet(agentId);
|
|
1212
1800
|
console.log("Wallet linked successfully");
|
|
@@ -1215,8 +1803,9 @@ var AgentRuntime = class {
|
|
|
1215
1803
|
}
|
|
1216
1804
|
}
|
|
1217
1805
|
/**
|
|
1218
|
-
* Sync the LLM config hash to chain for epoch tracking
|
|
1219
|
-
*
|
|
1806
|
+
* Sync the LLM config hash to chain for epoch tracking.
|
|
1807
|
+
* If the wallet has insufficient gas, enters a recovery loop
|
|
1808
|
+
* that waits for the user to fund the wallet.
|
|
1220
1809
|
*/
|
|
1221
1810
|
async syncConfigHash() {
|
|
1222
1811
|
const agentId = BigInt(this.config.agentId);
|
|
@@ -1226,9 +1815,50 @@ var AgentRuntime = class {
|
|
|
1226
1815
|
const onChainHash = await this.client.registry.getConfigHash(agentId);
|
|
1227
1816
|
if (onChainHash !== this.configHash) {
|
|
1228
1817
|
console.log("Config changed, updating on-chain...");
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1818
|
+
try {
|
|
1819
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
1820
|
+
const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
1821
|
+
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1824
|
+
if (message.includes("insufficient funds") || message.includes("gas") || message.includes("intrinsic gas too low") || message.includes("exceeds the balance")) {
|
|
1825
|
+
const ccUrl = `https://exagent.io/agents/${encodeURIComponent(this.config.name)}/command-center`;
|
|
1826
|
+
const chain = this.config.network === "mainnet" ? import_chains2.base : import_chains2.baseSepolia;
|
|
1827
|
+
const publicClientInstance = (0, import_viem2.createPublicClient)({
|
|
1828
|
+
chain,
|
|
1829
|
+
transport: (0, import_viem2.http)(this.getRpcUrl())
|
|
1830
|
+
});
|
|
1831
|
+
console.log("");
|
|
1832
|
+
console.log("=== ETH NEEDED FOR GAS ===");
|
|
1833
|
+
console.log("");
|
|
1834
|
+
console.log(` Wallet: ${this.client.address}`);
|
|
1835
|
+
console.log(" Your wallet needs ETH to pay for transaction gas.");
|
|
1836
|
+
console.log(" Opening the command center to fund your wallet...");
|
|
1837
|
+
console.log(` ${ccUrl}`);
|
|
1838
|
+
console.log("");
|
|
1839
|
+
openBrowser(ccUrl);
|
|
1840
|
+
console.log(" Waiting for ETH... (checking every 15s)");
|
|
1841
|
+
console.log(" Press Ctrl+C to exit.");
|
|
1842
|
+
console.log("");
|
|
1843
|
+
while (true) {
|
|
1844
|
+
await this.sleep(15e3);
|
|
1845
|
+
const balance = await publicClientInstance.getBalance({
|
|
1846
|
+
address: this.client.address
|
|
1847
|
+
});
|
|
1848
|
+
if (balance > BigInt(0)) {
|
|
1849
|
+
console.log(" ETH detected! Retrying config update...");
|
|
1850
|
+
console.log("");
|
|
1851
|
+
await this.client.registry.updateConfig(agentId, this.configHash);
|
|
1852
|
+
const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
1853
|
+
console.log(`Config updated, new epoch started: ${newEpoch}`);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
process.stdout.write(".");
|
|
1857
|
+
}
|
|
1858
|
+
} else {
|
|
1859
|
+
throw error;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1232
1862
|
} else {
|
|
1233
1863
|
const currentEpoch = await this.client.registry.getCurrentEpoch(agentId);
|
|
1234
1864
|
console.log(`Config hash matches on-chain (epoch ${currentEpoch})`);
|
|
@@ -1241,48 +1871,301 @@ var AgentRuntime = class {
|
|
|
1241
1871
|
return this.configHash;
|
|
1242
1872
|
}
|
|
1243
1873
|
/**
|
|
1244
|
-
* Start the
|
|
1874
|
+
* Start the agent in daemon mode.
|
|
1875
|
+
* The agent enters idle mode and waits for commands from the command center.
|
|
1876
|
+
* Trading begins only when a start_trading command is received.
|
|
1877
|
+
*
|
|
1878
|
+
* If relay is not configured, falls back to immediate trading mode.
|
|
1245
1879
|
*/
|
|
1246
1880
|
async run() {
|
|
1247
|
-
|
|
1248
|
-
|
|
1881
|
+
this.processAlive = true;
|
|
1882
|
+
if (this.relay) {
|
|
1883
|
+
console.log("");
|
|
1884
|
+
console.log("Agent is in IDLE mode. Waiting for commands from command center.");
|
|
1885
|
+
console.log("Visit https://exagent.io to start trading from the dashboard.");
|
|
1886
|
+
console.log("");
|
|
1887
|
+
this.mode = "idle";
|
|
1888
|
+
this.sendRelayStatus();
|
|
1889
|
+
this.relay.sendMessage(
|
|
1890
|
+
"system",
|
|
1891
|
+
"success",
|
|
1892
|
+
"Agent Connected",
|
|
1893
|
+
`${this.config.name} is online and waiting for commands.`,
|
|
1894
|
+
{ wallet: this.client.address }
|
|
1895
|
+
);
|
|
1896
|
+
while (this.processAlive) {
|
|
1897
|
+
if (this.mode === "trading" && this.isRunning) {
|
|
1898
|
+
try {
|
|
1899
|
+
await this.runCycle();
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1902
|
+
console.error("Error in trading cycle:", message);
|
|
1903
|
+
this.relay?.sendMessage(
|
|
1904
|
+
"system",
|
|
1905
|
+
"error",
|
|
1906
|
+
"Cycle Error",
|
|
1907
|
+
message
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
await this.sleep(this.config.trading.tradingIntervalMs);
|
|
1911
|
+
} else {
|
|
1912
|
+
this.sendRelayStatus();
|
|
1913
|
+
await this.sleep(3e4);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
} else {
|
|
1917
|
+
if (this.isRunning) {
|
|
1918
|
+
throw new Error("Agent is already running");
|
|
1919
|
+
}
|
|
1920
|
+
this.isRunning = true;
|
|
1921
|
+
this.mode = "trading";
|
|
1922
|
+
console.log("Starting trading loop...");
|
|
1923
|
+
console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
|
|
1924
|
+
while (this.isRunning) {
|
|
1925
|
+
try {
|
|
1926
|
+
await this.runCycle();
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
console.error("Error in trading cycle:", error);
|
|
1929
|
+
}
|
|
1930
|
+
await this.sleep(this.config.trading.tradingIntervalMs);
|
|
1931
|
+
}
|
|
1249
1932
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Handle a command from the command center
|
|
1936
|
+
*/
|
|
1937
|
+
async handleCommand(cmd) {
|
|
1938
|
+
console.log(`Command received: ${cmd.type}`);
|
|
1939
|
+
try {
|
|
1940
|
+
switch (cmd.type) {
|
|
1941
|
+
case "start_trading":
|
|
1942
|
+
if (this.mode === "trading") {
|
|
1943
|
+
this.relay?.sendCommandResult(cmd.id, true, "Already trading");
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
this.mode = "trading";
|
|
1947
|
+
this.isRunning = true;
|
|
1948
|
+
console.log("Trading started via command center");
|
|
1949
|
+
this.relay?.sendCommandResult(cmd.id, true, "Trading started");
|
|
1950
|
+
this.relay?.sendMessage(
|
|
1951
|
+
"system",
|
|
1952
|
+
"success",
|
|
1953
|
+
"Trading Started",
|
|
1954
|
+
"Agent is now actively trading."
|
|
1955
|
+
);
|
|
1956
|
+
this.sendRelayStatus();
|
|
1957
|
+
break;
|
|
1958
|
+
case "stop_trading":
|
|
1959
|
+
if (this.mode === "idle") {
|
|
1960
|
+
this.relay?.sendCommandResult(cmd.id, true, "Already idle");
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
this.mode = "idle";
|
|
1964
|
+
this.isRunning = false;
|
|
1965
|
+
console.log("Trading stopped via command center");
|
|
1966
|
+
this.relay?.sendCommandResult(cmd.id, true, "Trading stopped");
|
|
1967
|
+
this.relay?.sendMessage(
|
|
1968
|
+
"system",
|
|
1969
|
+
"info",
|
|
1970
|
+
"Trading Stopped",
|
|
1971
|
+
"Agent is now idle. Send start_trading to resume."
|
|
1972
|
+
);
|
|
1973
|
+
this.sendRelayStatus();
|
|
1974
|
+
break;
|
|
1975
|
+
case "update_risk_params": {
|
|
1976
|
+
const params = cmd.params || {};
|
|
1977
|
+
if (params.maxPositionSizeBps !== void 0) {
|
|
1978
|
+
this.config.trading.maxPositionSizeBps = Number(params.maxPositionSizeBps);
|
|
1979
|
+
}
|
|
1980
|
+
if (params.maxDailyLossBps !== void 0) {
|
|
1981
|
+
this.config.trading.maxDailyLossBps = Number(params.maxDailyLossBps);
|
|
1982
|
+
}
|
|
1983
|
+
this.riskManager = new RiskManager(this.config.trading);
|
|
1984
|
+
console.log("Risk params updated via command center");
|
|
1985
|
+
this.relay?.sendCommandResult(cmd.id, true, "Risk params updated");
|
|
1986
|
+
this.relay?.sendMessage(
|
|
1987
|
+
"config_updated",
|
|
1988
|
+
"info",
|
|
1989
|
+
"Risk Parameters Updated",
|
|
1990
|
+
`Max position: ${this.config.trading.maxPositionSizeBps / 100}%, Max daily loss: ${this.config.trading.maxDailyLossBps / 100}%`
|
|
1991
|
+
);
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
case "update_trading_interval": {
|
|
1995
|
+
const intervalMs = Number(cmd.params?.intervalMs);
|
|
1996
|
+
if (intervalMs && intervalMs >= 1e3) {
|
|
1997
|
+
this.config.trading.tradingIntervalMs = intervalMs;
|
|
1998
|
+
console.log(`Trading interval updated to ${intervalMs}ms`);
|
|
1999
|
+
this.relay?.sendCommandResult(cmd.id, true, `Interval set to ${intervalMs}ms`);
|
|
2000
|
+
} else {
|
|
2001
|
+
this.relay?.sendCommandResult(cmd.id, false, "Invalid interval (minimum 1000ms)");
|
|
2002
|
+
}
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
case "create_vault": {
|
|
2006
|
+
const result = await this.createVault();
|
|
2007
|
+
this.relay?.sendCommandResult(
|
|
2008
|
+
cmd.id,
|
|
2009
|
+
result.success,
|
|
2010
|
+
result.success ? `Vault created: ${result.vaultAddress}` : result.error
|
|
2011
|
+
);
|
|
2012
|
+
if (result.success) {
|
|
2013
|
+
this.relay?.sendMessage(
|
|
2014
|
+
"vault_created",
|
|
2015
|
+
"success",
|
|
2016
|
+
"Vault Created",
|
|
2017
|
+
`Vault deployed at ${result.vaultAddress}`,
|
|
2018
|
+
{ vaultAddress: result.vaultAddress }
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
case "refresh_status":
|
|
2024
|
+
this.sendRelayStatus();
|
|
2025
|
+
this.relay?.sendCommandResult(cmd.id, true, "Status refreshed");
|
|
2026
|
+
break;
|
|
2027
|
+
case "shutdown":
|
|
2028
|
+
console.log("Shutdown requested via command center");
|
|
2029
|
+
this.relay?.sendCommandResult(cmd.id, true, "Shutting down");
|
|
2030
|
+
this.relay?.sendMessage(
|
|
2031
|
+
"system",
|
|
2032
|
+
"info",
|
|
2033
|
+
"Shutting Down",
|
|
2034
|
+
"Agent is shutting down. Restart manually to reconnect."
|
|
2035
|
+
);
|
|
2036
|
+
await this.sleep(1e3);
|
|
2037
|
+
this.stop();
|
|
2038
|
+
break;
|
|
2039
|
+
default:
|
|
2040
|
+
console.warn(`Unknown command: ${cmd.type}`);
|
|
2041
|
+
this.relay?.sendCommandResult(cmd.id, false, `Unknown command: ${cmd.type}`);
|
|
1258
2042
|
}
|
|
1259
|
-
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2045
|
+
console.error(`Command ${cmd.type} failed:`, message);
|
|
2046
|
+
this.relay?.sendCommandResult(cmd.id, false, message);
|
|
1260
2047
|
}
|
|
1261
2048
|
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Send current status to the relay
|
|
2051
|
+
*/
|
|
2052
|
+
sendRelayStatus() {
|
|
2053
|
+
if (!this.relay) return;
|
|
2054
|
+
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
2055
|
+
const status = {
|
|
2056
|
+
mode: this.mode,
|
|
2057
|
+
agentId: String(this.config.agentId),
|
|
2058
|
+
wallet: this.client?.address,
|
|
2059
|
+
cycleCount: this.cycleCount,
|
|
2060
|
+
lastCycleAt: this.lastCycleAt,
|
|
2061
|
+
tradingIntervalMs: this.config.trading.tradingIntervalMs,
|
|
2062
|
+
llm: {
|
|
2063
|
+
provider: this.config.llm.provider,
|
|
2064
|
+
model: this.config.llm.model || "default"
|
|
2065
|
+
},
|
|
2066
|
+
risk: this.riskManager?.getStatus() || {
|
|
2067
|
+
dailyPnL: 0,
|
|
2068
|
+
dailyLossLimit: 0,
|
|
2069
|
+
isLimitHit: false
|
|
2070
|
+
},
|
|
2071
|
+
vault: {
|
|
2072
|
+
policy: vaultConfig.policy,
|
|
2073
|
+
hasVault: false,
|
|
2074
|
+
vaultAddress: null
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
this.relay.sendHeartbeat(status);
|
|
2078
|
+
}
|
|
1262
2079
|
/**
|
|
1263
2080
|
* Run a single trading cycle
|
|
1264
2081
|
*/
|
|
1265
2082
|
async runCycle() {
|
|
1266
2083
|
console.log(`
|
|
1267
2084
|
--- Trading Cycle: ${(/* @__PURE__ */ new Date()).toISOString()} ---`);
|
|
2085
|
+
this.cycleCount++;
|
|
2086
|
+
this.lastCycleAt = Date.now();
|
|
1268
2087
|
await this.checkVaultAutoCreation();
|
|
1269
2088
|
const tokens = this.config.allowedTokens || this.getDefaultTokens();
|
|
1270
2089
|
const marketData = await this.marketData.fetchMarketData(this.client.address, tokens);
|
|
1271
2090
|
console.log(`Portfolio value: $${marketData.portfolioValue.toFixed(2)}`);
|
|
1272
|
-
|
|
2091
|
+
this.checkFundsLow(marketData);
|
|
2092
|
+
let signals;
|
|
2093
|
+
try {
|
|
2094
|
+
signals = await this.strategy(marketData, this.llm, this.config);
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2097
|
+
console.error("LLM/strategy error:", message);
|
|
2098
|
+
this.relay?.sendMessage(
|
|
2099
|
+
"llm_error",
|
|
2100
|
+
"error",
|
|
2101
|
+
"Strategy Error",
|
|
2102
|
+
message
|
|
2103
|
+
);
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
1273
2106
|
console.log(`Strategy generated ${signals.length} signals`);
|
|
1274
2107
|
const filteredSignals = this.riskManager.filterSignals(signals, marketData);
|
|
1275
2108
|
console.log(`${filteredSignals.length} signals passed risk checks`);
|
|
2109
|
+
if (this.riskManager.getStatus().isLimitHit) {
|
|
2110
|
+
this.relay?.sendMessage(
|
|
2111
|
+
"risk_limit_hit",
|
|
2112
|
+
"warning",
|
|
2113
|
+
"Risk Limit Hit",
|
|
2114
|
+
`Daily loss limit reached: ${this.riskManager.getStatus().dailyPnL.toFixed(2)}`
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
1276
2117
|
if (filteredSignals.length > 0) {
|
|
1277
2118
|
const results = await this.executor.executeAll(filteredSignals);
|
|
1278
2119
|
for (const result of results) {
|
|
1279
2120
|
if (result.success) {
|
|
1280
2121
|
console.log(`Trade executed: ${result.signal.action} - ${result.txHash}`);
|
|
2122
|
+
this.relay?.sendMessage(
|
|
2123
|
+
"trade_executed",
|
|
2124
|
+
"success",
|
|
2125
|
+
"Trade Executed",
|
|
2126
|
+
`${result.signal.action.toUpperCase()}: ${result.signal.reasoning || "No reason provided"}`,
|
|
2127
|
+
{
|
|
2128
|
+
action: result.signal.action,
|
|
2129
|
+
txHash: result.txHash,
|
|
2130
|
+
tokenIn: result.signal.tokenIn,
|
|
2131
|
+
tokenOut: result.signal.tokenOut
|
|
2132
|
+
}
|
|
2133
|
+
);
|
|
1281
2134
|
} else {
|
|
1282
2135
|
console.warn(`Trade failed: ${result.error}`);
|
|
2136
|
+
this.relay?.sendMessage(
|
|
2137
|
+
"trade_failed",
|
|
2138
|
+
"error",
|
|
2139
|
+
"Trade Failed",
|
|
2140
|
+
result.error || "Unknown error",
|
|
2141
|
+
{ action: result.signal.action }
|
|
2142
|
+
);
|
|
1283
2143
|
}
|
|
1284
2144
|
}
|
|
1285
2145
|
}
|
|
2146
|
+
this.sendRelayStatus();
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Check if ETH balance is below threshold and notify
|
|
2150
|
+
*/
|
|
2151
|
+
checkFundsLow(marketData) {
|
|
2152
|
+
if (!this.relay) return;
|
|
2153
|
+
const wethAddress = "0x4200000000000000000000000000000000000006";
|
|
2154
|
+
const ethBalance = marketData.balances[wethAddress] || BigInt(0);
|
|
2155
|
+
const ethAmount = Number(ethBalance) / 1e18;
|
|
2156
|
+
if (ethAmount < FUNDS_LOW_THRESHOLD) {
|
|
2157
|
+
this.relay.sendMessage(
|
|
2158
|
+
"funds_low",
|
|
2159
|
+
"warning",
|
|
2160
|
+
"Low Funds",
|
|
2161
|
+
`ETH balance is ${ethAmount.toFixed(6)} ETH. Fund your trading wallet to continue trading.`,
|
|
2162
|
+
{
|
|
2163
|
+
ethBalance: ethAmount.toFixed(6),
|
|
2164
|
+
wallet: this.client.address,
|
|
2165
|
+
threshold: FUNDS_LOW_THRESHOLD
|
|
2166
|
+
}
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
1286
2169
|
}
|
|
1287
2170
|
/**
|
|
1288
2171
|
* Check for vault auto-creation based on policy
|
|
@@ -1296,7 +2179,14 @@ var AgentRuntime = class {
|
|
|
1296
2179
|
const result = await this.vaultManager.checkAndAutoCreateVault();
|
|
1297
2180
|
switch (result.action) {
|
|
1298
2181
|
case "created":
|
|
1299
|
-
console.log(
|
|
2182
|
+
console.log(`Vault created automatically: ${result.vaultAddress}`);
|
|
2183
|
+
this.relay?.sendMessage(
|
|
2184
|
+
"vault_created",
|
|
2185
|
+
"success",
|
|
2186
|
+
"Vault Auto-Created",
|
|
2187
|
+
`Vault deployed at ${result.vaultAddress}`,
|
|
2188
|
+
{ vaultAddress: result.vaultAddress }
|
|
2189
|
+
);
|
|
1300
2190
|
break;
|
|
1301
2191
|
case "already_exists":
|
|
1302
2192
|
break;
|
|
@@ -1308,11 +2198,16 @@ var AgentRuntime = class {
|
|
|
1308
2198
|
}
|
|
1309
2199
|
}
|
|
1310
2200
|
/**
|
|
1311
|
-
* Stop the
|
|
2201
|
+
* Stop the agent process completely
|
|
1312
2202
|
*/
|
|
1313
2203
|
stop() {
|
|
1314
2204
|
console.log("Stopping agent...");
|
|
1315
2205
|
this.isRunning = false;
|
|
2206
|
+
this.processAlive = false;
|
|
2207
|
+
this.mode = "idle";
|
|
2208
|
+
if (this.relay) {
|
|
2209
|
+
this.relay.disconnect();
|
|
2210
|
+
}
|
|
1316
2211
|
}
|
|
1317
2212
|
/**
|
|
1318
2213
|
* Get RPC URL based on network
|
|
@@ -1350,6 +2245,7 @@ var AgentRuntime = class {
|
|
|
1350
2245
|
const vaultConfig = this.config.vault || { policy: "disabled" };
|
|
1351
2246
|
return {
|
|
1352
2247
|
isRunning: this.isRunning,
|
|
2248
|
+
mode: this.mode,
|
|
1353
2249
|
agentId: Number(this.config.agentId),
|
|
1354
2250
|
wallet: this.client?.address || "not initialized",
|
|
1355
2251
|
llm: {
|
|
@@ -1363,7 +2259,11 @@ var AgentRuntime = class {
|
|
|
1363
2259
|
hasVault: false,
|
|
1364
2260
|
// Updated async via getVaultStatus
|
|
1365
2261
|
vaultAddress: null
|
|
1366
|
-
}
|
|
2262
|
+
},
|
|
2263
|
+
relay: {
|
|
2264
|
+
connected: this.relay?.isConnected || false
|
|
2265
|
+
},
|
|
2266
|
+
cycleCount: this.cycleCount
|
|
1367
2267
|
};
|
|
1368
2268
|
}
|
|
1369
2269
|
/**
|
|
@@ -1442,6 +2342,11 @@ var VaultConfigSchema = import_zod.z.object({
|
|
|
1442
2342
|
var WalletConfigSchema = import_zod.z.object({
|
|
1443
2343
|
setup: WalletSetupSchema.default("provide")
|
|
1444
2344
|
}).optional();
|
|
2345
|
+
var RelayConfigSchema = import_zod.z.object({
|
|
2346
|
+
enabled: import_zod.z.boolean().default(false),
|
|
2347
|
+
apiUrl: import_zod.z.string().url(),
|
|
2348
|
+
heartbeatIntervalMs: import_zod.z.number().min(5e3).default(3e4)
|
|
2349
|
+
}).optional();
|
|
1445
2350
|
var AgentConfigSchema = import_zod.z.object({
|
|
1446
2351
|
// Identity (from on-chain registration)
|
|
1447
2352
|
agentId: import_zod.z.union([import_zod.z.number().positive(), import_zod.z.string()]),
|
|
@@ -1457,6 +2362,8 @@ var AgentConfigSchema = import_zod.z.object({
|
|
|
1457
2362
|
trading: TradingConfigSchema.default({}),
|
|
1458
2363
|
// Vault configuration (copy trading)
|
|
1459
2364
|
vault: VaultConfigSchema.default({}),
|
|
2365
|
+
// Relay configuration (command center)
|
|
2366
|
+
relay: RelayConfigSchema,
|
|
1460
2367
|
// Allowed tokens (addresses)
|
|
1461
2368
|
allowedTokens: import_zod.z.array(import_zod.z.string()).optional()
|
|
1462
2369
|
});
|
|
@@ -1485,6 +2392,21 @@ function loadConfig(configPath) {
|
|
|
1485
2392
|
if (process.env.ANTHROPIC_API_KEY && config.llm.provider === "anthropic") {
|
|
1486
2393
|
llmConfig.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1487
2394
|
}
|
|
2395
|
+
if (process.env.GOOGLE_AI_API_KEY && config.llm.provider === "google") {
|
|
2396
|
+
llmConfig.apiKey = process.env.GOOGLE_AI_API_KEY;
|
|
2397
|
+
}
|
|
2398
|
+
if (process.env.DEEPSEEK_API_KEY && config.llm.provider === "deepseek") {
|
|
2399
|
+
llmConfig.apiKey = process.env.DEEPSEEK_API_KEY;
|
|
2400
|
+
}
|
|
2401
|
+
if (process.env.MISTRAL_API_KEY && config.llm.provider === "mistral") {
|
|
2402
|
+
llmConfig.apiKey = process.env.MISTRAL_API_KEY;
|
|
2403
|
+
}
|
|
2404
|
+
if (process.env.GROQ_API_KEY && config.llm.provider === "groq") {
|
|
2405
|
+
llmConfig.apiKey = process.env.GROQ_API_KEY;
|
|
2406
|
+
}
|
|
2407
|
+
if (process.env.TOGETHER_API_KEY && config.llm.provider === "together") {
|
|
2408
|
+
llmConfig.apiKey = process.env.TOGETHER_API_KEY;
|
|
2409
|
+
}
|
|
1488
2410
|
if (process.env.EXAGENT_LLM_URL) {
|
|
1489
2411
|
llmConfig.endpoint = process.env.EXAGENT_LLM_URL;
|
|
1490
2412
|
}
|
|
@@ -1518,7 +2440,193 @@ function validateConfig(config) {
|
|
|
1518
2440
|
}
|
|
1519
2441
|
|
|
1520
2442
|
// src/cli.ts
|
|
1521
|
-
var
|
|
2443
|
+
var import_accounts3 = require("viem/accounts");
|
|
2444
|
+
|
|
2445
|
+
// src/secure-env.ts
|
|
2446
|
+
var crypto = __toESM(require("crypto"));
|
|
2447
|
+
var fs = __toESM(require("fs"));
|
|
2448
|
+
var path = __toESM(require("path"));
|
|
2449
|
+
var ALGORITHM = "aes-256-gcm";
|
|
2450
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
2451
|
+
var SALT_LENGTH = 32;
|
|
2452
|
+
var IV_LENGTH = 16;
|
|
2453
|
+
var KEY_LENGTH = 32;
|
|
2454
|
+
var SENSITIVE_PATTERNS = [
|
|
2455
|
+
/PRIVATE_KEY$/i,
|
|
2456
|
+
/_API_KEY$/i,
|
|
2457
|
+
/API_KEY$/i,
|
|
2458
|
+
/_SECRET$/i,
|
|
2459
|
+
/^OPENAI_API_KEY$/i,
|
|
2460
|
+
/^ANTHROPIC_API_KEY$/i,
|
|
2461
|
+
/^GOOGLE_AI_API_KEY$/i,
|
|
2462
|
+
/^DEEPSEEK_API_KEY$/i,
|
|
2463
|
+
/^MISTRAL_API_KEY$/i,
|
|
2464
|
+
/^GROQ_API_KEY$/i,
|
|
2465
|
+
/^TOGETHER_API_KEY$/i
|
|
2466
|
+
];
|
|
2467
|
+
function isSensitiveKey(key) {
|
|
2468
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
2469
|
+
}
|
|
2470
|
+
function deriveKey(passphrase, salt) {
|
|
2471
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
2472
|
+
}
|
|
2473
|
+
function encryptValue(value, key) {
|
|
2474
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
2475
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
2476
|
+
let encrypted = cipher.update(value, "utf8", "hex");
|
|
2477
|
+
encrypted += cipher.final("hex");
|
|
2478
|
+
const tag = cipher.getAuthTag();
|
|
2479
|
+
return {
|
|
2480
|
+
iv: iv.toString("hex"),
|
|
2481
|
+
encrypted,
|
|
2482
|
+
tag: tag.toString("hex")
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
function decryptValue(encrypted, key, iv, tag) {
|
|
2486
|
+
const decipher = crypto.createDecipheriv(
|
|
2487
|
+
ALGORITHM,
|
|
2488
|
+
key,
|
|
2489
|
+
Buffer.from(iv, "hex")
|
|
2490
|
+
);
|
|
2491
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
2492
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
2493
|
+
decrypted += decipher.final("utf8");
|
|
2494
|
+
return decrypted;
|
|
2495
|
+
}
|
|
2496
|
+
function parseEnvFile(content) {
|
|
2497
|
+
const entries = [];
|
|
2498
|
+
for (const line of content.split("\n")) {
|
|
2499
|
+
const trimmed = line.trim();
|
|
2500
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2501
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2502
|
+
if (eqIndex === -1) continue;
|
|
2503
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
2504
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2505
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2506
|
+
value = value.slice(1, -1);
|
|
2507
|
+
}
|
|
2508
|
+
if (key && value) {
|
|
2509
|
+
entries.push({ key, value });
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
return entries;
|
|
2513
|
+
}
|
|
2514
|
+
function encryptEnvFile(envPath, passphrase, deleteOriginal = false) {
|
|
2515
|
+
if (!fs.existsSync(envPath)) {
|
|
2516
|
+
throw new Error(`File not found: ${envPath}`);
|
|
2517
|
+
}
|
|
2518
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
2519
|
+
const entries = parseEnvFile(content);
|
|
2520
|
+
if (entries.length === 0) {
|
|
2521
|
+
throw new Error("No environment variables found in file");
|
|
2522
|
+
}
|
|
2523
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
2524
|
+
const key = deriveKey(passphrase, salt);
|
|
2525
|
+
const encryptedEntries = entries.map(({ key: envKey, value }) => {
|
|
2526
|
+
if (isSensitiveKey(envKey)) {
|
|
2527
|
+
const { iv, encrypted, tag } = encryptValue(value, key);
|
|
2528
|
+
return {
|
|
2529
|
+
key: envKey,
|
|
2530
|
+
value: encrypted,
|
|
2531
|
+
encrypted: true,
|
|
2532
|
+
iv,
|
|
2533
|
+
tag
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
key: envKey,
|
|
2538
|
+
value,
|
|
2539
|
+
encrypted: false
|
|
2540
|
+
};
|
|
2541
|
+
});
|
|
2542
|
+
const encryptedEnv = {
|
|
2543
|
+
version: 1,
|
|
2544
|
+
salt: salt.toString("hex"),
|
|
2545
|
+
entries: encryptedEntries
|
|
2546
|
+
};
|
|
2547
|
+
const encPath = envPath + ".enc";
|
|
2548
|
+
fs.writeFileSync(encPath, JSON.stringify(encryptedEnv, null, 2), { mode: 384 });
|
|
2549
|
+
if (deleteOriginal) {
|
|
2550
|
+
fs.unlinkSync(envPath);
|
|
2551
|
+
}
|
|
2552
|
+
const sensitiveCount = encryptedEntries.filter((e) => e.encrypted).length;
|
|
2553
|
+
const plainCount = encryptedEntries.filter((e) => !e.encrypted).length;
|
|
2554
|
+
console.log(
|
|
2555
|
+
`Encrypted ${sensitiveCount} sensitive values (${plainCount} non-sensitive kept as plaintext)`
|
|
2556
|
+
);
|
|
2557
|
+
return encPath;
|
|
2558
|
+
}
|
|
2559
|
+
function decryptEnvFile(encPath, passphrase) {
|
|
2560
|
+
if (!fs.existsSync(encPath)) {
|
|
2561
|
+
throw new Error(`Encrypted env file not found: ${encPath}`);
|
|
2562
|
+
}
|
|
2563
|
+
const content = JSON.parse(fs.readFileSync(encPath, "utf-8"));
|
|
2564
|
+
if (content.version !== 1) {
|
|
2565
|
+
throw new Error(`Unsupported encrypted env version: ${content.version}`);
|
|
2566
|
+
}
|
|
2567
|
+
const salt = Buffer.from(content.salt, "hex");
|
|
2568
|
+
const key = deriveKey(passphrase, salt);
|
|
2569
|
+
const result = {};
|
|
2570
|
+
for (const entry of content.entries) {
|
|
2571
|
+
if (entry.encrypted) {
|
|
2572
|
+
if (!entry.iv || !entry.tag) {
|
|
2573
|
+
throw new Error(`Missing encryption metadata for ${entry.key}`);
|
|
2574
|
+
}
|
|
2575
|
+
try {
|
|
2576
|
+
result[entry.key] = decryptValue(entry.value, key, entry.iv, entry.tag);
|
|
2577
|
+
} catch {
|
|
2578
|
+
throw new Error(
|
|
2579
|
+
`Failed to decrypt ${entry.key}. Wrong passphrase or corrupted data.`
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
} else {
|
|
2583
|
+
result[entry.key] = entry.value;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return result;
|
|
2587
|
+
}
|
|
2588
|
+
function loadSecureEnv(basePath, passphrase) {
|
|
2589
|
+
const encPath = path.join(basePath, ".env.enc");
|
|
2590
|
+
const envPath = path.join(basePath, ".env");
|
|
2591
|
+
if (fs.existsSync(encPath)) {
|
|
2592
|
+
if (!passphrase) {
|
|
2593
|
+
passphrase = process.env.EXAGENT_PASSPHRASE;
|
|
2594
|
+
}
|
|
2595
|
+
if (!passphrase) {
|
|
2596
|
+
console.warn("");
|
|
2597
|
+
console.warn("WARNING: Found .env.enc but no passphrase provided.");
|
|
2598
|
+
console.warn(" Set EXAGENT_PASSPHRASE environment variable or");
|
|
2599
|
+
console.warn(" pass --passphrase when running the agent.");
|
|
2600
|
+
console.warn(" Falling back to plaintext .env file.");
|
|
2601
|
+
console.warn("");
|
|
2602
|
+
} else {
|
|
2603
|
+
const vars = decryptEnvFile(encPath, passphrase);
|
|
2604
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
2605
|
+
process.env[key] = value;
|
|
2606
|
+
}
|
|
2607
|
+
return true;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
if (fs.existsSync(envPath)) {
|
|
2611
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
2612
|
+
const entries = parseEnvFile(content);
|
|
2613
|
+
const sensitiveKeys = entries.filter(({ key }) => isSensitiveKey(key)).map(({ key }) => key);
|
|
2614
|
+
if (sensitiveKeys.length > 0) {
|
|
2615
|
+
console.warn("");
|
|
2616
|
+
console.warn("WARNING: Sensitive values stored in plaintext .env file:");
|
|
2617
|
+
for (const key of sensitiveKeys) {
|
|
2618
|
+
console.warn(` - ${key}`);
|
|
2619
|
+
}
|
|
2620
|
+
console.warn("");
|
|
2621
|
+
console.warn(' Run "npx @exagent/agent encrypt" to secure your keys.');
|
|
2622
|
+
console.warn("");
|
|
2623
|
+
}
|
|
2624
|
+
return false;
|
|
2625
|
+
}
|
|
2626
|
+
return false;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/cli.ts
|
|
1522
2630
|
(0, import_dotenv2.config)();
|
|
1523
2631
|
var program = new import_commander.Command();
|
|
1524
2632
|
program.name("exagent").description("Exagent autonomous trading agent").version("0.1.0");
|
|
@@ -1563,9 +2671,13 @@ function prompt(question, hidden = false) {
|
|
|
1563
2671
|
});
|
|
1564
2672
|
}
|
|
1565
2673
|
async function checkFirstRunSetup(configPath) {
|
|
1566
|
-
const envPath =
|
|
1567
|
-
|
|
1568
|
-
|
|
2674
|
+
const envPath = path2.join(path2.dirname(configPath), ".env");
|
|
2675
|
+
const encPath = envPath + ".enc";
|
|
2676
|
+
if (fs2.existsSync(encPath)) {
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (fs2.existsSync(envPath)) {
|
|
2680
|
+
const envContent2 = fs2.readFileSync(envPath, "utf-8");
|
|
1569
2681
|
const hasPrivateKey = envContent2.includes("EXAGENT_PRIVATE_KEY=") && !envContent2.includes("EXAGENT_PRIVATE_KEY=\n") && !envContent2.includes("EXAGENT_PRIVATE_KEY=$");
|
|
1570
2682
|
if (hasPrivateKey) {
|
|
1571
2683
|
return;
|
|
@@ -1591,8 +2703,8 @@ async function checkFirstRunSetup(configPath) {
|
|
|
1591
2703
|
if (walletSetup === "generate") {
|
|
1592
2704
|
console.log("[WALLET] Generating a new wallet for your agent...");
|
|
1593
2705
|
console.log("");
|
|
1594
|
-
const generatedKey = (0,
|
|
1595
|
-
const account = (0,
|
|
2706
|
+
const generatedKey = (0, import_accounts3.generatePrivateKey)();
|
|
2707
|
+
const account = (0, import_accounts3.privateKeyToAccount)(generatedKey);
|
|
1596
2708
|
privateKey = generatedKey;
|
|
1597
2709
|
walletAddress = account.address;
|
|
1598
2710
|
console.log(" New wallet created!");
|
|
@@ -1622,7 +2734,7 @@ async function checkFirstRunSetup(configPath) {
|
|
|
1622
2734
|
process.exit(1);
|
|
1623
2735
|
}
|
|
1624
2736
|
try {
|
|
1625
|
-
const account = (0,
|
|
2737
|
+
const account = (0, import_accounts3.privateKeyToAccount)(privateKey);
|
|
1626
2738
|
walletAddress = account.address;
|
|
1627
2739
|
console.log("");
|
|
1628
2740
|
console.log(` Wallet address: ${walletAddress}`);
|
|
@@ -1689,13 +2801,46 @@ EXAGENT_NETWORK=${config.network || "testnet"}
|
|
|
1689
2801
|
# LLM (${llmProvider})
|
|
1690
2802
|
${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
|
|
1691
2803
|
`;
|
|
1692
|
-
|
|
2804
|
+
fs2.writeFileSync(envPath, envContent, { mode: 384 });
|
|
1693
2805
|
console.log("");
|
|
1694
2806
|
console.log("=".repeat(60));
|
|
1695
|
-
console.log("
|
|
2807
|
+
console.log(" ENCRYPT YOUR SECRETS");
|
|
1696
2808
|
console.log("=".repeat(60));
|
|
1697
2809
|
console.log("");
|
|
1698
|
-
console.log(" Your .env file
|
|
2810
|
+
console.log(" Your .env file contains private keys and API credentials.");
|
|
2811
|
+
console.log(" Encrypting it protects you if someone accesses your files.");
|
|
2812
|
+
console.log("");
|
|
2813
|
+
console.log(" Press Enter to skip (you can encrypt later with: npx @exagent/agent encrypt)");
|
|
2814
|
+
console.log("");
|
|
2815
|
+
const passphrase = await prompt(" Choose encryption passphrase (or Enter to skip): ", true);
|
|
2816
|
+
if (passphrase && passphrase.length >= 4) {
|
|
2817
|
+
const confirmPassphrase = await prompt(" Confirm passphrase: ", true);
|
|
2818
|
+
if (passphrase === confirmPassphrase) {
|
|
2819
|
+
console.log("");
|
|
2820
|
+
encryptEnvFile(envPath, passphrase, true);
|
|
2821
|
+
process.env.EXAGENT_PASSPHRASE = passphrase;
|
|
2822
|
+
console.log(" Your secrets are encrypted. Plaintext .env has been deleted.");
|
|
2823
|
+
console.log("");
|
|
2824
|
+
console.log(" Remember your passphrase \u2014 you need it every time the agent starts.");
|
|
2825
|
+
console.log(" Or set: export EXAGENT_PASSPHRASE=<your-passphrase>");
|
|
2826
|
+
} else {
|
|
2827
|
+
console.log("");
|
|
2828
|
+
console.log(" Passphrases did not match. Skipping encryption.");
|
|
2829
|
+
console.log(" Run: npx @exagent/agent encrypt");
|
|
2830
|
+
}
|
|
2831
|
+
} else if (passphrase && passphrase.length < 4) {
|
|
2832
|
+
console.log("");
|
|
2833
|
+
console.log(" Passphrase too short (min 4 chars). Skipping encryption.");
|
|
2834
|
+
console.log(" Run: npx @exagent/agent encrypt");
|
|
2835
|
+
} else {
|
|
2836
|
+
console.log("");
|
|
2837
|
+
console.log(" Skipped encryption. Your .env is stored in plaintext.");
|
|
2838
|
+
console.log(" To encrypt later: npx @exagent/agent encrypt --delete");
|
|
2839
|
+
}
|
|
2840
|
+
console.log("");
|
|
2841
|
+
console.log("=".repeat(60));
|
|
2842
|
+
console.log(" SETUP COMPLETE");
|
|
2843
|
+
console.log("=".repeat(60));
|
|
1699
2844
|
console.log("");
|
|
1700
2845
|
console.log(` Wallet: ${walletAddress}`);
|
|
1701
2846
|
console.log(` LLM: ${llmProvider}`);
|
|
@@ -1707,11 +2852,20 @@ ${llmEnvVar}EXAGENT_LLM_MODEL=${config.llm?.model || ""}
|
|
|
1707
2852
|
console.log("");
|
|
1708
2853
|
console.log(" The agent will now start...");
|
|
1709
2854
|
console.log("");
|
|
1710
|
-
(
|
|
2855
|
+
if (!process.env.EXAGENT_PASSPHRASE) {
|
|
2856
|
+
(0, import_dotenv2.config)({ path: envPath, override: true });
|
|
2857
|
+
}
|
|
1711
2858
|
}
|
|
1712
|
-
program.command("run").description("Start the trading agent").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").action(async (options) => {
|
|
2859
|
+
program.command("run").description("Start the trading agent").option("-c, --config <path>", "Path to agent-config.json", "agent-config.json").option("-p, --passphrase <passphrase>", "Passphrase to decrypt .env.enc").action(async (options) => {
|
|
1713
2860
|
try {
|
|
1714
2861
|
await checkFirstRunSetup(options.config);
|
|
2862
|
+
const configDir = path2.dirname(
|
|
2863
|
+
options.config.startsWith("/") ? options.config : path2.join(process.cwd(), options.config)
|
|
2864
|
+
);
|
|
2865
|
+
const usedEncrypted = loadSecureEnv(configDir, options.passphrase);
|
|
2866
|
+
if (usedEncrypted) {
|
|
2867
|
+
console.log("Loaded encrypted environment (.env.enc)");
|
|
2868
|
+
}
|
|
1715
2869
|
console.log("Loading configuration...");
|
|
1716
2870
|
const config = loadConfig(options.config);
|
|
1717
2871
|
validateConfig(config);
|
|
@@ -1820,9 +2974,65 @@ program.command("api-keys").description("Show how to get API keys for each LLM p
|
|
|
1820
2974
|
console.log("OLLAMA (Local - No API Key Required)");
|
|
1821
2975
|
console.log(" Install: https://ollama.com/download");
|
|
1822
2976
|
console.log(" Run: ollama serve");
|
|
1823
|
-
console.log(" Pull model: ollama pull
|
|
2977
|
+
console.log(" Pull model: ollama pull llama3.2");
|
|
1824
2978
|
console.log("");
|
|
1825
2979
|
console.log("Note: Your API keys are stored locally in .env and never sent to Exagent servers.");
|
|
1826
2980
|
console.log("");
|
|
1827
2981
|
});
|
|
2982
|
+
program.command("encrypt").description("Encrypt .env file to .env.enc for secure storage").option("-d, --dir <path>", "Directory containing .env file", ".").option("--delete", "Delete plaintext .env after encryption", false).action(async (options) => {
|
|
2983
|
+
try {
|
|
2984
|
+
const dir = options.dir.startsWith("/") ? options.dir : path2.join(process.cwd(), options.dir);
|
|
2985
|
+
const envPath = path2.join(dir, ".env");
|
|
2986
|
+
if (!fs2.existsSync(envPath)) {
|
|
2987
|
+
console.error("No .env file found in", dir);
|
|
2988
|
+
process.exit(1);
|
|
2989
|
+
}
|
|
2990
|
+
const encPath = envPath + ".enc";
|
|
2991
|
+
if (fs2.existsSync(encPath)) {
|
|
2992
|
+
const overwrite = await prompt(" .env.enc already exists. Overwrite? (y/n): ");
|
|
2993
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
2994
|
+
console.log("Aborted.");
|
|
2995
|
+
process.exit(0);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
console.log("");
|
|
2999
|
+
console.log("=".repeat(50));
|
|
3000
|
+
console.log(" ENCRYPT ENVIRONMENT FILE");
|
|
3001
|
+
console.log("=".repeat(50));
|
|
3002
|
+
console.log("");
|
|
3003
|
+
console.log(" This will encrypt your .env file using a passphrase.");
|
|
3004
|
+
console.log(" You will need this passphrase every time the agent starts.");
|
|
3005
|
+
console.log("");
|
|
3006
|
+
console.log(" Alternatively, set EXAGENT_PASSPHRASE env var to skip");
|
|
3007
|
+
console.log(" the prompt on startup.");
|
|
3008
|
+
console.log("");
|
|
3009
|
+
const passphrase = await prompt(" Enter encryption passphrase: ", true);
|
|
3010
|
+
if (!passphrase || passphrase.length < 4) {
|
|
3011
|
+
console.error(" Passphrase must be at least 4 characters.");
|
|
3012
|
+
process.exit(1);
|
|
3013
|
+
}
|
|
3014
|
+
const confirm = await prompt(" Confirm passphrase: ", true);
|
|
3015
|
+
if (passphrase !== confirm) {
|
|
3016
|
+
console.error(" Passphrases do not match.");
|
|
3017
|
+
process.exit(1);
|
|
3018
|
+
}
|
|
3019
|
+
console.log("");
|
|
3020
|
+
encryptEnvFile(envPath, passphrase, options.delete);
|
|
3021
|
+
console.log("");
|
|
3022
|
+
console.log(" Encrypted file saved to: .env.enc");
|
|
3023
|
+
if (options.delete) {
|
|
3024
|
+
console.log(" Plaintext .env has been deleted.");
|
|
3025
|
+
} else {
|
|
3026
|
+
console.log(" Your plaintext .env file is still present.");
|
|
3027
|
+
console.log(" Run with --delete to remove it after encryption.");
|
|
3028
|
+
}
|
|
3029
|
+
console.log("");
|
|
3030
|
+
console.log(" To use: npx @exagent/agent run --passphrase <your-passphrase>");
|
|
3031
|
+
console.log(" Or set: export EXAGENT_PASSPHRASE=<your-passphrase>");
|
|
3032
|
+
console.log("");
|
|
3033
|
+
} catch (error) {
|
|
3034
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
3035
|
+
process.exit(1);
|
|
3036
|
+
}
|
|
3037
|
+
});
|
|
1828
3038
|
program.parse();
|