@exagent/agent 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/{chunk-VDK4XPAC.js → chunk-WTECTX2Z.js} +31 -4
  2. package/dist/cli.js +212 -77
  3. package/dist/index.js +1 -1
  4. package/package.json +1 -1
  5. package/src/cli.ts +122 -3
  6. package/src/config.ts +19 -2
  7. package/src/runtime.ts +18 -2
  8. package/src/setup.ts +118 -67
  9. package/dist/chunk-25J5ZDKX.js +0 -5622
  10. package/dist/chunk-2ML4XS5X.js +0 -5626
  11. package/dist/chunk-2OKYNZ3J.js +0 -5622
  12. package/dist/chunk-2PAASZJN.js +0 -5132
  13. package/dist/chunk-2WAYVOLU.js +0 -5624
  14. package/dist/chunk-2XBVVY3I.js +0 -5621
  15. package/dist/chunk-37HCPVBZ.js +0 -2941
  16. package/dist/chunk-3DZAZBLF.js +0 -5652
  17. package/dist/chunk-3IXCKNSV.js +0 -5008
  18. package/dist/chunk-3MXFTRXB.js +0 -3326
  19. package/dist/chunk-4MURFLNJ.js +0 -5136
  20. package/dist/chunk-4UVMO6ZM.js +0 -6318
  21. package/dist/chunk-56YQROFU.js +0 -5639
  22. package/dist/chunk-5B3ZEGMD.js +0 -4330
  23. package/dist/chunk-5NU6FDDE.js +0 -6020
  24. package/dist/chunk-5WADKPJU.js +0 -1552
  25. package/dist/chunk-6LOIFEEV.js +0 -4895
  26. package/dist/chunk-6YIXPNL4.js +0 -5626
  27. package/dist/chunk-72HK2V74.js +0 -5224
  28. package/dist/chunk-7Q72QQIV.js +0 -4697
  29. package/dist/chunk-7V3XTBIF.js +0 -4896
  30. package/dist/chunk-A4CM4F7Q.js +0 -5633
  31. package/dist/chunk-ADXXR2MA.js +0 -4816
  32. package/dist/chunk-AG6CJPIC.js +0 -4895
  33. package/dist/chunk-AJPXHBTF.js +0 -4459
  34. package/dist/chunk-AQ6R37XV.js +0 -4809
  35. package/dist/chunk-AS4UEZMU.js +0 -5001
  36. package/dist/chunk-ASYMD22Y.js +0 -5624
  37. package/dist/chunk-AU4MCQCE.js +0 -5624
  38. package/dist/chunk-B4VHIITU.js +0 -5748
  39. package/dist/chunk-B6GVDNKQ.js +0 -5624
  40. package/dist/chunk-BCIAW6ZL.js +0 -5132
  41. package/dist/chunk-BJZ5PCG3.js +0 -5358
  42. package/dist/chunk-BS4J5QSM.js +0 -5622
  43. package/dist/chunk-BV2AUUX6.js +0 -2940
  44. package/dist/chunk-BWNSH2LK.js +0 -1574
  45. package/dist/chunk-C3GMBW3R.js +0 -5640
  46. package/dist/chunk-C6CVQGJ4.js +0 -5624
  47. package/dist/chunk-CGXKXNUJ.js +0 -5626
  48. package/dist/chunk-CIEZAYOU.js +0 -4701
  49. package/dist/chunk-CORZCEAQ.js +0 -5621
  50. package/dist/chunk-CS5LGZWP.js +0 -5357
  51. package/dist/chunk-CVT3KC24.js +0 -5624
  52. package/dist/chunk-D5MJ45R7.js +0 -3258
  53. package/dist/chunk-DSBRZ5DZ.js +0 -5624
  54. package/dist/chunk-E2X7JARQ.js +0 -4437
  55. package/dist/chunk-E3GY36ZP.js +0 -3258
  56. package/dist/chunk-E6NCIFKB.js +0 -4733
  57. package/dist/chunk-EOXLKW4D.js +0 -4895
  58. package/dist/chunk-FCI7LX4Q.js +0 -5624
  59. package/dist/chunk-FFJSKTOL.js +0 -4539
  60. package/dist/chunk-FOQYP3IB.js +0 -2950
  61. package/dist/chunk-GNEYTZDH.js +0 -4686
  62. package/dist/chunk-GPMXUMYH.js +0 -5991
  63. package/dist/chunk-GZWPAQPU.js +0 -4593
  64. package/dist/chunk-H5DXDKMX.js +0 -5619
  65. package/dist/chunk-HFQRTMS6.js +0 -3377
  66. package/dist/chunk-HQKRHX6Y.js +0 -5626
  67. package/dist/chunk-HTF3TNBY.js +0 -4834
  68. package/dist/chunk-IADSQBBY.js +0 -5523
  69. package/dist/chunk-IE2SXMZK.js +0 -4890
  70. package/dist/chunk-IGUQVJCB.js +0 -5622
  71. package/dist/chunk-IIREL7SL.js +0 -5615
  72. package/dist/chunk-IJK4EFTJ.js +0 -6043
  73. package/dist/chunk-J2MQ3Y5O.js +0 -5223
  74. package/dist/chunk-J3NG7AGT.js +0 -6047
  75. package/dist/chunk-JIBBZ3NV.js +0 -5132
  76. package/dist/chunk-JIPSBE6S.js +0 -5622
  77. package/dist/chunk-JPG755XK.js +0 -4589
  78. package/dist/chunk-JQBNL5GX.js +0 -5230
  79. package/dist/chunk-KS3F5WSX.js +0 -4831
  80. package/dist/chunk-KUYTQ4FR.js +0 -4808
  81. package/dist/chunk-KVP4CMJ5.js +0 -4711
  82. package/dist/chunk-LAR2I44B.js +0 -5626
  83. package/dist/chunk-LBTHSED2.js +0 -1531
  84. package/dist/chunk-M6OAMYVM.js +0 -5621
  85. package/dist/chunk-MFN5WWOY.js +0 -5132
  86. package/dist/chunk-MMTSKXLK.js +0 -5624
  87. package/dist/chunk-MPUSQLTH.js +0 -5626
  88. package/dist/chunk-MREXDTWL.js +0 -1555
  89. package/dist/chunk-MUEDKRFC.js +0 -5624
  90. package/dist/chunk-NOVPL2JH.js +0 -3327
  91. package/dist/chunk-NQIP4MHV.js +0 -4334
  92. package/dist/chunk-NXXKMYLS.js +0 -5624
  93. package/dist/chunk-OBYNZXNM.js +0 -4756
  94. package/dist/chunk-OFY4HBOJ.js +0 -5624
  95. package/dist/chunk-OJNUEZEK.js +0 -5602
  96. package/dist/chunk-OQCJOMUQ.js +0 -5624
  97. package/dist/chunk-OZH75GY6.js +0 -5132
  98. package/dist/chunk-P3IJVDMZ.js +0 -4700
  99. package/dist/chunk-PMYMYMBH.js +0 -4877
  100. package/dist/chunk-PRELNRVN.js +0 -5623
  101. package/dist/chunk-PSQUSNSI.js +0 -4703
  102. package/dist/chunk-QAIQ5IB6.js +0 -5624
  103. package/dist/chunk-QG22GADV.js +0 -6316
  104. package/dist/chunk-QNE2KGGK.js +0 -3315
  105. package/dist/chunk-RH7ZBSG4.js +0 -5132
  106. package/dist/chunk-RLD5MUCR.js +0 -5626
  107. package/dist/chunk-S42VEBNR.js +0 -3268
  108. package/dist/chunk-SEM6UXU4.js +0 -3324
  109. package/dist/chunk-SI5WP77M.js +0 -4430
  110. package/dist/chunk-SID4SQSY.js +0 -4837
  111. package/dist/chunk-SIELPKWF.js +0 -1558
  112. package/dist/chunk-SVBLY6QT.js +0 -5742
  113. package/dist/chunk-SVFTC5V2.js +0 -6021
  114. package/dist/chunk-SXHTX62B.js +0 -4823
  115. package/dist/chunk-T2YCEA5U.js +0 -4730
  116. package/dist/chunk-TARCHIOU.js +0 -4718
  117. package/dist/chunk-TDACLKD7.js +0 -5867
  118. package/dist/chunk-TGCBM3NP.js +0 -4890
  119. package/dist/chunk-TIWG6KAK.js +0 -4769
  120. package/dist/chunk-TKLKATVM.js +0 -1534
  121. package/dist/chunk-TSLZ4A5P.js +0 -5222
  122. package/dist/chunk-TWSDKORW.js +0 -4698
  123. package/dist/chunk-U5QHYVMJ.js +0 -3341
  124. package/dist/chunk-UAP5CTHB.js +0 -5985
  125. package/dist/chunk-UK6SEUWU.js +0 -3210
  126. package/dist/chunk-UKU5YO65.js +0 -5132
  127. package/dist/chunk-UOZQXP4Q.js +0 -5144
  128. package/dist/chunk-UPTN2TSS.js +0 -4727
  129. package/dist/chunk-UQT2APOE.js +0 -2944
  130. package/dist/chunk-V32QDZKW.js +0 -5132
  131. package/dist/chunk-VKY2CDCD.js +0 -5622
  132. package/dist/chunk-VUCSYMCY.js +0 -3323
  133. package/dist/chunk-VVLNBD5Y.js +0 -5132
  134. package/dist/chunk-W3TQ22O6.js +0 -4459
  135. package/dist/chunk-WA4DSGOM.js +0 -3355
  136. package/dist/chunk-WI6MIICK.js +0 -4687
  137. package/dist/chunk-XRHJLL74.js +0 -4893
  138. package/dist/chunk-XXWXEBJQ.js +0 -4885
  139. package/dist/chunk-YC6TH2H3.js +0 -5624
  140. package/dist/chunk-YDH6HCUJ.js +0 -5624
  141. package/dist/chunk-YJD35VKQ.js +0 -4890
  142. package/dist/chunk-ZBIQJBY7.js +0 -5620
  143. package/dist/chunk-ZKTSA2AE.js +0 -5629
  144. package/dist/chunk-ZKZZL3PE.js +0 -3379
  145. package/dist/chunk-ZM5KCPRK.js +0 -4541
  146. package/dist/chunk-ZTYPDSE3.js +0 -3258
