@exagent/agent 0.2.1 → 0.3.0
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/.turbo/turbo-build.log +4 -4
- package/dist/chunk-TDACLKD7.js +5867 -0
- package/dist/chunk-UAP5CTHB.js +5985 -0
- package/dist/cli.js +204 -5
- package/dist/index.d.ts +14 -1
- package/dist/index.js +1 -1
- package/package.json +8 -8
- package/src/cli.ts +25 -4
- package/src/config.ts +225 -25
- package/src/setup.ts +233 -0
- package/src/strategy/loader.ts +98 -0
package/src/config.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
2
5
|
import { z } from 'zod';
|
|
3
6
|
import type { LLMProvider } from '@exagent/sdk';
|
|
4
7
|
|
|
@@ -20,6 +23,11 @@ export interface RuntimeConfig {
|
|
|
20
23
|
strategy: {
|
|
21
24
|
file?: string;
|
|
22
25
|
template?: string;
|
|
26
|
+
prompt?: {
|
|
27
|
+
name?: string;
|
|
28
|
+
systemPrompt: string;
|
|
29
|
+
venues?: string[];
|
|
30
|
+
};
|
|
23
31
|
};
|
|
24
32
|
trading: {
|
|
25
33
|
mode: 'live' | 'paper';
|
|
@@ -79,15 +87,43 @@ export interface RuntimeConfig {
|
|
|
79
87
|
};
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
|
|
90
|
+
export interface LocalSecretPayload {
|
|
91
|
+
apiToken: string;
|
|
92
|
+
walletPrivateKey?: string;
|
|
93
|
+
llmApiKey?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SecureStoreFile {
|
|
97
|
+
version: 1;
|
|
98
|
+
algorithm: 'aes-256-gcm';
|
|
99
|
+
kdf: {
|
|
100
|
+
name: 'scrypt';
|
|
101
|
+
salt: string;
|
|
102
|
+
keyLength: 32;
|
|
103
|
+
cost: number;
|
|
104
|
+
blockSize: number;
|
|
105
|
+
parallelization: number;
|
|
106
|
+
};
|
|
107
|
+
iv: string;
|
|
108
|
+
ciphertext: string;
|
|
109
|
+
authTag: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface LoadConfigOptions {
|
|
113
|
+
getSecretPassword?: () => Promise<string>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const providerEnum = z.enum(['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama']);
|
|
117
|
+
|
|
118
|
+
const runtimeSchema = z.object({
|
|
83
119
|
agentId: z.string(),
|
|
84
120
|
apiUrl: z.string().url(),
|
|
85
|
-
apiToken: z.string(),
|
|
121
|
+
apiToken: z.string().min(1),
|
|
86
122
|
wallet: z.object({
|
|
87
|
-
privateKey: z.string(),
|
|
123
|
+
privateKey: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
88
124
|
}).optional(),
|
|
89
125
|
llm: z.object({
|
|
90
|
-
provider:
|
|
126
|
+
provider: providerEnum,
|
|
91
127
|
model: z.string().optional(),
|
|
92
128
|
apiKey: z.string().optional(),
|
|
93
129
|
endpoint: z.string().optional(),
|
|
@@ -97,6 +133,11 @@ const configSchema = z.object({
|
|
|
97
133
|
strategy: z.object({
|
|
98
134
|
file: z.string().optional(),
|
|
99
135
|
template: z.string().optional(),
|
|
136
|
+
prompt: z.object({
|
|
137
|
+
name: z.string().optional(),
|
|
138
|
+
systemPrompt: z.string().min(1),
|
|
139
|
+
venues: z.array(z.string()).optional(),
|
|
140
|
+
}).optional(),
|
|
100
141
|
}),
|
|
101
142
|
trading: z.object({
|
|
102
143
|
mode: z.enum(['live', 'paper']).default('paper'),
|
|
@@ -156,23 +197,172 @@ const configSchema = z.object({
|
|
|
156
197
|
}).optional(),
|
|
157
198
|
});
|
|
158
199
|
|
|
159
|
-
|
|
200
|
+
const configFileSchema = z.object({
|
|
201
|
+
agentId: z.string(),
|
|
202
|
+
apiUrl: z.string().url(),
|
|
203
|
+
apiToken: z.string().min(1).optional(),
|
|
204
|
+
wallet: z.object({
|
|
205
|
+
privateKey: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
206
|
+
}).optional(),
|
|
207
|
+
llm: z.object({
|
|
208
|
+
provider: providerEnum.optional(),
|
|
209
|
+
model: z.string().optional(),
|
|
210
|
+
apiKey: z.string().optional(),
|
|
211
|
+
endpoint: z.string().optional(),
|
|
212
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
213
|
+
maxTokens: z.number().optional(),
|
|
214
|
+
}).default({}),
|
|
215
|
+
strategy: z.object({
|
|
216
|
+
file: z.string().optional(),
|
|
217
|
+
template: z.string().optional(),
|
|
218
|
+
prompt: z.object({
|
|
219
|
+
name: z.string().optional(),
|
|
220
|
+
systemPrompt: z.string().min(1),
|
|
221
|
+
venues: z.array(z.string()).optional(),
|
|
222
|
+
}).optional(),
|
|
223
|
+
}),
|
|
224
|
+
trading: runtimeSchema.shape.trading,
|
|
225
|
+
venues: runtimeSchema.shape.venues,
|
|
226
|
+
relay: runtimeSchema.shape.relay,
|
|
227
|
+
llmBudget: runtimeSchema.shape.llmBudget,
|
|
228
|
+
rpcOverrides: runtimeSchema.shape.rpcOverrides,
|
|
229
|
+
logging: runtimeSchema.shape.logging,
|
|
230
|
+
secrets: z.object({
|
|
231
|
+
bootstrapToken: z.string().optional(),
|
|
232
|
+
bootstrapExpiresAt: z.string().optional(),
|
|
233
|
+
secureStorePath: z.string().optional(),
|
|
234
|
+
}).optional(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const secureStoreSchema = z.object({
|
|
238
|
+
version: z.literal(1),
|
|
239
|
+
algorithm: z.literal('aes-256-gcm'),
|
|
240
|
+
kdf: z.object({
|
|
241
|
+
name: z.literal('scrypt'),
|
|
242
|
+
salt: z.string(),
|
|
243
|
+
keyLength: z.literal(32),
|
|
244
|
+
cost: z.number().int().positive(),
|
|
245
|
+
blockSize: z.number().int().positive(),
|
|
246
|
+
parallelization: z.number().int().positive(),
|
|
247
|
+
}),
|
|
248
|
+
iv: z.string(),
|
|
249
|
+
ciphertext: z.string(),
|
|
250
|
+
authTag: z.string(),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
export type RuntimeConfigFile = z.infer<typeof configFileSchema>;
|
|
254
|
+
|
|
255
|
+
const DEFAULT_SCRYPT_COST = 32768;
|
|
256
|
+
const DEFAULT_SCRYPT_BLOCK_SIZE = 8;
|
|
257
|
+
const DEFAULT_SCRYPT_PARALLELIZATION = 1;
|
|
258
|
+
|
|
259
|
+
function expandHomeDir(path: string): string {
|
|
260
|
+
if (!path.startsWith('~/')) return path;
|
|
261
|
+
return resolve(homedir(), path.slice(2));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function getDefaultSecureStorePath(agentId: string): string {
|
|
265
|
+
return resolve(homedir(), '.exagent', 'agents', agentId, 'secrets.json');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function readConfigFile(path: string = 'agent-config.json'): RuntimeConfigFile {
|
|
160
269
|
if (!existsSync(path)) {
|
|
161
270
|
throw new Error(`Config file not found: ${path}. Run 'exagent init' first.`);
|
|
162
271
|
}
|
|
163
272
|
|
|
164
|
-
const raw = readFileSync(path, 'utf-8');
|
|
165
273
|
let parsed: unknown;
|
|
166
|
-
|
|
167
274
|
try {
|
|
168
|
-
parsed = JSON.parse(
|
|
275
|
+
parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
169
276
|
} catch {
|
|
170
277
|
throw new Error(`Invalid JSON in ${path}`);
|
|
171
278
|
}
|
|
172
279
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
280
|
+
const result = configFileSchema.safeParse(parsed);
|
|
281
|
+
if (!result.success) {
|
|
282
|
+
const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
283
|
+
throw new Error(`Invalid config file:\n${issues}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result.data;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function writeConfigFile(path: string, config: RuntimeConfigFile): void {
|
|
290
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function encryptSecretPayload(payload: LocalSecretPayload, password: string): SecureStoreFile {
|
|
294
|
+
const salt = randomBytes(16);
|
|
295
|
+
const iv = randomBytes(12);
|
|
296
|
+
const key = scryptSync(password, salt, 32, {
|
|
297
|
+
N: DEFAULT_SCRYPT_COST,
|
|
298
|
+
r: DEFAULT_SCRYPT_BLOCK_SIZE,
|
|
299
|
+
p: DEFAULT_SCRYPT_PARALLELIZATION,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
303
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
304
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
version: 1,
|
|
308
|
+
algorithm: 'aes-256-gcm',
|
|
309
|
+
kdf: {
|
|
310
|
+
name: 'scrypt',
|
|
311
|
+
salt: salt.toString('hex'),
|
|
312
|
+
keyLength: 32,
|
|
313
|
+
cost: DEFAULT_SCRYPT_COST,
|
|
314
|
+
blockSize: DEFAULT_SCRYPT_BLOCK_SIZE,
|
|
315
|
+
parallelization: DEFAULT_SCRYPT_PARALLELIZATION,
|
|
316
|
+
},
|
|
317
|
+
iv: iv.toString('hex'),
|
|
318
|
+
ciphertext: ciphertext.toString('hex'),
|
|
319
|
+
authTag: cipher.getAuthTag().toString('hex'),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function decryptSecretPayload(path: string, password: string): LocalSecretPayload {
|
|
324
|
+
const secureStorePath = expandHomeDir(path);
|
|
325
|
+
if (!existsSync(secureStorePath)) {
|
|
326
|
+
throw new Error(`Encrypted secret store not found: ${secureStorePath}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let parsed: unknown;
|
|
330
|
+
try {
|
|
331
|
+
parsed = JSON.parse(readFileSync(secureStorePath, 'utf-8'));
|
|
332
|
+
} catch {
|
|
333
|
+
throw new Error(`Invalid JSON in encrypted secret store: ${secureStorePath}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const result = secureStoreSchema.safeParse(parsed);
|
|
337
|
+
if (!result.success) {
|
|
338
|
+
const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
339
|
+
throw new Error(`Invalid encrypted secret store:\n${issues}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const store = result.data;
|
|
343
|
+
const key = scryptSync(password, Buffer.from(store.kdf.salt, 'hex'), store.kdf.keyLength, {
|
|
344
|
+
N: store.kdf.cost,
|
|
345
|
+
r: store.kdf.blockSize,
|
|
346
|
+
p: store.kdf.parallelization,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const decipher = createDecipheriv(store.algorithm, key, Buffer.from(store.iv, 'hex'));
|
|
351
|
+
decipher.setAuthTag(Buffer.from(store.authTag, 'hex'));
|
|
352
|
+
const plaintext = Buffer.concat([
|
|
353
|
+
decipher.update(Buffer.from(store.ciphertext, 'hex')),
|
|
354
|
+
decipher.final(),
|
|
355
|
+
]);
|
|
356
|
+
return JSON.parse(plaintext.toString('utf8')) as LocalSecretPayload;
|
|
357
|
+
} catch {
|
|
358
|
+
throw new Error('Unable to decrypt local secret store. Check your password.');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function loadConfig(path: string = 'agent-config.json', options: LoadConfigOptions = {}): Promise<RuntimeConfig> {
|
|
363
|
+
const parsed = readConfigFile(path);
|
|
364
|
+
const config = structuredClone(parsed) as RuntimeConfigFile & Record<string, unknown>;
|
|
365
|
+
const llm = { ...(config.llm || {}) } as Record<string, unknown>;
|
|
176
366
|
|
|
177
367
|
if (process.env.EXAGENT_LLM_PROVIDER) llm.provider = process.env.EXAGENT_LLM_PROVIDER;
|
|
178
368
|
if (process.env.EXAGENT_LLM_MODEL) llm.model = process.env.EXAGENT_LLM_MODEL;
|
|
@@ -183,9 +373,24 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
|
|
|
183
373
|
config.wallet = { privateKey: process.env.EXAGENT_WALLET_PRIVATE_KEY };
|
|
184
374
|
}
|
|
185
375
|
|
|
376
|
+
if ((!config.apiToken || !llm.apiKey || !config.wallet) && parsed.secrets?.secureStorePath) {
|
|
377
|
+
const password = process.env.EXAGENT_SECRET_PASSWORD || await options.getSecretPassword?.();
|
|
378
|
+
if (!password) {
|
|
379
|
+
throw new Error('Encrypted secret store found, but no password was provided.');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const secrets = decryptSecretPayload(parsed.secrets.secureStorePath, password);
|
|
383
|
+
if (!config.apiToken) config.apiToken = secrets.apiToken;
|
|
384
|
+
if (!config.wallet && secrets.walletPrivateKey) config.wallet = { privateKey: secrets.walletPrivateKey };
|
|
385
|
+
if (!llm.apiKey && secrets.llmApiKey) llm.apiKey = secrets.llmApiKey;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if ((!config.apiToken || !llm.apiKey || !config.wallet) && parsed.secrets?.bootstrapToken && !parsed.secrets?.secureStorePath) {
|
|
389
|
+
throw new Error(`Config ${path} still requires first-time secure setup. Run 'exagent setup --config ${path}' or start the agent interactively.`);
|
|
390
|
+
}
|
|
391
|
+
|
|
186
392
|
config.llm = llm;
|
|
187
393
|
|
|
188
|
-
// Apply RPC endpoint overrides from environment
|
|
189
394
|
const rpcOverrides: Record<string, string> = (config.rpcOverrides as Record<string, string>) || {};
|
|
190
395
|
if (process.env.EXAGENT_RPC_BASE) rpcOverrides.base = process.env.EXAGENT_RPC_BASE;
|
|
191
396
|
if (process.env.EXAGENT_RPC_ARBITRUM) rpcOverrides.arbitrum = process.env.EXAGENT_RPC_ARBITRUM;
|
|
@@ -195,9 +400,9 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
|
|
|
195
400
|
config.rpcOverrides = rpcOverrides;
|
|
196
401
|
}
|
|
197
402
|
|
|
198
|
-
const result =
|
|
403
|
+
const result = runtimeSchema.safeParse(config);
|
|
199
404
|
if (!result.success) {
|
|
200
|
-
const issues = result.error.issues.map(
|
|
405
|
+
const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
201
406
|
throw new Error(`Invalid config:\n${issues}`);
|
|
202
407
|
}
|
|
203
408
|
|
|
@@ -205,18 +410,10 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
|
|
|
205
410
|
}
|
|
206
411
|
|
|
207
412
|
export function generateSampleConfig(agentId: string, apiUrl: string): string {
|
|
208
|
-
const config = {
|
|
413
|
+
const config: RuntimeConfigFile = {
|
|
209
414
|
agentId,
|
|
210
415
|
apiUrl,
|
|
211
|
-
|
|
212
|
-
wallet: {
|
|
213
|
-
privateKey: '<your-hex-private-key>',
|
|
214
|
-
},
|
|
215
|
-
llm: {
|
|
216
|
-
provider: 'openai',
|
|
217
|
-
model: 'gpt-4o',
|
|
218
|
-
apiKey: '<your-api-key>',
|
|
219
|
-
},
|
|
416
|
+
llm: {},
|
|
220
417
|
strategy: {
|
|
221
418
|
template: 'momentum',
|
|
222
419
|
},
|
|
@@ -266,6 +463,9 @@ export function generateSampleConfig(agentId: string, apiUrl: string): string {
|
|
|
266
463
|
heartbeatIntervalMs: 30000,
|
|
267
464
|
reconnectMaxAttempts: 50,
|
|
268
465
|
},
|
|
466
|
+
secrets: {
|
|
467
|
+
secureStorePath: getDefaultSecureStorePath(agentId),
|
|
468
|
+
},
|
|
269
469
|
};
|
|
270
470
|
|
|
271
471
|
return JSON.stringify(config, null, 2);
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
7
|
+
import {
|
|
8
|
+
encryptSecretPayload,
|
|
9
|
+
getDefaultSecureStorePath,
|
|
10
|
+
readConfigFile,
|
|
11
|
+
type LocalSecretPayload,
|
|
12
|
+
type RuntimeConfigFile,
|
|
13
|
+
writeConfigFile,
|
|
14
|
+
} from './config.js';
|
|
15
|
+
|
|
16
|
+
interface BootstrapPayload {
|
|
17
|
+
apiToken: string;
|
|
18
|
+
walletPrivateKey?: string;
|
|
19
|
+
llm?: {
|
|
20
|
+
provider?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expandHomeDir(path: string): string {
|
|
27
|
+
if (!path.startsWith('~/')) return path;
|
|
28
|
+
return resolve(homedir(), path.slice(2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function prompt(question: string): Promise<string> {
|
|
32
|
+
const rl = createInterface({ input, output });
|
|
33
|
+
try {
|
|
34
|
+
return (await rl.question(question)).trim();
|
|
35
|
+
} finally {
|
|
36
|
+
rl.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function promptSecret(question: string): Promise<string> {
|
|
41
|
+
const rl = createInterface({ input, output, terminal: true }) as ReturnType<typeof createInterface> & {
|
|
42
|
+
stdoutMuted?: boolean;
|
|
43
|
+
_writeToOutput?: (text: string) => void;
|
|
44
|
+
};
|
|
45
|
+
rl.stdoutMuted = true;
|
|
46
|
+
const originalWrite = rl._writeToOutput?.bind(rl);
|
|
47
|
+
rl._writeToOutput = (text: string) => {
|
|
48
|
+
if (!rl.stdoutMuted) {
|
|
49
|
+
originalWrite?.(text);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const answer = (await rl.question(question)).trim();
|
|
55
|
+
output.write('\n');
|
|
56
|
+
return answer;
|
|
57
|
+
} finally {
|
|
58
|
+
rl.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function promptSecretPassword(question: string = 'Device password: '): Promise<string> {
|
|
63
|
+
return promptSecret(question);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function promptPasswordWithConfirmation(): Promise<string> {
|
|
67
|
+
output.write('\n');
|
|
68
|
+
output.write('Important: this password encrypts the local wallet key, relay token, and agent LLM key for this device.\n');
|
|
69
|
+
output.write('If you lose this password and the agent wallet holds funds, those funds cannot be recovered.\n\n');
|
|
70
|
+
|
|
71
|
+
const ack = await prompt('Type "I UNDERSTAND" to continue: ');
|
|
72
|
+
if (ack !== 'I UNDERSTAND') {
|
|
73
|
+
throw new Error('Secure setup aborted');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
const password = await promptSecret('Create a device password (min 12 chars): ');
|
|
78
|
+
if (password.length < 12) {
|
|
79
|
+
output.write('Password must be at least 12 characters.\n');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const confirm = await promptSecret('Confirm device password: ');
|
|
84
|
+
if (password !== confirm) {
|
|
85
|
+
output.write('Passwords did not match. Try again.\n');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return password;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function promptWalletPrivateKey(): Promise<string> {
|
|
94
|
+
while (true) {
|
|
95
|
+
const choice = (await prompt('Wallet setup — [1] generate new wallet locally, [2] use existing private key: ')).trim();
|
|
96
|
+
if (choice === '1') {
|
|
97
|
+
const privateKey = generatePrivateKey();
|
|
98
|
+
const address = privateKeyToAccount(privateKey).address;
|
|
99
|
+
output.write(`Generated wallet address: ${address}\n`);
|
|
100
|
+
return privateKey;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (choice === '2') {
|
|
104
|
+
const privateKey = await promptSecret('Wallet private key (0x...): ');
|
|
105
|
+
if (/^0x[a-fA-F0-9]{64}$/.test(privateKey)) {
|
|
106
|
+
return privateKey;
|
|
107
|
+
}
|
|
108
|
+
output.write('Invalid private key. Expected a 32-byte hex string prefixed with 0x.\n');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
output.write('Enter 1 or 2.\n');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function promptLlmProvider(): Promise<string> {
|
|
117
|
+
while (true) {
|
|
118
|
+
const provider = (await prompt('Agent LLM provider (openai/anthropic/google/deepseek/mistral/groq/together/ollama): ')).toLowerCase();
|
|
119
|
+
if (['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama'].includes(provider)) {
|
|
120
|
+
return provider;
|
|
121
|
+
}
|
|
122
|
+
output.write('Unsupported provider.\n');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function consumeBootstrapPackage(config: RuntimeConfigFile): Promise<BootstrapPayload> {
|
|
127
|
+
if (!config.secrets?.bootstrapToken) {
|
|
128
|
+
return { apiToken: '' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const res = await fetch(`${config.apiUrl}/v1/agents/bootstrap/consume`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
agentId: config.agentId,
|
|
136
|
+
token: config.secrets.bootstrapToken,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const body = await res.text();
|
|
142
|
+
throw new Error(`Failed to consume secure setup package: ${body}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const data = await res.json() as { payload: BootstrapPayload };
|
|
146
|
+
return data.payload;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function buildLocalSecrets(config: RuntimeConfigFile, bootstrapPayload: BootstrapPayload): Promise<{ config: RuntimeConfigFile; secrets: LocalSecretPayload }> {
|
|
150
|
+
const nextConfig = structuredClone(config);
|
|
151
|
+
const llm = { ...(nextConfig.llm || {}) };
|
|
152
|
+
|
|
153
|
+
if (bootstrapPayload.llm?.provider && !llm.provider) {
|
|
154
|
+
llm.provider = bootstrapPayload.llm.provider as RuntimeConfigFile['llm']['provider'];
|
|
155
|
+
}
|
|
156
|
+
if (bootstrapPayload.llm?.model && !llm.model) {
|
|
157
|
+
llm.model = bootstrapPayload.llm.model;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!llm.provider) {
|
|
161
|
+
llm.provider = await promptLlmProvider() as RuntimeConfigFile['llm']['provider'];
|
|
162
|
+
}
|
|
163
|
+
if (!llm.model) {
|
|
164
|
+
llm.model = await prompt('Agent LLM model: ');
|
|
165
|
+
if (!llm.model) {
|
|
166
|
+
throw new Error('Agent LLM model is required');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const secrets: LocalSecretPayload = {
|
|
171
|
+
apiToken: bootstrapPayload.apiToken || nextConfig.apiToken || await promptSecret('Agent relay token: '),
|
|
172
|
+
walletPrivateKey: bootstrapPayload.walletPrivateKey || nextConfig.wallet?.privateKey || await promptWalletPrivateKey(),
|
|
173
|
+
llmApiKey: bootstrapPayload.llm?.apiKey || nextConfig.llm.apiKey || await promptSecret('Agent LLM API key: '),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!secrets.apiToken) {
|
|
177
|
+
throw new Error('Agent relay token is required');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
nextConfig.llm = llm;
|
|
181
|
+
delete nextConfig.apiToken;
|
|
182
|
+
delete nextConfig.wallet;
|
|
183
|
+
delete nextConfig.llm.apiKey;
|
|
184
|
+
|
|
185
|
+
return { config: nextConfig, secrets };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function writeSecureStore(path: string, secrets: LocalSecretPayload, password: string): string {
|
|
189
|
+
const secureStorePath = expandHomeDir(path);
|
|
190
|
+
const encrypted = encryptSecretPayload(secrets, password);
|
|
191
|
+
const dir = dirname(secureStorePath);
|
|
192
|
+
if (!existsSync(dir)) {
|
|
193
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
|
|
196
|
+
try {
|
|
197
|
+
chmodSync(secureStorePath, 0o600);
|
|
198
|
+
} catch {
|
|
199
|
+
// Best effort only — some platforms ignore chmod semantics.
|
|
200
|
+
}
|
|
201
|
+
return secureStorePath;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function ensureLocalSetup(configPath: string): Promise<void> {
|
|
205
|
+
const config = readConfigFile(configPath);
|
|
206
|
+
const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
|
|
207
|
+
if (
|
|
208
|
+
existingSecureStorePath &&
|
|
209
|
+
!config.secrets?.bootstrapToken &&
|
|
210
|
+
existsSync(existingSecureStorePath) &&
|
|
211
|
+
!config.apiToken &&
|
|
212
|
+
!config.wallet?.privateKey &&
|
|
213
|
+
!config.llm.apiKey
|
|
214
|
+
) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const bootstrapPayload = await consumeBootstrapPackage(config);
|
|
219
|
+
const { config: nextConfig, secrets } = await buildLocalSecrets(config, bootstrapPayload);
|
|
220
|
+
const password = await promptPasswordWithConfirmation();
|
|
221
|
+
const secureStorePath = writeSecureStore(
|
|
222
|
+
nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
|
|
223
|
+
secrets,
|
|
224
|
+
password,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
nextConfig.secrets = {
|
|
228
|
+
secureStorePath,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
writeConfigFile(configPath, nextConfig);
|
|
232
|
+
output.write(`Encrypted local secret store created at ${secureStorePath}\n`);
|
|
233
|
+
}
|
package/src/strategy/loader.ts
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import type { StrategyFunction, StrategyContext, TradeSignal } from '@exagent/sdk';
|
|
4
5
|
import { getTemplate } from './templates.js';
|
|
5
6
|
|
|
7
|
+
const promptSignalSchema = z.object({
|
|
8
|
+
symbol: z.string().min(1),
|
|
9
|
+
side: z.enum(['buy', 'sell', 'long', 'short']),
|
|
10
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
11
|
+
reasoning: z.string().optional(),
|
|
12
|
+
venue: z.string().optional(),
|
|
13
|
+
chain: z.string().optional(),
|
|
14
|
+
size: z.number().positive().optional(),
|
|
15
|
+
price: z.number().positive().optional(),
|
|
16
|
+
fee: z.number().min(0).optional(),
|
|
17
|
+
venueFillId: z.string().optional(),
|
|
18
|
+
venueTimestamp: z.string().optional(),
|
|
19
|
+
leverage: z.number().positive().optional(),
|
|
20
|
+
orderType: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const promptSignalArraySchema = z.array(promptSignalSchema);
|
|
24
|
+
|
|
6
25
|
export async function loadStrategy(config: {
|
|
7
26
|
file?: string;
|
|
8
27
|
template?: string;
|
|
28
|
+
prompt?: {
|
|
29
|
+
name?: string;
|
|
30
|
+
systemPrompt: string;
|
|
31
|
+
venues?: string[];
|
|
32
|
+
};
|
|
9
33
|
}): Promise<StrategyFunction> {
|
|
10
34
|
if (config.file) {
|
|
11
35
|
return loadFromFile(config.file);
|
|
12
36
|
}
|
|
13
37
|
|
|
38
|
+
if (config.prompt) {
|
|
39
|
+
return loadFromPrompt(config.prompt);
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
if (config.template) {
|
|
15
43
|
const template = getTemplate(config.template);
|
|
16
44
|
if (!template) {
|
|
@@ -51,6 +79,76 @@ async function loadFromCode(code: string): Promise<StrategyFunction> {
|
|
|
51
79
|
return fn;
|
|
52
80
|
}
|
|
53
81
|
|
|
82
|
+
function loadFromPrompt(config: {
|
|
83
|
+
name?: string;
|
|
84
|
+
systemPrompt: string;
|
|
85
|
+
venues?: string[];
|
|
86
|
+
}): StrategyFunction {
|
|
87
|
+
return async (context: StrategyContext): Promise<TradeSignal[]> => {
|
|
88
|
+
const prices = context.market.getPrices();
|
|
89
|
+
const positions = context.position.openPositions.map((position) => ({
|
|
90
|
+
token: position.token,
|
|
91
|
+
quantity: position.quantity,
|
|
92
|
+
costBasisPerUnit: position.costBasisPerUnit,
|
|
93
|
+
venue: position.venue,
|
|
94
|
+
chain: position.chain,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
const response = await context.llm.chat([
|
|
98
|
+
{ role: 'system', content: config.systemPrompt },
|
|
99
|
+
{
|
|
100
|
+
role: 'user',
|
|
101
|
+
content: [
|
|
102
|
+
`Strategy: ${config.name || 'Prompt Strategy'}`,
|
|
103
|
+
`Allowed venues: ${(config.venues || []).join(', ') || 'any'}`,
|
|
104
|
+
`Current prices: ${JSON.stringify(prices)}`,
|
|
105
|
+
`Open positions: ${JSON.stringify(positions)}`,
|
|
106
|
+
`Risk config: ${JSON.stringify(context.config)}`,
|
|
107
|
+
'Return ONLY a JSON array of trade signals.',
|
|
108
|
+
].join('\n'),
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const match = response.content.match(/\[[\s\S]*\]/);
|
|
113
|
+
if (!match) {
|
|
114
|
+
context.log('Prompt strategy returned a non-JSON response; no signals emitted.');
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const parsed = promptSignalArraySchema.parse(JSON.parse(match[0]));
|
|
120
|
+
const signals: TradeSignal[] = [];
|
|
121
|
+
for (const signal of parsed) {
|
|
122
|
+
const price = signal.price ?? prices[signal.symbol.toUpperCase()];
|
|
123
|
+
if (!price || price <= 0) {
|
|
124
|
+
context.log(`Prompt strategy skipped ${signal.symbol}: no usable price in response or market cache.`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
signals.push({
|
|
129
|
+
symbol: signal.symbol,
|
|
130
|
+
side: signal.side,
|
|
131
|
+
confidence: signal.confidence ?? 0.5,
|
|
132
|
+
reasoning: signal.reasoning,
|
|
133
|
+
venue: signal.venue || config.venues?.[0] || 'manual',
|
|
134
|
+
chain: signal.chain,
|
|
135
|
+
size: signal.size ?? 1,
|
|
136
|
+
price,
|
|
137
|
+
fee: signal.fee ?? 0,
|
|
138
|
+
venueFillId: signal.venueFillId ?? '',
|
|
139
|
+
venueTimestamp: signal.venueTimestamp ?? new Date().toISOString(),
|
|
140
|
+
leverage: signal.leverage,
|
|
141
|
+
orderType: signal.orderType,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return signals;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
context.log(`Prompt strategy parse failed: ${(err as Error).message}`);
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
54
152
|
const holdStrategy: StrategyFunction = async (_context: StrategyContext): Promise<TradeSignal[]> => {
|
|
55
153
|
return []; // No trades — hold position
|
|
56
154
|
};
|