@exagent/agent 0.3.1 → 0.3.3

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
@@ -8,7 +8,7 @@ import {
8
8
  readConfigFile,
9
9
  writeConfigFile,
10
10
  writeSampleConfig
11
- } from "./chunk-VDK4XPAC.js";
11
+ } from "./chunk-F4TCYYTD.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { Command } from "commander";
@@ -87,6 +87,21 @@ function cancelled() {
87
87
  clack.cancel("Setup cancelled.");
88
88
  process.exit(0);
89
89
  }
90
+ function isNonInteractive() {
91
+ return process.env.EXAGENT_NONINTERACTIVE === "1" || process.env.EXAGENT_NONINTERACTIVE === "true";
92
+ }
93
+ var LLM_KEY_PREFIXES = {
94
+ openai: "sk-",
95
+ anthropic: "sk-ant-"
96
+ };
97
+ function validateLlmKeyFormat(provider, key) {
98
+ if (!key.trim()) return "API key is required.";
99
+ const expectedPrefix = LLM_KEY_PREFIXES[provider];
100
+ if (expectedPrefix && !key.startsWith(expectedPrefix)) {
101
+ return `${provider} API keys typically start with "${expectedPrefix}". Double-check your key.`;
102
+ }
103
+ if (key.length < 10) return "API key seems too short.";
104
+ }
90
105
  async function consumeBootstrapPackage(config) {
91
106
  if (!config.secrets?.bootstrapToken) {
92
107
  return { apiToken: "" };
@@ -114,6 +129,22 @@ async function setupWallet(config) {
114
129
  printDone(`Using existing wallet: ${pc.dim(account.address)}`);
115
130
  return config.wallet.privateKey;
116
131
  }
132
+ if (isNonInteractive()) {
133
+ const mode = process.env.EXAGENT_WALLET_MODE || "generate";
134
+ if (mode === "import") {
135
+ const key = process.env.EXAGENT_WALLET_KEY;
136
+ if (!key || !/^0x[a-fA-F0-9]{64}$/.test(key)) {
137
+ throw new Error("EXAGENT_WALLET_KEY must be a valid 0x-prefixed 64-char hex private key in non-interactive mode");
138
+ }
139
+ const address3 = privateKeyToAccount(key).address;
140
+ printDone(`Wallet imported: ${pc.dim(address3)}`);
141
+ return key;
142
+ }
143
+ const privateKey2 = generatePrivateKey();
144
+ const address2 = privateKeyToAccount(privateKey2).address;
145
+ printDone(`Wallet created: ${pc.dim(address2)}`);
146
+ return privateKey2;
147
+ }
117
148
  const method = await clack.select({
118
149
  message: "How would you like to set up your wallet?",
119
150
  options: [
@@ -143,7 +174,18 @@ async function setupWallet(config) {
143
174
  }
144
175
  var LLM_PROVIDERS = ["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama"];
145
176
  async function setupLlm(config, bootstrapPayload) {
146
- let provider = config.llm?.provider || bootstrapPayload.llm?.provider;
177
+ const hasRealLlmConfig = !!(bootstrapPayload.llm?.apiKey || config.llm?.apiKey);
178
+ if (isNonInteractive()) {
179
+ const provider2 = process.env.EXAGENT_LLM_PROVIDER || bootstrapPayload.llm?.provider || (hasRealLlmConfig ? config.llm?.provider : void 0);
180
+ const model2 = process.env.EXAGENT_LLM_MODEL || bootstrapPayload.llm?.model || (hasRealLlmConfig ? config.llm?.model : void 0);
181
+ const apiKey2 = process.env.EXAGENT_LLM_KEY || bootstrapPayload.llm?.apiKey || config.llm?.apiKey;
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 };
187
+ }
188
+ let provider = bootstrapPayload.llm?.provider || (hasRealLlmConfig ? config.llm?.provider : void 0);
147
189
  if (provider) {
148
190
  printInfo(`Provider: ${pc.cyan(provider)} ${pc.dim("(from dashboard)")}`);
149
191
  } else {
@@ -154,7 +196,7 @@ async function setupLlm(config, bootstrapPayload) {
154
196
  if (clack.isCancel(selected)) cancelled();
155
197
  provider = selected;
156
198
  }
157
- let model = config.llm?.model || bootstrapPayload.llm?.model;
199
+ let model = bootstrapPayload.llm?.model || (hasRealLlmConfig ? config.llm?.model : void 0);
158
200
  if (model) {
159
201
  printInfo(`Model: ${pc.cyan(model)} ${pc.dim("(from dashboard)")}`);
160
202
  } else {
@@ -185,9 +227,7 @@ async function setupLlm(config, bootstrapPayload) {
185
227
  if (!apiKey) {
186
228
  const entered = await clack.password({
187
229
  message: "LLM API key:",
188
- validate: (val) => {
189
- if (!val.trim()) return "API key is required.";
190
- }
230
+ validate: (val) => validateLlmKeyFormat(provider, val)
191
231
  });
192
232
  if (clack.isCancel(entered)) cancelled();
193
233
  apiKey = entered;
@@ -196,6 +236,13 @@ async function setupLlm(config, bootstrapPayload) {
196
236
  return { provider, model, apiKey };
197
237
  }
198
238
  async function setupEncryption() {
239
+ if (isNonInteractive()) {
240
+ const password3 = process.env.EXAGENT_PASSWORD;
241
+ if (!password3 || password3.length < 12) {
242
+ throw new Error("EXAGENT_PASSWORD must be at least 12 characters in non-interactive mode");
243
+ }
244
+ return password3;
245
+ }
199
246
  printInfo(`Secrets encrypted with ${pc.cyan("AES-256-GCM")} (${pc.cyan("scrypt")} KDF)`);
200
247
  printInfo("The password never leaves this machine.");
201
248
  console.log();
@@ -230,6 +277,11 @@ function writeSecureStore(path, secrets, password2) {
230
277
  return secureStorePath;
231
278
  }
232
279
  async function promptSecretPassword(question = "Device password:") {
280
+ if (isNonInteractive()) {
281
+ const password3 = process.env.EXAGENT_PASSWORD || process.env.EXAGENT_SECRET_PASSWORD;
282
+ if (!password3) throw new Error("EXAGENT_PASSWORD required in non-interactive mode");
283
+ return password3;
284
+ }
233
285
  const password2 = await clack.password({ message: question });
234
286
  if (clack.isCancel(password2)) cancelled();
235
287
  return password2;
@@ -264,14 +316,20 @@ async function ensureLocalSetup(configPath) {
264
316
  llmApiKey: llm.apiKey
265
317
  };
266
318
  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;
319
+ if (isNonInteractive()) {
320
+ const token = process.env.EXAGENT_API_TOKEN;
321
+ if (!token) throw new Error("EXAGENT_API_TOKEN required in non-interactive mode (no relay token from bootstrap)");
322
+ secrets.apiToken = token;
323
+ } else {
324
+ const token = await clack.password({
325
+ message: "Agent relay token:",
326
+ validate: (val) => {
327
+ if (!val.trim()) return "Relay token is required.";
328
+ }
329
+ });
330
+ if (clack.isCancel(token)) cancelled();
331
+ secrets.apiToken = token;
332
+ }
275
333
  }
276
334
  const nextConfig = structuredClone(config);
277
335
  nextConfig.llm = {
@@ -301,7 +359,7 @@ async function ensureLocalSetup(configPath) {
301
359
 
302
360
  // src/cli.ts
303
361
  var program = new Command();
304
- program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.3.0");
362
+ program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.3.3");
305
363
  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
364
  printBanner();
307
365
  writeSampleConfig(opts.agentId, opts.apiUrl, opts.config);
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-F4TCYYTD.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.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@clack/prompts": "^1.1.0",
22
- "@exagent/sdk": "workspace:*",
22
+ "@exagent/sdk": "^0.2.1",
23
23
  "@polymarket/clob-client": "^4.0.0",
24
24
  "boxen": "^8.0.1",
25
25
  "commander": "^12.0.0",
package/src/cli.ts CHANGED
@@ -11,7 +11,7 @@ const program = new Command();
11
11
  program
12
12
  .name('exagent')
13
13
  .description('Exagent — LLM trading agent runtime')
14
- .version('0.3.0');
14
+ .version('0.3.3');
15
15
 
16
16
  program
17
17
  .command('init')
package/src/config.ts CHANGED
@@ -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
 
package/src/runtime.ts CHANGED
@@ -58,6 +58,21 @@ try { SDK_VERSION = _require('../package.json').version; } catch {}
58
58
  /** Number of consecutive cycle failures before switching to idle */
59
59
  const MAX_CONSECUTIVE_FAILURES = 3;
60
60
 
61
+ function getCycleErrorHint(category: string, msg: string): string | null {
62
+ const lower = msg.toLowerCase();
63
+ if (category === 'auth' || lower.includes('401') || lower.includes('unauthorized') || lower.includes('invalid api key') || lower.includes('authentication'))
64
+ return 'Check your LLM API key — update via npx exagent setup';
65
+ if (category === 'network' || lower.includes('econnrefused') || lower.includes('enotfound') || lower.includes('fetch failed') || lower.includes('timeout'))
66
+ return 'Network connectivity issue — check your internet connection';
67
+ if (category === 'venue' || lower.includes('hyperliquid') || lower.includes('polymarket') || lower.includes('venue'))
68
+ return 'Venue API error — the venue may be experiencing issues';
69
+ if (category === 'strategy' || lower.includes('invalid') && lower.includes('signal') || lower.includes('strategy'))
70
+ return 'Strategy returned invalid output — check your strategy file';
71
+ if (lower.includes('rate limit') || lower.includes('429'))
72
+ return 'Rate limited — consider increasing tradingIntervalMs';
73
+ return null;
74
+ }
75
+
61
76
  export class AgentRuntime {
62
77
  private config: RuntimeConfig;
63
78
  private relay: RelayClient;
@@ -809,12 +824,13 @@ export class AgentRuntime {
809
824
  timings.totalMs = Date.now() - cycleStart;
810
825
 
811
826
  const categorized = this.diagnostics.recordError(err as Error);
812
- log.error('cycle', `Cycle error: ${errMsg}`, {
827
+ const hint = getCycleErrorHint(categorized.category, errMsg);
828
+ log.error('cycle', `Cycle error: ${errMsg}${hint ? ` — ${hint}` : ''}`, {
813
829
  cycle: this.cycleCount,
814
830
  totalMs: timings.totalMs,
815
831
  category: categorized.category,
816
832
  });
817
- this.signal.reportError('Cycle Error', errMsg);
833
+ this.signal.reportError('Cycle Error', `${errMsg}${hint ? ` — ${hint}` : ''}`);
818
834
 
819
835
  this.diagnostics.recordCycle({
820
836
  cycleNumber: this.cycleCount,
package/src/setup.ts CHANGED
@@ -33,6 +33,24 @@ function cancelled(): never {
33
33
  process.exit(0);
34
34
  }
35
35
 
36
+ function isNonInteractive(): boolean {
37
+ return process.env.EXAGENT_NONINTERACTIVE === '1' || process.env.EXAGENT_NONINTERACTIVE === 'true';
38
+ }
39
+
40
+ const LLM_KEY_PREFIXES: Record<string, string> = {
41
+ openai: 'sk-',
42
+ anthropic: 'sk-ant-',
43
+ };
44
+
45
+ function validateLlmKeyFormat(provider: string, key: string): string | undefined {
46
+ if (!key.trim()) return 'API key is required.';
47
+ const expectedPrefix = LLM_KEY_PREFIXES[provider];
48
+ if (expectedPrefix && !key.startsWith(expectedPrefix)) {
49
+ return `${provider} API keys typically start with "${expectedPrefix}". Double-check your key.`;
50
+ }
51
+ if (key.length < 10) return 'API key seems too short.';
52
+ }
53
+
36
54
  // ---------------------------------------------------------------------------
37
55
  // Step 1: Bootstrap
38
56
  // ---------------------------------------------------------------------------
@@ -74,6 +92,24 @@ async function setupWallet(config: RuntimeConfigFile): Promise<string> {
74
92
  return config.wallet.privateKey;
75
93
  }
76
94
 
95
+ // Non-interactive: env var wallet
96
+ if (isNonInteractive()) {
97
+ const mode = process.env.EXAGENT_WALLET_MODE || 'generate';
98
+ if (mode === 'import') {
99
+ const key = process.env.EXAGENT_WALLET_KEY;
100
+ if (!key || !/^0x[a-fA-F0-9]{64}$/.test(key)) {
101
+ throw new Error('EXAGENT_WALLET_KEY must be a valid 0x-prefixed 64-char hex private key in non-interactive mode');
102
+ }
103
+ const address = privateKeyToAccount(key as `0x${string}`).address;
104
+ printDone(`Wallet imported: ${pc.dim(address)}`);
105
+ return key;
106
+ }
107
+ const privateKey = generatePrivateKey();
108
+ const address = privateKeyToAccount(privateKey).address;
109
+ printDone(`Wallet created: ${pc.dim(address)}`);
110
+ return privateKey;
111
+ }
112
+
77
113
  const method = await clack.select({
78
114
  message: 'How would you like to set up your wallet?',
79
115
  options: [
@@ -116,8 +152,24 @@ async function setupLlm(
116
152
  config: RuntimeConfigFile,
117
153
  bootstrapPayload: BootstrapPayload,
118
154
  ): Promise<{ provider: string; model: string; apiKey: string }> {
119
- // Provider
120
- let provider = config.llm?.provider || bootstrapPayload.llm?.provider;
155
+ // D8: If bootstrap has no LLM payload AND config has no real apiKey,
156
+ // the config's provider/model are just wizard defaults — don't trust them.
157
+ const hasRealLlmConfig = !!(bootstrapPayload.llm?.apiKey || config.llm?.apiKey);
158
+
159
+ // Non-interactive mode
160
+ if (isNonInteractive()) {
161
+ const provider = process.env.EXAGENT_LLM_PROVIDER || bootstrapPayload.llm?.provider || (hasRealLlmConfig ? config.llm?.provider : undefined);
162
+ const model = process.env.EXAGENT_LLM_MODEL || bootstrapPayload.llm?.model || (hasRealLlmConfig ? config.llm?.model : undefined);
163
+ const apiKey = process.env.EXAGENT_LLM_KEY || bootstrapPayload.llm?.apiKey || config.llm?.apiKey;
164
+ if (!provider) throw new Error('EXAGENT_LLM_PROVIDER required in non-interactive mode');
165
+ if (!model) throw new Error('EXAGENT_LLM_MODEL required in non-interactive mode');
166
+ if (!apiKey) throw new Error('EXAGENT_LLM_KEY required in non-interactive mode');
167
+ printDone('LLM configured');
168
+ return { provider, model, apiKey };
169
+ }
170
+
171
+ // Provider — only trust config value if we have a real LLM config
172
+ let provider = bootstrapPayload.llm?.provider || (hasRealLlmConfig ? config.llm?.provider : undefined);
121
173
  if (provider) {
122
174
  printInfo(`Provider: ${pc.cyan(provider)} ${pc.dim('(from dashboard)')}`);
123
175
  } else {
@@ -129,8 +181,8 @@ async function setupLlm(
129
181
  provider = selected;
130
182
  }
131
183
 
132
- // Model
133
- let model = config.llm?.model || bootstrapPayload.llm?.model;
184
+ // Model — only trust config value if we have a real LLM config
185
+ let model = bootstrapPayload.llm?.model || (hasRealLlmConfig ? config.llm?.model : undefined);
134
186
  if (model) {
135
187
  printInfo(`Model: ${pc.cyan(model)} ${pc.dim('(from dashboard)')}`);
136
188
  } else {
@@ -163,9 +215,7 @@ async function setupLlm(
163
215
  if (!apiKey) {
164
216
  const entered = await clack.password({
165
217
  message: 'LLM API key:',
166
- validate: (val) => {
167
- if (!val.trim()) return 'API key is required.';
168
- },
218
+ validate: (val) => validateLlmKeyFormat(provider, val),
169
219
  });
170
220
  if (clack.isCancel(entered)) cancelled();
171
221
  apiKey = entered;
@@ -180,6 +230,15 @@ async function setupLlm(
180
230
  // ---------------------------------------------------------------------------
181
231
 
182
232
  async function setupEncryption(): Promise<string> {
233
+ // Non-interactive mode
234
+ if (isNonInteractive()) {
235
+ const password = process.env.EXAGENT_PASSWORD;
236
+ if (!password || password.length < 12) {
237
+ throw new Error('EXAGENT_PASSWORD must be at least 12 characters in non-interactive mode');
238
+ }
239
+ return password;
240
+ }
241
+
183
242
  printInfo(`Secrets encrypted with ${pc.cyan('AES-256-GCM')} (${pc.cyan('scrypt')} KDF)`);
184
243
  printInfo('The password never leaves this machine.');
185
244
  console.log();
@@ -228,6 +287,11 @@ function writeSecureStore(path: string, secrets: LocalSecretPayload, password: s
228
287
  // ---------------------------------------------------------------------------
229
288
 
230
289
  export async function promptSecretPassword(question: string = 'Device password:'): Promise<string> {
290
+ if (isNonInteractive()) {
291
+ const password = process.env.EXAGENT_PASSWORD || process.env.EXAGENT_SECRET_PASSWORD;
292
+ if (!password) throw new Error('EXAGENT_PASSWORD required in non-interactive mode');
293
+ return password;
294
+ }
231
295
  const password = await clack.password({ message: question });
232
296
  if (clack.isCancel(password)) cancelled();
233
297
  return password;
@@ -287,15 +351,20 @@ export async function ensureLocalSetup(configPath: string): Promise<void> {
287
351
  };
288
352
 
289
353
  if (!secrets.apiToken) {
290
- // Prompt for relay token if not available from bootstrap or config
291
- const token = await clack.password({
292
- message: 'Agent relay token:',
293
- validate: (val) => {
294
- if (!val.trim()) return 'Relay token is required.';
295
- },
296
- });
297
- if (clack.isCancel(token)) cancelled();
298
- secrets.apiToken = token;
354
+ if (isNonInteractive()) {
355
+ const token = process.env.EXAGENT_API_TOKEN;
356
+ if (!token) throw new Error('EXAGENT_API_TOKEN required in non-interactive mode (no relay token from bootstrap)');
357
+ secrets.apiToken = token;
358
+ } else {
359
+ const token = await clack.password({
360
+ message: 'Agent relay token:',
361
+ validate: (val) => {
362
+ if (!val.trim()) return 'Relay token is required.';
363
+ },
364
+ });
365
+ if (clack.isCancel(token)) cancelled();
366
+ secrets.apiToken = token;
367
+ }
299
368
  }
300
369
 
301
370
  const nextConfig = structuredClone(config);