@@ -1,6 +1,6 @@
1
1
  // src/config.ts
2
2
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
3
- import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from "fs";
4
4
  import { homedir } from "os";
5
5
  import { resolve } from "path";
6
6
  import { z } from "zod";
@@ -141,7 +141,7 @@ var secureStoreSchema = z.object({
141
141
  ciphertext: z.string(),
142
142
  authTag: z.string()
143
143
  });
144
- var DEFAULT_SCRYPT_COST = 32768;
144
+ var DEFAULT_SCRYPT_COST = 16384;
145
145
  var DEFAULT_SCRYPT_BLOCK_SIZE = 8;
146
146
  var DEFAULT_SCRYPT_PARALLELIZATION = 1;
147
147
  function expandHomeDir(path) {
@@ -276,6 +276,17 @@ ${issues}`);
276
276
  }
277
277
  return result.data;
278
278
  }
279
+ function updateSecureStore(path, password, updates) {
280
+ const secrets = decryptSecretPayload(path, password);
281
+ const updated = { ...secrets, ...updates };
282
+ const encrypted = encryptSecretPayload(updated, password);
283
+ const secureStorePath = expandHomeDir(path);
284
+ writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
285
+ try {
286
+ chmodSync(secureStorePath, 384);
287
+ } catch {
288
+ }
289
+ }
279
290
  function generateSampleConfig(agentId, apiUrl) {
280
291
  const config = {
281
292
  agentId,
@@ -4868,6 +4879,20 @@ try {
4868
4879
  } catch {
4869
4880
  }
4870
4881
  var MAX_CONSECUTIVE_FAILURES = 3;
4882
+ function getCycleErrorHint(category, msg) {
4883
+ const lower = msg.toLowerCase();
4884
+ if (category === "auth" || lower.includes("401") || lower.includes("unauthorized") || lower.includes("invalid api key") || lower.includes("authentication"))
4885
+ return "Check your LLM API key \u2014 update via npx exagent setup";
4886
+ if (category === "network" || lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("fetch failed") || lower.includes("timeout"))
4887
+ return "Network connectivity issue \u2014 check your internet connection";
4888
+ if (category === "venue" || lower.includes("hyperliquid") || lower.includes("polymarket") || lower.includes("venue"))
4889
+ return "Venue API error \u2014 the venue may be experiencing issues";
4890
+ if (category === "strategy" || lower.includes("invalid") && lower.includes("signal") || lower.includes("strategy"))
4891
+ return "Strategy returned invalid output \u2014 check your strategy file";
4892
+ if (lower.includes("rate limit") || lower.includes("429"))
4893
+ return "Rate limited \u2014 consider increasing tradingIntervalMs";
4894
+ return null;
4895
+ }
4871
4896
  var AgentRuntime = class _AgentRuntime {
4872
4897
  config;
4873
4898
  relay;
@@ -5477,12 +5502,13 @@ var AgentRuntime = class _AgentRuntime {
5477
5502
  const errMsg = err.message;
5478
5503
  timings.totalMs = Date.now() - cycleStart;
5479
5504
  const categorized = this.diagnostics.recordError(err);
5480
- log.error("cycle", `Cycle error: ${errMsg}`, {
5505
+ const hint = getCycleErrorHint(categorized.category, errMsg);
5506
+ log.error("cycle", `Cycle error: ${errMsg}${hint ? ` \u2014 ${hint}` : ""}`, {
5481
5507
  cycle: this.cycleCount,
5482
5508
  totalMs: timings.totalMs,
5483
5509
  category: categorized.category
5484
5510
  });
5485
- this.signal.reportError("Cycle Error", errMsg);
5511
+ this.signal.reportError("Cycle Error", `${errMsg}${hint ? ` \u2014 ${hint}` : ""}`);
5486
5512
  this.diagnostics.recordCycle({
5487
5513
  cycleNumber: this.cycleCount,
5488
5514
  startedAt: cycleStart,
@@ -6272,6 +6298,7 @@ export {
6272
6298
  writeConfigFile,
6273
6299
  encryptSecretPayload,
6274
6300
  loadConfig,
6301
+ updateSecureStore,
6275
6302
  generateSampleConfig,
6276
6303
  writeSampleConfig,
6277
6304
  CHAIN_CONFIGS,
package/dist/cli.js CHANGED
@@ -6,9 +6,10 @@ import {
6
6
  listTemplates,
7
7
  loadConfig,
8
8
  readConfigFile,
9
+ updateSecureStore,
9
10
  writeConfigFile,
10
11
  writeSampleConfig
11
- } from "./chunk-VDK4XPAC.js";
12
+ } from "./chunk-WTECTX2Z.js";
12
13
 
13
14
  // src/cli.ts
14
15
  import { Command } from "commander";
@@ -87,6 +88,21 @@ function cancelled() {
87
88
  clack.cancel("Setup cancelled.");
88
89
  process.exit(0);
89
90
  }
91
+ function isNonInteractive() {
92
+ return process.env.EXAGENT_NONINTERACTIVE === "1" || process.env.EXAGENT_NONINTERACTIVE === "true";
93
+ }
94
+ var LLM_KEY_PREFIXES = {
95
+ openai: "sk-",
96
+ anthropic: "sk-ant-"
97
+ };
98
+ function validateLlmKeyFormat(provider, key) {
99
+ if (!key.trim()) return "API key is required.";
100
+ const expectedPrefix = LLM_KEY_PREFIXES[provider];
101
+ if (expectedPrefix && !key.startsWith(expectedPrefix)) {
102
+ return `${provider} API keys typically start with "${expectedPrefix}". Double-check your key.`;
103
+ }
104
+ if (key.length < 10) return "API key seems too short.";
105
+ }
90
106
  async function consumeBootstrapPackage(config) {
91
107
  if (!config.secrets?.bootstrapToken) {
92
108
  return { apiToken: "" };
@@ -114,6 +130,22 @@ async function setupWallet(config) {
114
130
  printDone(`Using existing wallet: ${pc.dim(account.address)}`);
115
131
  return config.wallet.privateKey;
116
132
  }
133
+ if (isNonInteractive()) {
134
+ const mode = process.env.EXAGENT_WALLET_MODE || "generate";
135
+ if (mode === "import") {
136
+ const key = process.env.EXAGENT_WALLET_KEY;
137
+ if (!key || !/^0x[a-fA-F0-9]{64}$/.test(key)) {
138
+ throw new Error("EXAGENT_WALLET_KEY must be a valid 0x-prefixed 64-char hex private key in non-interactive mode");
139
+ }
140
+ const address3 = privateKeyToAccount(key).address;
141
+ printDone(`Wallet imported: ${pc.dim(address3)}`);
142
+ return key;
143
+ }
144
+ const privateKey2 = generatePrivateKey();
145
+ const address2 = privateKeyToAccount(privateKey2).address;
146
+ printDone(`Wallet created: ${pc.dim(address2)}`);
147
+ return privateKey2;
148
+ }
117
149
  const method = await clack.select({
118
150
  message: "How would you like to set up your wallet?",
119
151
  options: [
@@ -142,82 +174,75 @@ async function setupWallet(config) {
142
174
  return privateKey;
143
175
  }
144
176
  var LLM_PROVIDERS = ["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama"];
145
- async function setupLlm(config, bootstrapPayload) {
146
- let provider = config.llm?.provider || bootstrapPayload.llm?.provider;
147
- if (provider) {
148
- printInfo(`Provider: ${pc.cyan(provider)} ${pc.dim("(from dashboard)")}`);
149
- } else {
150
- const selected = await clack.select({
151
- message: "LLM provider:",
152
- options: LLM_PROVIDERS.map((p) => ({ value: p, label: p }))
153
- });
154
- if (clack.isCancel(selected)) cancelled();
155
- provider = selected;
156
- }
157
- let model = config.llm?.model || bootstrapPayload.llm?.model;
158
- if (model) {
159
- printInfo(`Model: ${pc.cyan(model)} ${pc.dim("(from dashboard)")}`);
160
- } else {
161
- const entered = await clack.text({
162
- message: "LLM model:",
163
- placeholder: "gpt-4o",
164
- validate: (val) => {
165
- if (!val.trim()) return "Model name is required.";
166
- }
167
- });
168
- if (clack.isCancel(entered)) cancelled();
169
- model = entered;
177
+ async function setupLlm(config) {
178
+ if (isNonInteractive()) {
179
+ const provider2 = process.env.EXAGENT_LLM_PROVIDER || config.llm?.provider;
180
+ const model2 = process.env.EXAGENT_LLM_MODEL || config.llm?.model;
181
+ const apiKey2 = process.env.EXAGENT_LLM_KEY;
182
+ if (!provider2) throw new Error("EXAGENT_LLM_PROVIDER required in non-interactive mode");
183
+ if (!model2) throw new Error("EXAGENT_LLM_MODEL required in non-interactive mode");
184
+ if (!apiKey2) throw new Error("EXAGENT_LLM_KEY required in non-interactive mode");
185
+ printDone("LLM configured");
186
+ return { provider: provider2, model: model2, apiKey: apiKey2 };
170
187
  }
171
- let apiKey;
172
- if (bootstrapPayload.llm?.apiKey) {
173
- const useBootstrap = await clack.confirm({
174
- message: "LLM API key received from dashboard. Use it?",
175
- initialValue: true
176
- });
177
- if (clack.isCancel(useBootstrap)) cancelled();
178
- if (useBootstrap) {
179
- apiKey = bootstrapPayload.llm.apiKey;
188
+ const defaultProvider = config.llm?.provider;
189
+ const providerOptions = LLM_PROVIDERS.map((p) => ({ value: p, label: p }));
190
+ const selected = await clack.select({
191
+ message: "LLM provider:",
192
+ options: providerOptions,
193
+ initialValue: defaultProvider || void 0
194
+ });
195
+ if (clack.isCancel(selected)) cancelled();
196
+ const provider = selected;
197
+ const defaultModel = config.llm?.model;
198
+ const entered = await clack.text({
199
+ message: "LLM model:",
200
+ placeholder: defaultModel || "gpt-4o",
201
+ defaultValue: defaultModel || void 0,
202
+ validate: (val) => {
203
+ if (!val.trim()) return "Model name is required.";
180
204
  }
181
- }
182
- if (!apiKey) {
183
- apiKey = config.llm?.apiKey;
184
- }
185
- if (!apiKey) {
186
- const entered = await clack.password({
187
- message: "LLM API key:",
188
- validate: (val) => {
189
- if (!val.trim()) return "API key is required.";
190
- }
191
- });
192
- if (clack.isCancel(entered)) cancelled();
193
- apiKey = entered;
194
- }
205
+ });
206
+ if (clack.isCancel(entered)) cancelled();
207
+ const model = entered;
208
+ const apiKey = await clack.password({
209
+ message: "LLM API key:",
210
+ validate: (val) => validateLlmKeyFormat(provider, val)
211
+ });
212
+ if (clack.isCancel(apiKey)) cancelled();
195
213
  printDone("LLM configured");
196
214
  return { provider, model, apiKey };
197
215
  }
198
216
  async function setupEncryption() {
217
+ if (isNonInteractive()) {
218
+ const password4 = process.env.EXAGENT_PASSWORD;
219
+ if (!password4 || password4.length < 12) {
220
+ throw new Error("EXAGENT_PASSWORD must be at least 12 characters in non-interactive mode");
221
+ }
222
+ return password4;
223
+ }
199
224
  printInfo(`Secrets encrypted with ${pc.cyan("AES-256-GCM")} (${pc.cyan("scrypt")} KDF)`);
200
225
  printInfo("The password never leaves this machine.");
201
226
  console.log();
202
- const password2 = await clack.password({
227
+ const password3 = await clack.password({
203
228
  message: "Choose a device password (12+ characters):",
204
229
  validate: (val) => {
205
230
  if (val.length < 12) return "Password must be at least 12 characters.";
206
231
  }
207
232
  });
208
- if (clack.isCancel(password2)) cancelled();
209
- const confirm2 = await clack.password({
233
+ if (clack.isCancel(password3)) cancelled();
234
+ const confirm = await clack.password({
210
235
  message: "Confirm password:",
211
236
  validate: (val) => {
212
- if (val !== password2) return "Passwords do not match.";
237
+ if (val !== password3) return "Passwords do not match.";
213
238
  }
214
239
  });
215
- if (clack.isCancel(confirm2)) cancelled();
216
- return password2;
240
+ if (clack.isCancel(confirm)) cancelled();
241
+ return password3;
217
242
  }
218
- function writeSecureStore(path, secrets, password2) {
243
+ function writeSecureStore(path, secrets, password3) {
219
244
  const secureStorePath = expandHomeDir(path);
220
- const encrypted = encryptSecretPayload(secrets, password2);
245
+ const encrypted = encryptSecretPayload(secrets, password3);
221
246
  const dir = dirname(secureStorePath);
222
247
  if (!existsSync(dir)) {
223
248
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -230,14 +255,27 @@ function writeSecureStore(path, secrets, password2) {
230
255
  return secureStorePath;
231
256
  }
232
257
  async function promptSecretPassword(question = "Device password:") {
233
- const password2 = await clack.password({ message: question });
234
- if (clack.isCancel(password2)) cancelled();
235
- return password2;
258
+ if (isNonInteractive()) {
259
+ const password4 = process.env.EXAGENT_PASSWORD || process.env.EXAGENT_SECRET_PASSWORD;
260
+ if (!password4) throw new Error("EXAGENT_PASSWORD required in non-interactive mode");
261
+ return password4;
262
+ }
263
+ const password3 = await clack.password({ message: question });
264
+ if (clack.isCancel(password3)) cancelled();
265
+ return password3;
236
266
  }
237
267
  async function ensureLocalSetup(configPath) {
238
268
  const config = readConfigFile(configPath);
239
269
  const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
240
270
  if (existingSecureStorePath && !config.secrets?.bootstrapToken && existsSync(existingSecureStorePath) && !config.apiToken && !config.wallet?.privateKey && !config.llm.apiKey) {
271
+ printBanner();
272
+ printSuccess("Already set up", [
273
+ `${pc.cyan("npx exagent run")} Start the agent`,
274
+ `${pc.cyan("npx exagent config")} Change LLM API key or model`,
275
+ `${pc.cyan("npx exagent status")} Check agent connection`,
276
+ "",
277
+ `${pc.dim("Dashboard:")} ${pc.cyan("https://exagent.io")}`
278
+ ]);
241
279
  return;
242
280
  }
243
281
  printBanner();
@@ -246,32 +284,35 @@ async function ensureLocalSetup(configPath) {
246
284
  const bootstrapPayload = await consumeBootstrapPackage(config);
247
285
  if (config.secrets?.bootstrapToken) {
248
286
  printDone("Bootstrap package consumed");
249
- if (bootstrapPayload.llm?.provider) {
250
- printInfo(`LLM config received: ${pc.cyan(bootstrapPayload.llm.provider)}${bootstrapPayload.llm.model ? ` / ${pc.cyan(bootstrapPayload.llm.model)}` : ""}`);
251
- }
252
287
  } else {
253
288
  printInfo("No bootstrap token \u2014 manual configuration");
254
289
  }
255
290
  printStep(2, 4, "Wallet setup");
256
291
  const walletPrivateKey = await setupWallet(config);
257
292
  printStep(3, 4, "LLM configuration");
258
- const llm = await setupLlm(config, bootstrapPayload);
293
+ const llm = await setupLlm(config);
259
294
  printStep(4, 4, "Device encryption");
260
- const password2 = await setupEncryption();
295
+ const password3 = await setupEncryption();
261
296
  const secrets = {
262
297
  apiToken: bootstrapPayload.apiToken || config.apiToken || "",
263
298
  walletPrivateKey,
264
299
  llmApiKey: llm.apiKey
265
300
  };
266
301
  if (!secrets.apiToken) {
267
- const token = await clack.password({
268
- message: "Agent relay token:",
269
- validate: (val) => {
270
- if (!val.trim()) return "Relay token is required.";
271
- }
272
- });
273
- if (clack.isCancel(token)) cancelled();
274
- secrets.apiToken = token;
302
+ if (isNonInteractive()) {
303
+ const token = process.env.EXAGENT_API_TOKEN;
304
+ if (!token) throw new Error("EXAGENT_API_TOKEN required in non-interactive mode (no relay token from bootstrap)");
305
+ secrets.apiToken = token;
306
+ } else {
307
+ const token = await clack.password({
308
+ message: "Agent relay token:",
309
+ validate: (val) => {
310
+ if (!val.trim()) return "Relay token is required.";
311
+ }
312
+ });
313
+ if (clack.isCancel(token)) cancelled();
314
+ secrets.apiToken = token;
315
+ }
275
316
  }
276
317
  const nextConfig = structuredClone(config);
277
318
  nextConfig.llm = {
@@ -285,23 +326,25 @@ async function ensureLocalSetup(configPath) {
285
326
  const secureStorePath = writeSecureStore(
286
327
  nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
287
328
  secrets,
288
- password2
329
+ password3
289
330
  );
290
331
  nextConfig.secrets = { secureStorePath };
291
332
  writeConfigFile(configPath, nextConfig);
292
333
  printDone(`Encrypted store: ${pc.dim(secureStorePath)}`);
293
334
  clack.outro(pc.green("Setup complete"));
294
335
  printSuccess("Ready", [
295
- `${pc.cyan("npx exagent run")} Start trading`,
296
- `${pc.cyan("npx exagent status")} Check connection`,
336
+ `${pc.cyan("npx exagent run")} Start the agent`,
337
+ `${pc.cyan("npx exagent config")} Change LLM API key or model`,
338
+ `${pc.cyan("npx exagent status")} Check agent connection`,
297
339
  "",
298
340
  `${pc.dim("Dashboard:")} ${pc.cyan("https://exagent.io")}`
299
341
  ]);
300
342
  }
301
343
 
302
344
  // src/cli.ts
345
+ import * as clack2 from "@clack/prompts";
303
346
  var program = new Command();
304
- program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.3.0");
347
+ program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.3.4");
305
348
  program.command("init").description("Create a sample agent configuration file").option("--agent-id <id>", "Agent ID (from dashboard)", "my-agent").option("--api-url <url>", "API server URL", "http://localhost:3002").option("--config <path>", "Config file path", "agent-config.json").action((opts) => {
306
349
  printBanner();
307
350
  writeSampleConfig(opts.agentId, opts.apiUrl, opts.config);
@@ -377,4 +420,96 @@ program.command("status").description("Check agent status").option("--config <pa
377
420
  process.exit(1);
378
421
  }
379
422
  });
423
+ var LLM_PROVIDERS2 = ["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama"];
424
+ program.command("config").description("Change LLM provider, model, or API key").option("--config <path>", "Config file path", "agent-config.json").action(async (opts) => {
425
+ try {
426
+ printBanner();
427
+ const config = readConfigFile(opts.config);
428
+ const secureStorePath = config.secrets?.secureStorePath;
429
+ if (!secureStorePath) {
430
+ printError("No secure store found. Run setup first: npx exagent setup");
431
+ process.exit(1);
432
+ }
433
+ clack2.intro(pc.bold("Agent Configuration"));
434
+ const password3 = await promptSecretPassword();
435
+ const currentConfig = await loadConfig(opts.config, {
436
+ getSecretPassword: async () => password3
437
+ });
438
+ const currentProvider = currentConfig.llm.provider;
439
+ const currentModel = currentConfig.llm.model || "not set";
440
+ const currentKey = currentConfig.llm.apiKey;
441
+ const maskedKey = currentKey ? `${currentKey.slice(0, 7)}...${currentKey.slice(-4)}` : "not set";
442
+ printInfo(`Provider: ${pc.cyan(currentProvider)}`);
443
+ printInfo(`Model: ${pc.cyan(currentModel)}`);
444
+ printInfo(`API key: ${pc.dim(maskedKey)}`);
445
+ console.log();
446
+ const action = await clack2.select({
447
+ message: "What would you like to change?",
448
+ options: [
449
+ { value: "key", label: "LLM API key" },
450
+ { value: "all", label: "Provider, model, and API key" }
451
+ ]
452
+ });
453
+ if (clack2.isCancel(action)) {
454
+ clack2.cancel("Cancelled.");
455
+ process.exit(0);
456
+ }
457
+ let newProvider = currentProvider;
458
+ let newModel = currentModel;
459
+ if (action === "all") {
460
+ const selectedProvider = await clack2.select({
461
+ message: "LLM provider:",
462
+ options: LLM_PROVIDERS2.map((p) => ({ value: p, label: p })),
463
+ initialValue: currentProvider || void 0
464
+ });
465
+ if (clack2.isCancel(selectedProvider)) {
466
+ clack2.cancel("Cancelled.");
467
+ process.exit(0);
468
+ }
469
+ newProvider = selectedProvider;
470
+ const enteredModel = await clack2.text({
471
+ message: "LLM model:",
472
+ defaultValue: currentModel,
473
+ validate: (val) => {
474
+ if (!val?.trim()) return "Model name is required.";
475
+ }
476
+ });
477
+ if (clack2.isCancel(enteredModel)) {
478
+ clack2.cancel("Cancelled.");
479
+ process.exit(0);
480
+ }
481
+ newModel = enteredModel;
482
+ }
483
+ const newKey = await clack2.password({
484
+ message: "New LLM API key:",
485
+ validate: (val) => {
486
+ if (!val?.trim()) return "API key is required.";
487
+ if (val.length < 10) return "API key seems too short.";
488
+ }
489
+ });
490
+ if (clack2.isCancel(newKey)) {
491
+ clack2.cancel("Cancelled.");
492
+ process.exit(0);
493
+ }
494
+ updateSecureStore(secureStorePath, password3, { llmApiKey: newKey });
495
+ const updatedConfig = readConfigFile(opts.config);
496
+ updatedConfig.llm = {
497
+ ...updatedConfig.llm,
498
+ provider: newProvider,
499
+ model: newModel
500
+ };
501
+ writeConfigFile(opts.config, updatedConfig);
502
+ clack2.outro(pc.green("Configuration updated"));
503
+ printSuccess("Updated", [
504
+ `${pc.dim("Provider:")} ${pc.cyan(newProvider)}`,
505
+ `${pc.dim("Model:")} ${pc.cyan(newModel)}`,
506
+ `${pc.dim("API key:")} ${pc.dim(`${newKey.slice(0, 7)}...${newKey.slice(-4)}`)}`,
507
+ "",
508
+ `Run ${pc.cyan("npx exagent run")} to start with the new configuration.`
509
+ ]);
510
+ } catch (err) {
511
+ printError(err.message);
512
+ process.exit(1);
513
+ }
514
+ });
380
515
  program.parse();
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  loadConfig,
44
44
  loadStrategy,
45
45
  writeSampleConfig
46
- } from "./chunk-VDK4XPAC.js";
46
+ } from "./chunk-WTECTX2Z.js";
47
47
  export {
48
48
  AcrossAdapter,
49
49
  AerodromeAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exagent/agent",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/cli.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { loadConfig, writeSampleConfig } from './config.js';
3
+ import { loadConfig, readConfigFile, writeConfigFile, updateSecureStore, writeSampleConfig } from './config.js';
4
4
  import { AgentRuntime } from './runtime.js';
5
5
  import { listTemplates } from './strategy/index.js';
6
6
  import { ensureLocalSetup, promptSecretPassword } from './setup.js';
7
- import { printBanner, printDone, printError, printSuccess, pc } from './ui.js';
7
+ import { printBanner, printDone, printError, printInfo, printSuccess, pc } from './ui.js';
8
+ import * as clack from '@clack/prompts';
8
9
 
9
10
  const program = new Command();
10
11
 
11
12
  program
12
13
  .name('exagent')
13
14
  .description('Exagent — LLM trading agent runtime')
14
- .version('0.3.0');
15
+ .version('0.3.4');
15
16
 
16
17
  program
17
18
  .command('init')
@@ -119,4 +120,122 @@ program
119
120
  }
120
121
  });
121
122
 
123
+ const LLM_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama'] as const;
124
+
125
+ program
126
+ .command('config')
127
+ .description('Change LLM provider, model, or API key')
128
+ .option('--config <path>', 'Config file path', 'agent-config.json')
129
+ .action(async (opts) => {
130
+ try {
131
+ printBanner();
132
+ const config = readConfigFile(opts.config);
133
+ const secureStorePath = config.secrets?.secureStorePath;
134
+
135
+ if (!secureStorePath) {
136
+ printError('No secure store found. Run setup first: npx exagent setup');
137
+ process.exit(1);
138
+ }
139
+
140
+ clack.intro(pc.bold('Agent Configuration'));
141
+
142
+ // Decrypt current secrets
143
+ const password = await promptSecretPassword();
144
+ const currentConfig = await loadConfig(opts.config, {
145
+ getSecretPassword: async () => password,
146
+ });
147
+
148
+ // Show current LLM settings
149
+ const currentProvider = currentConfig.llm.provider;
150
+ const currentModel = currentConfig.llm.model || 'not set';
151
+ const currentKey = currentConfig.llm.apiKey;
152
+ const maskedKey = currentKey
153
+ ? `${currentKey.slice(0, 7)}...${currentKey.slice(-4)}`
154
+ : 'not set';
155
+
156
+ printInfo(`Provider: ${pc.cyan(currentProvider)}`);
157
+ printInfo(`Model: ${pc.cyan(currentModel)}`);
158
+ printInfo(`API key: ${pc.dim(maskedKey)}`);
159
+ console.log();
160
+
161
+ const action = await clack.select({
162
+ message: 'What would you like to change?',
163
+ options: [
164
+ { value: 'key', label: 'LLM API key' },
165
+ { value: 'all', label: 'Provider, model, and API key' },
166
+ ],
167
+ });
168
+ if (clack.isCancel(action)) {
169
+ clack.cancel('Cancelled.');
170
+ process.exit(0);
171
+ }
172
+
173
+ let newProvider = currentProvider;
174
+ let newModel = currentModel;
175
+
176
+ if (action === 'all') {
177
+ const selectedProvider = await clack.select({
178
+ message: 'LLM provider:',
179
+ options: LLM_PROVIDERS.map(p => ({ value: p, label: p })),
180
+ initialValue: currentProvider || undefined,
181
+ });
182
+ if (clack.isCancel(selectedProvider)) {
183
+ clack.cancel('Cancelled.');
184
+ process.exit(0);
185
+ }
186
+ newProvider = selectedProvider;
187
+
188
+ const enteredModel = await clack.text({
189
+ message: 'LLM model:',
190
+ defaultValue: currentModel,
191
+ validate: (val) => {
192
+ if (!val?.trim()) return 'Model name is required.';
193
+ },
194
+ });
195
+ if (clack.isCancel(enteredModel)) {
196
+ clack.cancel('Cancelled.');
197
+ process.exit(0);
198
+ }
199
+ newModel = enteredModel;
200
+ }
201
+
202
+ const newKey = await clack.password({
203
+ message: 'New LLM API key:',
204
+ validate: (val) => {
205
+ if (!val?.trim()) return 'API key is required.';
206
+ if (val.length < 10) return 'API key seems too short.';
207
+ },
208
+ });
209
+ if (clack.isCancel(newKey)) {
210
+ clack.cancel('Cancelled.');
211
+ process.exit(0);
212
+ }
213
+
214
+ // Update secure store with new API key
215
+ updateSecureStore(secureStorePath, password, { llmApiKey: newKey });
216
+
217
+ // Update config file with new provider/model
218
+ const updatedConfig = readConfigFile(opts.config);
219
+ updatedConfig.llm = {
220
+ ...updatedConfig.llm,
221
+ provider: newProvider as typeof updatedConfig.llm.provider,
222
+ model: newModel,
223
+ };
224
+ writeConfigFile(opts.config, updatedConfig);
225
+
226
+ clack.outro(pc.green('Configuration updated'));
227
+
228
+ printSuccess('Updated', [
229
+ `${pc.dim('Provider:')} ${pc.cyan(newProvider)}`,
230
+ `${pc.dim('Model:')} ${pc.cyan(newModel)}`,
231
+ `${pc.dim('API key:')} ${pc.dim(`${newKey.slice(0, 7)}...${newKey.slice(-4)}`)}`,
232
+ '',
233
+ `Run ${pc.cyan('npx exagent run')} to start with the new configuration.`,
234
+ ]);
235
+ } catch (err) {
236
+ printError((err as Error).message);
237
+ process.exit(1);
238
+ }
239
+ });
240
+
122
241
  program.parse();
package/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, resolve } from 'node:path';
5
5
  import { z } from 'zod';
@@ -258,7 +258,7 @@ const secureStoreSchema = z.object({
258
258
 
259
259
  export type RuntimeConfigFile = z.infer<typeof configFileSchema>;
260
260
 
261
- const DEFAULT_SCRYPT_COST = 32768;
261
+ const DEFAULT_SCRYPT_COST = 16384;
262
262
  const DEFAULT_SCRYPT_BLOCK_SIZE = 8;
263
263
  const DEFAULT_SCRYPT_PARALLELIZATION = 1;
264
264
 
@@ -415,6 +415,23 @@ export async function loadConfig(path: string = 'agent-config.json', options: Lo
415
415
  return result.data;
416
416
  }
417
417
 
418
+ export function updateSecureStore(
419
+ path: string,
420
+ password: string,
421
+ updates: Partial<LocalSecretPayload>,
422
+ ): void {
423
+ const secrets = decryptSecretPayload(path, password);
424
+ const updated = { ...secrets, ...updates };
425
+ const encrypted = encryptSecretPayload(updated, password);
426
+ const secureStorePath = expandHomeDir(path);
427
+ writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
428
+ try {
429
+ chmodSync(secureStorePath, 0o600);
430
+ } catch {
431
+ // Best effort
432
+ }
433
+ }
434
+
418
435
  export function generateSampleConfig(agentId: string, apiUrl: string): string {
419
436
  const config: RuntimeConfigFile = {
420
437
  agentId,