@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/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 fs = __toESM(require("fs"));
31
- var path = __toESM(require("path"));
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-turbo-preview",
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-3-opus-20240229",
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 || "mistral",
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 || "mistral",
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(path2) {
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(path2).href}').then(m => console.log(JSON.stringify({ exports: Object.keys(m) }))).catch(e => console.error('ERROR:', e.message))`
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(path2);
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
- throw new Error(
1208
- `Cannot link wallet: ${address} is not the owner of agent ${this.config.agentId}. Owner is ${agent?.owner}`
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
- * This ensures trades are attributed to the correct config epoch
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
- await this.client.registry.updateConfig(agentId, this.configHash);
1230
- const newEpoch = await this.client.registry.getCurrentEpoch(agentId);
1231
- console.log(`Config updated, new epoch started: ${newEpoch}`);
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 trading loop
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
- if (this.isRunning) {
1248
- throw new Error("Agent is already running");
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
- this.isRunning = true;
1251
- console.log("Starting trading loop...");
1252
- console.log(`Interval: ${this.config.trading.tradingIntervalMs}ms`);
1253
- while (this.isRunning) {
1254
- try {
1255
- await this.runCycle();
1256
- } catch (error) {
1257
- console.error("Error in trading cycle:", error);
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
- await this.sleep(this.config.trading.tradingIntervalMs);
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
- const signals = await this.strategy(marketData, this.llm, this.config);
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(`\u{1F389} Vault created automatically: ${result.vaultAddress}`);
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 trading loop
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 import_accounts2 = require("viem/accounts");
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 = path.join(path.dirname(configPath), ".env");
1567
- if (fs.existsSync(envPath)) {
1568
- const envContent2 = fs.readFileSync(envPath, "utf-8");
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, import_accounts2.generatePrivateKey)();
1595
- const account = (0, import_accounts2.privateKeyToAccount)(generatedKey);
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, import_accounts2.privateKeyToAccount)(privateKey);
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
- fs.writeFileSync(envPath, envContent, { mode: 384 });
2804
+ fs2.writeFileSync(envPath, envContent, { mode: 384 });
1693
2805
  console.log("");
1694
2806
  console.log("=".repeat(60));
1695
- console.log(" SETUP COMPLETE");
2807
+ console.log(" ENCRYPT YOUR SECRETS");
1696
2808
  console.log("=".repeat(60));
1697
2809
  console.log("");
1698
- console.log(" Your .env file has been created.");
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
- (0, import_dotenv2.config)({ path: envPath, override: true });
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 mistral");
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();