@exagent/agent 0.2.0 → 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 +1 -1
- 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/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
|
};
|