@exagent/agent 0.2.1 → 0.3.1
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 +6 -6
- package/dist/chunk-4UVMO6ZM.js +6318 -0
- package/dist/chunk-5NU6FDDE.js +6020 -0
- package/dist/chunk-GPMXUMYH.js +5991 -0
- package/dist/chunk-IJK4EFTJ.js +6043 -0
- package/dist/chunk-J3NG7AGT.js +6047 -0
- package/dist/chunk-QG22GADV.js +6316 -0
- package/dist/chunk-SVFTC5V2.js +6021 -0
- package/dist/chunk-TDACLKD7.js +5867 -0
- package/dist/chunk-UAP5CTHB.js +5985 -0
- package/dist/chunk-VDK4XPAC.js +6318 -0
- package/dist/cli.js +337 -15
- package/dist/index.d.ts +29 -1
- package/dist/index.js +1 -1
- package/package.json +15 -8
- package/src/cli.ts +54 -15
- package/src/config.ts +231 -25
- package/src/runtime.ts +370 -37
- package/src/scrub-secrets.ts +39 -0
- package/src/setup.ts +332 -0
- package/src/strategy/loader.ts +132 -1
- package/src/ui.ts +75 -0
package/src/setup.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
import * as clack from '@clack/prompts';
|
|
6
|
+
import {
|
|
7
|
+
encryptSecretPayload,
|
|
8
|
+
getDefaultSecureStorePath,
|
|
9
|
+
readConfigFile,
|
|
10
|
+
type LocalSecretPayload,
|
|
11
|
+
type RuntimeConfigFile,
|
|
12
|
+
writeConfigFile,
|
|
13
|
+
} from './config.js';
|
|
14
|
+
import { printBanner, printStep, printDone, printInfo, printError, printSuccess, pc } from './ui.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
|
+
function cancelled(): never {
|
|
32
|
+
clack.cancel('Setup cancelled.');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Step 1: Bootstrap
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
async function consumeBootstrapPackage(config: RuntimeConfigFile): Promise<BootstrapPayload> {
|
|
41
|
+
if (!config.secrets?.bootstrapToken) {
|
|
42
|
+
return { apiToken: '' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const apiHost = new URL(config.apiUrl).host;
|
|
46
|
+
printInfo(`Connecting to ${pc.cyan(apiHost)}...`);
|
|
47
|
+
|
|
48
|
+
const res = await fetch(`${config.apiUrl}/v1/agents/bootstrap/consume`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
agentId: config.agentId,
|
|
53
|
+
token: config.secrets.bootstrapToken,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const body = await res.text();
|
|
59
|
+
throw new Error(`Failed to consume bootstrap package: ${body}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await res.json() as { payload: BootstrapPayload };
|
|
63
|
+
return data.payload;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Step 2: Wallet
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
async function setupWallet(config: RuntimeConfigFile): Promise<string> {
|
|
71
|
+
if (config.wallet?.privateKey) {
|
|
72
|
+
const account = privateKeyToAccount(config.wallet.privateKey as `0x${string}`);
|
|
73
|
+
printDone(`Using existing wallet: ${pc.dim(account.address)}`);
|
|
74
|
+
return config.wallet.privateKey;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const method = await clack.select({
|
|
78
|
+
message: 'How would you like to set up your wallet?',
|
|
79
|
+
options: [
|
|
80
|
+
{ value: 'generate', label: 'Generate new wallet locally', hint: 'recommended' },
|
|
81
|
+
{ value: 'import', label: 'Import existing private key' },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
if (clack.isCancel(method)) cancelled();
|
|
85
|
+
|
|
86
|
+
if (method === 'generate') {
|
|
87
|
+
const privateKey = generatePrivateKey();
|
|
88
|
+
const address = privateKeyToAccount(privateKey).address;
|
|
89
|
+
printDone(`Wallet created: ${pc.dim(address)}`);
|
|
90
|
+
return privateKey;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Import flow
|
|
94
|
+
const privateKey = await clack.password({
|
|
95
|
+
message: 'Wallet private key (0x...):',
|
|
96
|
+
validate: (val) => {
|
|
97
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(val)) {
|
|
98
|
+
return 'Invalid private key. Expected a 32-byte hex string prefixed with 0x.';
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (clack.isCancel(privateKey)) cancelled();
|
|
103
|
+
|
|
104
|
+
const address = privateKeyToAccount(privateKey as `0x${string}`).address;
|
|
105
|
+
printDone(`Wallet imported: ${pc.dim(address)}`);
|
|
106
|
+
return privateKey;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Step 3: LLM
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
const LLM_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama'] as const;
|
|
114
|
+
|
|
115
|
+
async function setupLlm(
|
|
116
|
+
config: RuntimeConfigFile,
|
|
117
|
+
bootstrapPayload: BootstrapPayload,
|
|
118
|
+
): Promise<{ provider: string; model: string; apiKey: string }> {
|
|
119
|
+
// Provider
|
|
120
|
+
let provider = config.llm?.provider || bootstrapPayload.llm?.provider;
|
|
121
|
+
if (provider) {
|
|
122
|
+
printInfo(`Provider: ${pc.cyan(provider)} ${pc.dim('(from dashboard)')}`);
|
|
123
|
+
} else {
|
|
124
|
+
const selected = await clack.select({
|
|
125
|
+
message: 'LLM provider:',
|
|
126
|
+
options: LLM_PROVIDERS.map(p => ({ value: p, label: p })),
|
|
127
|
+
});
|
|
128
|
+
if (clack.isCancel(selected)) cancelled();
|
|
129
|
+
provider = selected;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Model
|
|
133
|
+
let model = config.llm?.model || bootstrapPayload.llm?.model;
|
|
134
|
+
if (model) {
|
|
135
|
+
printInfo(`Model: ${pc.cyan(model)} ${pc.dim('(from dashboard)')}`);
|
|
136
|
+
} else {
|
|
137
|
+
const entered = await clack.text({
|
|
138
|
+
message: 'LLM model:',
|
|
139
|
+
placeholder: 'gpt-4o',
|
|
140
|
+
validate: (val) => {
|
|
141
|
+
if (!val.trim()) return 'Model name is required.';
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
if (clack.isCancel(entered)) cancelled();
|
|
145
|
+
model = entered;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// API Key
|
|
149
|
+
let apiKey: string | undefined;
|
|
150
|
+
if (bootstrapPayload.llm?.apiKey) {
|
|
151
|
+
const useBootstrap = await clack.confirm({
|
|
152
|
+
message: 'LLM API key received from dashboard. Use it?',
|
|
153
|
+
initialValue: true,
|
|
154
|
+
});
|
|
155
|
+
if (clack.isCancel(useBootstrap)) cancelled();
|
|
156
|
+
if (useBootstrap) {
|
|
157
|
+
apiKey = bootstrapPayload.llm.apiKey;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!apiKey) {
|
|
161
|
+
apiKey = config.llm?.apiKey;
|
|
162
|
+
}
|
|
163
|
+
if (!apiKey) {
|
|
164
|
+
const entered = await clack.password({
|
|
165
|
+
message: 'LLM API key:',
|
|
166
|
+
validate: (val) => {
|
|
167
|
+
if (!val.trim()) return 'API key is required.';
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
if (clack.isCancel(entered)) cancelled();
|
|
171
|
+
apiKey = entered;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
printDone('LLM configured');
|
|
175
|
+
return { provider, model, apiKey };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Step 4: Encryption
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
async function setupEncryption(): Promise<string> {
|
|
183
|
+
printInfo(`Secrets encrypted with ${pc.cyan('AES-256-GCM')} (${pc.cyan('scrypt')} KDF)`);
|
|
184
|
+
printInfo('The password never leaves this machine.');
|
|
185
|
+
console.log();
|
|
186
|
+
|
|
187
|
+
const password = await clack.password({
|
|
188
|
+
message: 'Choose a device password (12+ characters):',
|
|
189
|
+
validate: (val) => {
|
|
190
|
+
if (val.length < 12) return 'Password must be at least 12 characters.';
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
if (clack.isCancel(password)) cancelled();
|
|
194
|
+
|
|
195
|
+
const confirm = await clack.password({
|
|
196
|
+
message: 'Confirm password:',
|
|
197
|
+
validate: (val) => {
|
|
198
|
+
if (val !== password) return 'Passwords do not match.';
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
if (clack.isCancel(confirm)) cancelled();
|
|
202
|
+
|
|
203
|
+
return password;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Secure store
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function writeSecureStore(path: string, secrets: LocalSecretPayload, password: string): string {
|
|
211
|
+
const secureStorePath = expandHomeDir(path);
|
|
212
|
+
const encrypted = encryptSecretPayload(secrets, password);
|
|
213
|
+
const dir = dirname(secureStorePath);
|
|
214
|
+
if (!existsSync(dir)) {
|
|
215
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
216
|
+
}
|
|
217
|
+
writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
|
|
218
|
+
try {
|
|
219
|
+
chmodSync(secureStorePath, 0o600);
|
|
220
|
+
} catch {
|
|
221
|
+
// Best effort — some platforms ignore chmod semantics.
|
|
222
|
+
}
|
|
223
|
+
return secureStorePath;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Public: password prompt (used by run/status commands)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
export async function promptSecretPassword(question: string = 'Device password:'): Promise<string> {
|
|
231
|
+
const password = await clack.password({ message: question });
|
|
232
|
+
if (clack.isCancel(password)) cancelled();
|
|
233
|
+
return password;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Main setup orchestrator
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
export async function ensureLocalSetup(configPath: string): Promise<void> {
|
|
241
|
+
const config = readConfigFile(configPath);
|
|
242
|
+
const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
|
|
243
|
+
if (
|
|
244
|
+
existingSecureStorePath &&
|
|
245
|
+
!config.secrets?.bootstrapToken &&
|
|
246
|
+
existsSync(existingSecureStorePath) &&
|
|
247
|
+
!config.apiToken &&
|
|
248
|
+
!config.wallet?.privateKey &&
|
|
249
|
+
!config.llm.apiKey
|
|
250
|
+
) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
printBanner();
|
|
255
|
+
|
|
256
|
+
clack.intro(pc.bold('Agent Setup'));
|
|
257
|
+
|
|
258
|
+
// Step 1: Bootstrap
|
|
259
|
+
printStep(1, 4, 'Bootstrap package');
|
|
260
|
+
const bootstrapPayload = await consumeBootstrapPackage(config);
|
|
261
|
+
if (config.secrets?.bootstrapToken) {
|
|
262
|
+
printDone('Bootstrap package consumed');
|
|
263
|
+
if (bootstrapPayload.llm?.provider) {
|
|
264
|
+
printInfo(`LLM config received: ${pc.cyan(bootstrapPayload.llm.provider)}${bootstrapPayload.llm.model ? ` / ${pc.cyan(bootstrapPayload.llm.model)}` : ''}`);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
printInfo('No bootstrap token — manual configuration');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Step 2: Wallet
|
|
271
|
+
printStep(2, 4, 'Wallet setup');
|
|
272
|
+
const walletPrivateKey = await setupWallet(config);
|
|
273
|
+
|
|
274
|
+
// Step 3: LLM
|
|
275
|
+
printStep(3, 4, 'LLM configuration');
|
|
276
|
+
const llm = await setupLlm(config, bootstrapPayload);
|
|
277
|
+
|
|
278
|
+
// Step 4: Encryption
|
|
279
|
+
printStep(4, 4, 'Device encryption');
|
|
280
|
+
const password = await setupEncryption();
|
|
281
|
+
|
|
282
|
+
// Build secrets and write
|
|
283
|
+
const secrets: LocalSecretPayload = {
|
|
284
|
+
apiToken: bootstrapPayload.apiToken || config.apiToken || '',
|
|
285
|
+
walletPrivateKey,
|
|
286
|
+
llmApiKey: llm.apiKey,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
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;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const nextConfig = structuredClone(config);
|
|
302
|
+
nextConfig.llm = {
|
|
303
|
+
...nextConfig.llm,
|
|
304
|
+
provider: llm.provider as RuntimeConfigFile['llm']['provider'],
|
|
305
|
+
model: llm.model,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Strip plaintext secrets from config file
|
|
309
|
+
delete nextConfig.apiToken;
|
|
310
|
+
delete nextConfig.wallet;
|
|
311
|
+
delete nextConfig.llm.apiKey;
|
|
312
|
+
|
|
313
|
+
const secureStorePath = writeSecureStore(
|
|
314
|
+
nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
|
|
315
|
+
secrets,
|
|
316
|
+
password,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
nextConfig.secrets = { secureStorePath };
|
|
320
|
+
writeConfigFile(configPath, nextConfig);
|
|
321
|
+
|
|
322
|
+
printDone(`Encrypted store: ${pc.dim(secureStorePath)}`);
|
|
323
|
+
|
|
324
|
+
clack.outro(pc.green('Setup complete'));
|
|
325
|
+
|
|
326
|
+
printSuccess('Ready', [
|
|
327
|
+
`${pc.cyan('npx exagent run')} Start trading`,
|
|
328
|
+
`${pc.cyan('npx exagent status')} Check connection`,
|
|
329
|
+
'',
|
|
330
|
+
`${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
|
|
331
|
+
]);
|
|
332
|
+
}
|
package/src/strategy/loader.ts
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
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';
|
|
6
|
+
import { scrubSecrets } from '../scrub-secrets.js';
|
|
7
|
+
|
|
8
|
+
const promptSignalSchema = z.object({
|
|
9
|
+
symbol: z.string().min(1),
|
|
10
|
+
side: z.enum(['buy', 'sell', 'long', 'short']),
|
|
11
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
12
|
+
reasoning: z.string().optional(),
|
|
13
|
+
venue: z.string().optional(),
|
|
14
|
+
chain: z.string().optional(),
|
|
15
|
+
size: z.number().positive().optional(),
|
|
16
|
+
price: z.number().positive().optional(),
|
|
17
|
+
fee: z.number().min(0).optional(),
|
|
18
|
+
venueFillId: z.string().optional(),
|
|
19
|
+
venueTimestamp: z.string().optional(),
|
|
20
|
+
leverage: z.number().positive().optional(),
|
|
21
|
+
orderType: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const promptSignalArraySchema = z.array(promptSignalSchema);
|
|
5
25
|
|
|
6
26
|
export async function loadStrategy(config: {
|
|
7
27
|
file?: string;
|
|
28
|
+
code?: string;
|
|
8
29
|
template?: string;
|
|
30
|
+
prompt?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
systemPrompt: string;
|
|
33
|
+
venues?: string[];
|
|
34
|
+
};
|
|
9
35
|
}): Promise<StrategyFunction> {
|
|
10
36
|
if (config.file) {
|
|
11
37
|
return loadFromFile(config.file);
|
|
12
38
|
}
|
|
13
39
|
|
|
40
|
+
if (config.code) {
|
|
41
|
+
return loadFromCode(config.code);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (config.prompt) {
|
|
45
|
+
return loadFromPrompt(config.prompt);
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
if (config.template) {
|
|
15
49
|
const template = getTemplate(config.template);
|
|
16
50
|
if (!template) {
|
|
@@ -34,7 +68,31 @@ async function loadFromFile(filePath: string): Promise<StrategyFunction> {
|
|
|
34
68
|
const fn = mod.default || mod.strategy;
|
|
35
69
|
|
|
36
70
|
if (typeof fn !== 'function') {
|
|
37
|
-
|
|
71
|
+
if (typeof mod.code === 'string' && mod.code.trim()) {
|
|
72
|
+
return loadFromCode(mod.code);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof mod.systemPrompt === 'string' && mod.systemPrompt.trim()) {
|
|
76
|
+
const venues = Array.isArray(mod.venues)
|
|
77
|
+
? mod.venues.filter((venue: unknown): venue is string => typeof venue === 'string')
|
|
78
|
+
: undefined;
|
|
79
|
+
const name = typeof mod.name === 'string' ? mod.name : undefined;
|
|
80
|
+
return loadFromPrompt({
|
|
81
|
+
name,
|
|
82
|
+
systemPrompt: mod.systemPrompt,
|
|
83
|
+
venues,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof mod.template === 'string' && mod.template.trim()) {
|
|
88
|
+
const template = getTemplate(mod.template);
|
|
89
|
+
if (!template) {
|
|
90
|
+
throw new Error(`Unknown strategy template: ${mod.template}. Available: momentum, value, arbitrage, hold`);
|
|
91
|
+
}
|
|
92
|
+
return loadFromCode(template.code);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error(`Strategy file must export a default function, 'strategy' function, 'code' string, 'systemPrompt' string, or 'template' string`);
|
|
38
96
|
}
|
|
39
97
|
|
|
40
98
|
return fn as StrategyFunction;
|
|
@@ -51,6 +109,79 @@ async function loadFromCode(code: string): Promise<StrategyFunction> {
|
|
|
51
109
|
return fn;
|
|
52
110
|
}
|
|
53
111
|
|
|
112
|
+
function loadFromPrompt(config: {
|
|
113
|
+
name?: string;
|
|
114
|
+
systemPrompt: string;
|
|
115
|
+
venues?: string[];
|
|
116
|
+
}): StrategyFunction {
|
|
117
|
+
return async (context: StrategyContext): Promise<TradeSignal[]> => {
|
|
118
|
+
const prices = context.market.getPrices();
|
|
119
|
+
const positions = context.position.openPositions.map((position) => ({
|
|
120
|
+
token: position.token,
|
|
121
|
+
quantity: position.quantity,
|
|
122
|
+
costBasisPerUnit: position.costBasisPerUnit,
|
|
123
|
+
venue: position.venue,
|
|
124
|
+
chain: position.chain,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const response = await context.llm.chat([
|
|
128
|
+
{ role: 'system', content: config.systemPrompt },
|
|
129
|
+
{
|
|
130
|
+
role: 'user',
|
|
131
|
+
content: [
|
|
132
|
+
`Strategy: ${config.name || 'Prompt Strategy'}`,
|
|
133
|
+
`Allowed venues: ${(config.venues || []).join(', ') || 'any'}`,
|
|
134
|
+
`Current prices: ${JSON.stringify(prices)}`,
|
|
135
|
+
`Open positions: ${JSON.stringify(positions)}`,
|
|
136
|
+
`Risk config: ${JSON.stringify(context.config)}`,
|
|
137
|
+
'Return ONLY a JSON array of trade signals.',
|
|
138
|
+
].join('\n'),
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// Defense-in-depth: scrub any secrets the LLM might echo back before parsing
|
|
143
|
+
const scrubbedContent = scrubSecrets(response.content);
|
|
144
|
+
|
|
145
|
+
const match = scrubbedContent.match(/\[[\s\S]*\]/);
|
|
146
|
+
if (!match) {
|
|
147
|
+
context.log('Prompt strategy returned a non-JSON response; no signals emitted.');
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const parsed = promptSignalArraySchema.parse(JSON.parse(match[0]));
|
|
153
|
+
const signals: TradeSignal[] = [];
|
|
154
|
+
for (const signal of parsed) {
|
|
155
|
+
const price = signal.price ?? prices[signal.symbol.toUpperCase()];
|
|
156
|
+
if (!price || price <= 0) {
|
|
157
|
+
context.log(`Prompt strategy skipped ${signal.symbol}: no usable price in response or market cache.`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
signals.push({
|
|
162
|
+
symbol: signal.symbol,
|
|
163
|
+
side: signal.side,
|
|
164
|
+
confidence: signal.confidence ?? 0.5,
|
|
165
|
+
reasoning: signal.reasoning,
|
|
166
|
+
venue: signal.venue || config.venues?.[0] || 'manual',
|
|
167
|
+
chain: signal.chain,
|
|
168
|
+
size: signal.size ?? 1,
|
|
169
|
+
price,
|
|
170
|
+
fee: signal.fee ?? 0,
|
|
171
|
+
venueFillId: signal.venueFillId ?? '',
|
|
172
|
+
venueTimestamp: signal.venueTimestamp ?? new Date().toISOString(),
|
|
173
|
+
leverage: signal.leverage,
|
|
174
|
+
orderType: signal.orderType,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return signals;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
context.log(`Prompt strategy parse failed: ${(err as Error).message}`);
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
54
185
|
const holdStrategy: StrategyFunction = async (_context: StrategyContext): Promise<TradeSignal[]> => {
|
|
55
186
|
return []; // No trades — hold position
|
|
56
187
|
};
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import boxen from 'boxen';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
// Brand gradient: Blue → Indigo → Violet (from Exagent design system)
|
|
8
|
+
const brandGradient: (text: string) => string = gradient(['#3B82F6', '#6366F1', '#7C3AED']);
|
|
9
|
+
|
|
10
|
+
// Secondary gradient: Cyan → Blue
|
|
11
|
+
const accentGradient: (text: string) => string = gradient(['#22D3EE', '#3B82F6']);
|
|
12
|
+
|
|
13
|
+
function getVersion(): string {
|
|
14
|
+
try {
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const pkg = require('../package.json');
|
|
17
|
+
return pkg.version || '0.0.0';
|
|
18
|
+
} catch {
|
|
19
|
+
return '0.0.0';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function printBanner(): void {
|
|
24
|
+
const art = figlet.textSync('EXAGENT', {
|
|
25
|
+
font: 'Small',
|
|
26
|
+
horizontalLayout: 'default',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(brandGradient(art));
|
|
31
|
+
console.log(pc.dim(` v${getVersion()}`));
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function printSuccess(title: string, lines: string[]): void {
|
|
36
|
+
const body = [
|
|
37
|
+
'',
|
|
38
|
+
pc.bold(pc.white(title)),
|
|
39
|
+
'',
|
|
40
|
+
...lines.map(l => ` ${l}`),
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(boxen(body, {
|
|
46
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
47
|
+
borderColor: '#3B82F6',
|
|
48
|
+
borderStyle: 'round',
|
|
49
|
+
dimBorder: false,
|
|
50
|
+
}));
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function printStep(step: number, total: number, label: string): void {
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(accentGradient(` Step ${step} of ${total}`) + pc.dim(` — ${label}`));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function printDone(message: string): void {
|
|
60
|
+
console.log(` ${pc.green('✓')} ${message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function printInfo(message: string): void {
|
|
64
|
+
console.log(` ${pc.dim('│')} ${message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function printWarn(message: string): void {
|
|
68
|
+
console.log(` ${pc.yellow('!')} ${message}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function printError(message: string): void {
|
|
72
|
+
console.log(` ${pc.red('✗')} ${message}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { pc, brandGradient, accentGradient };
|