@exagent/agent 0.3.6 → 0.3.8
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/chunk-7UGLJO6W.js +6392 -0
- package/dist/chunk-EHAOPCTJ.js +6406 -0
- package/dist/chunk-FGMXTW5I.js +6540 -0
- package/dist/chunk-GYYW4EKM.js +6756 -0
- package/dist/chunk-IVA2SCSN.js +6756 -0
- package/dist/chunk-JHXCSGPC.js +6352 -0
- package/dist/chunk-V6O4UXVN.js +6345 -0
- package/dist/chunk-WTECTX2Z.js +6345 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1 -1
- package/package.json +12 -9
- package/src/bridge/across.ts +0 -240
- package/src/bridge/bridge-manager.ts +0 -87
- package/src/bridge/index.ts +0 -9
- package/src/bridge/types.ts +0 -77
- package/src/chains.ts +0 -105
- package/src/cli.ts +0 -250
- package/src/config.ts +0 -502
- package/src/diagnostics.ts +0 -335
- package/src/index.ts +0 -98
- package/src/llm/anthropic.ts +0 -63
- package/src/llm/base.ts +0 -264
- package/src/llm/deepseek.ts +0 -48
- package/src/llm/google.ts +0 -63
- package/src/llm/groq.ts +0 -48
- package/src/llm/index.ts +0 -42
- package/src/llm/mistral.ts +0 -48
- package/src/llm/ollama.ts +0 -52
- package/src/llm/openai.ts +0 -94
- package/src/llm/together.ts +0 -48
- package/src/llm-providers.ts +0 -8
- package/src/logger.ts +0 -137
- package/src/paper/executor.ts +0 -201
- package/src/paper/index.ts +0 -1
- package/src/perp/client.ts +0 -200
- package/src/perp/index.ts +0 -12
- package/src/perp/msgpack.ts +0 -272
- package/src/perp/orders.ts +0 -234
- package/src/perp/positions.ts +0 -126
- package/src/perp/signer.ts +0 -277
- package/src/perp/types.ts +0 -192
- package/src/perp/websocket.ts +0 -274
- package/src/position-tracker.ts +0 -243
- package/src/prediction/client.ts +0 -288
- package/src/prediction/index.ts +0 -3
- package/src/prediction/order-manager.ts +0 -297
- package/src/prediction/types.ts +0 -151
- package/src/relay.ts +0 -254
- package/src/runtime.ts +0 -1755
- package/src/scrub-secrets.ts +0 -39
- package/src/setup.ts +0 -392
- package/src/signal.ts +0 -212
- package/src/spot/aerodrome.ts +0 -158
- package/src/spot/client.ts +0 -138
- package/src/spot/index.ts +0 -11
- package/src/spot/swap-manager.ts +0 -219
- package/src/spot/types.ts +0 -203
- package/src/spot/uniswap.ts +0 -150
- package/src/store.ts +0 -50
- package/src/strategy/index.ts +0 -2
- package/src/strategy/loader.ts +0 -265
- package/src/strategy/templates.ts +0 -74
- package/src/trading/index.ts +0 -2
- package/src/trading/market.ts +0 -120
- package/src/trading/risk.ts +0 -107
- package/src/ui.ts +0 -75
- package/test/strategy-loader.test.ts +0 -150
- package/tsconfig.json +0 -8
package/src/scrub-secrets.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Secret scrubbing utility — defense-in-depth protection against LLM responses
|
|
3
|
-
* or log messages accidentally containing API keys, private keys, or tokens.
|
|
4
|
-
*
|
|
5
|
-
* Applied to:
|
|
6
|
-
* - LLM response content before parsing (strategy/loader.ts)
|
|
7
|
-
* - Strategy log output (runtime.ts context.log)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const SECRET_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
11
|
-
// OpenAI API keys
|
|
12
|
-
{ pattern: /sk-[a-zA-Z0-9]{20,}/g, label: '[REDACTED:openai-key]' },
|
|
13
|
-
// Anthropic API keys
|
|
14
|
-
{ pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g, label: '[REDACTED:anthropic-key]' },
|
|
15
|
-
// Google API keys
|
|
16
|
-
{ pattern: /AIza[a-zA-Z0-9_-]{35}/g, label: '[REDACTED:google-key]' },
|
|
17
|
-
// Private keys (64 hex chars after 0x)
|
|
18
|
-
{ pattern: /0x[a-fA-F0-9]{64}/g, label: '[REDACTED:private-key]' },
|
|
19
|
-
// Agent tokens
|
|
20
|
-
{ pattern: /exg_[a-fA-F0-9]{64}/g, label: '[REDACTED:agent-token]' },
|
|
21
|
-
// Bootstrap tokens
|
|
22
|
-
{ pattern: /exb_[a-fA-F0-9]{64}/g, label: '[REDACTED:bootstrap-token]' },
|
|
23
|
-
// Generic long bearer tokens (base64-ish, 40+ chars)
|
|
24
|
-
{ pattern: /Bearer\s+[A-Za-z0-9_-]{40,}/g, label: '[REDACTED:bearer-token]' },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Scrub known secret patterns from text. Returns the scrubbed string.
|
|
29
|
-
* Safe to call on any text — if no patterns match, returns the original unchanged.
|
|
30
|
-
*/
|
|
31
|
-
export function scrubSecrets(text: string): string {
|
|
32
|
-
let result = text;
|
|
33
|
-
for (const { pattern, label } of SECRET_PATTERNS) {
|
|
34
|
-
// Reset lastIndex for global regexes
|
|
35
|
-
pattern.lastIndex = 0;
|
|
36
|
-
result = result.replace(pattern, label);
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
package/src/setup.ts
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
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
|
-
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
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Step 1: Bootstrap
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
async function consumeBootstrapPackage(config: RuntimeConfigFile): Promise<BootstrapPayload> {
|
|
59
|
-
if (!config.secrets?.bootstrapToken) {
|
|
60
|
-
return { apiToken: '' };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const apiHost = new URL(config.apiUrl).host;
|
|
64
|
-
printInfo(`Connecting to ${pc.cyan(apiHost)}...`);
|
|
65
|
-
|
|
66
|
-
const res = await fetch(`${config.apiUrl}/v1/agents/bootstrap/consume`, {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: { 'Content-Type': 'application/json' },
|
|
69
|
-
body: JSON.stringify({
|
|
70
|
-
agentId: config.agentId,
|
|
71
|
-
token: config.secrets.bootstrapToken,
|
|
72
|
-
}),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
if (!res.ok) {
|
|
76
|
-
const body = await res.text();
|
|
77
|
-
throw new Error(`Failed to consume bootstrap package: ${body}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const data = await res.json() as { payload: BootstrapPayload };
|
|
81
|
-
return data.payload;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Step 2: Wallet
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
async function setupWallet(config: RuntimeConfigFile): Promise<string> {
|
|
89
|
-
if (config.wallet?.privateKey) {
|
|
90
|
-
const account = privateKeyToAccount(config.wallet.privateKey as `0x${string}`);
|
|
91
|
-
printDone(`Using existing wallet: ${pc.dim(account.address)}`);
|
|
92
|
-
return config.wallet.privateKey;
|
|
93
|
-
}
|
|
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
|
-
|
|
113
|
-
const method = await clack.select({
|
|
114
|
-
message: 'How would you like to set up your wallet?',
|
|
115
|
-
options: [
|
|
116
|
-
{ value: 'generate', label: 'Generate new wallet locally', hint: 'recommended' },
|
|
117
|
-
{ value: 'import', label: 'Import existing private key' },
|
|
118
|
-
],
|
|
119
|
-
});
|
|
120
|
-
if (clack.isCancel(method)) cancelled();
|
|
121
|
-
|
|
122
|
-
if (method === 'generate') {
|
|
123
|
-
const privateKey = generatePrivateKey();
|
|
124
|
-
const address = privateKeyToAccount(privateKey).address;
|
|
125
|
-
printDone(`Wallet created: ${pc.dim(address)}`);
|
|
126
|
-
return privateKey;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Import flow
|
|
130
|
-
const privateKey = await clack.password({
|
|
131
|
-
message: 'Wallet private key (0x...):',
|
|
132
|
-
validate: (val) => {
|
|
133
|
-
if (!/^0x[a-fA-F0-9]{64}$/.test(val)) {
|
|
134
|
-
return 'Invalid private key. Expected a 32-byte hex string prefixed with 0x.';
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
if (clack.isCancel(privateKey)) cancelled();
|
|
139
|
-
|
|
140
|
-
const address = privateKeyToAccount(privateKey as `0x${string}`).address;
|
|
141
|
-
printDone(`Wallet imported: ${pc.dim(address)}`);
|
|
142
|
-
return privateKey;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// Step 3: LLM
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
|
|
149
|
-
import { LLM_PROVIDERS, getDefaultModel, getProvider, providerRequiresApiKey } from './llm-providers.js';
|
|
150
|
-
|
|
151
|
-
async function setupLlm(
|
|
152
|
-
config: RuntimeConfigFile,
|
|
153
|
-
): Promise<{ provider: string; model: string; apiKey?: string }> {
|
|
154
|
-
// LLM config is always entered locally — never pulled from bootstrap.
|
|
155
|
-
// Config file may have provider/model as defaults from the deploy wizard.
|
|
156
|
-
|
|
157
|
-
// Non-interactive mode
|
|
158
|
-
if (isNonInteractive()) {
|
|
159
|
-
const provider = process.env.EXAGENT_LLM_PROVIDER || config.llm?.provider;
|
|
160
|
-
const model = process.env.EXAGENT_LLM_MODEL || config.llm?.model;
|
|
161
|
-
const apiKey = process.env.EXAGENT_LLM_KEY;
|
|
162
|
-
if (!provider) throw new Error('EXAGENT_LLM_PROVIDER required in non-interactive mode');
|
|
163
|
-
if (!model) throw new Error('EXAGENT_LLM_MODEL required in non-interactive mode');
|
|
164
|
-
if (providerRequiresApiKey(provider) && !apiKey) {
|
|
165
|
-
throw new Error('EXAGENT_LLM_KEY required in non-interactive mode');
|
|
166
|
-
}
|
|
167
|
-
printDone('LLM configured');
|
|
168
|
-
return { provider, model, apiKey };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Provider — use config as default selection if available
|
|
172
|
-
const defaultProvider = config.llm?.provider;
|
|
173
|
-
const providerOptions = LLM_PROVIDERS.map(p => ({ value: p.id, label: p.label }));
|
|
174
|
-
const selected = await clack.select({
|
|
175
|
-
message: 'LLM provider:',
|
|
176
|
-
options: providerOptions,
|
|
177
|
-
initialValue: defaultProvider || undefined,
|
|
178
|
-
});
|
|
179
|
-
if (clack.isCancel(selected)) cancelled();
|
|
180
|
-
const provider = selected;
|
|
181
|
-
|
|
182
|
-
// Model — show available models for the selected provider
|
|
183
|
-
const defaultModel = config.llm?.model;
|
|
184
|
-
const providerInfo = getProvider(provider);
|
|
185
|
-
const modelOptions = providerInfo
|
|
186
|
-
? providerInfo.models.map(m => ({ value: m.id, label: m.label }))
|
|
187
|
-
: [{ value: defaultModel || getDefaultModel('openai'), label: defaultModel || getDefaultModel('openai') }];
|
|
188
|
-
const selectedModel = await clack.select({
|
|
189
|
-
message: 'LLM model:',
|
|
190
|
-
options: modelOptions,
|
|
191
|
-
initialValue: defaultModel || undefined,
|
|
192
|
-
});
|
|
193
|
-
if (clack.isCancel(selectedModel)) cancelled();
|
|
194
|
-
const model = selectedModel;
|
|
195
|
-
|
|
196
|
-
let apiKey: string | undefined;
|
|
197
|
-
if (providerRequiresApiKey(provider)) {
|
|
198
|
-
// API Key — always prompt, never from bootstrap
|
|
199
|
-
const enteredApiKey = await clack.password({
|
|
200
|
-
message: 'LLM API key:',
|
|
201
|
-
validate: (val) => validateLlmKeyFormat(provider, val),
|
|
202
|
-
});
|
|
203
|
-
if (clack.isCancel(enteredApiKey)) cancelled();
|
|
204
|
-
apiKey = enteredApiKey;
|
|
205
|
-
} else {
|
|
206
|
-
printInfo('Ollama uses your local server; no API key needed.');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
printDone('LLM configured');
|
|
210
|
-
return { provider, model, apiKey };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
// Step 4: Encryption
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
|
|
217
|
-
async function setupEncryption(): Promise<string> {
|
|
218
|
-
// Non-interactive mode
|
|
219
|
-
if (isNonInteractive()) {
|
|
220
|
-
const password = process.env.EXAGENT_PASSWORD;
|
|
221
|
-
if (!password || password.length < 12) {
|
|
222
|
-
throw new Error('EXAGENT_PASSWORD must be at least 12 characters in non-interactive mode');
|
|
223
|
-
}
|
|
224
|
-
return password;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
printInfo(`Secrets encrypted with ${pc.cyan('AES-256-GCM')} (${pc.cyan('scrypt')} KDF)`);
|
|
228
|
-
printInfo('The password never leaves this machine.');
|
|
229
|
-
console.log();
|
|
230
|
-
|
|
231
|
-
const password = await clack.password({
|
|
232
|
-
message: 'Choose a device password (12+ characters):',
|
|
233
|
-
validate: (val) => {
|
|
234
|
-
if (val.length < 12) return 'Password must be at least 12 characters.';
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
if (clack.isCancel(password)) cancelled();
|
|
238
|
-
|
|
239
|
-
const confirm = await clack.password({
|
|
240
|
-
message: 'Confirm password:',
|
|
241
|
-
validate: (val) => {
|
|
242
|
-
if (val !== password) return 'Passwords do not match.';
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
if (clack.isCancel(confirm)) cancelled();
|
|
246
|
-
|
|
247
|
-
return password;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
// Secure store
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
|
|
254
|
-
function writeSecureStore(path: string, secrets: LocalSecretPayload, password: string): string {
|
|
255
|
-
const secureStorePath = expandHomeDir(path);
|
|
256
|
-
const encrypted = encryptSecretPayload(secrets, password);
|
|
257
|
-
const dir = dirname(secureStorePath);
|
|
258
|
-
if (!existsSync(dir)) {
|
|
259
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
260
|
-
}
|
|
261
|
-
writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
|
|
262
|
-
try {
|
|
263
|
-
chmodSync(secureStorePath, 0o600);
|
|
264
|
-
} catch {
|
|
265
|
-
// Best effort — some platforms ignore chmod semantics.
|
|
266
|
-
}
|
|
267
|
-
return secureStorePath;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// Public: password prompt (used by run/status commands)
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
|
-
|
|
274
|
-
export async function promptSecretPassword(question: string = 'Device password:'): Promise<string> {
|
|
275
|
-
if (isNonInteractive()) {
|
|
276
|
-
const password = process.env.EXAGENT_PASSWORD || process.env.EXAGENT_SECRET_PASSWORD;
|
|
277
|
-
if (!password) throw new Error('EXAGENT_PASSWORD required in non-interactive mode');
|
|
278
|
-
return password;
|
|
279
|
-
}
|
|
280
|
-
const password = await clack.password({ message: question });
|
|
281
|
-
if (clack.isCancel(password)) cancelled();
|
|
282
|
-
return password;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
// Main setup orchestrator
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
export async function ensureLocalSetup(configPath: string): Promise<void> {
|
|
290
|
-
const config = readConfigFile(configPath);
|
|
291
|
-
const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
|
|
292
|
-
if (
|
|
293
|
-
existingSecureStorePath &&
|
|
294
|
-
!config.secrets?.bootstrapToken &&
|
|
295
|
-
existsSync(existingSecureStorePath) &&
|
|
296
|
-
!config.apiToken &&
|
|
297
|
-
!config.wallet?.privateKey &&
|
|
298
|
-
!config.llm.apiKey
|
|
299
|
-
) {
|
|
300
|
-
printBanner();
|
|
301
|
-
printSuccess('Already set up', [
|
|
302
|
-
`${pc.cyan('npx exagent run')} Start the agent`,
|
|
303
|
-
`${pc.cyan('npx exagent config')} Change LLM API key or model`,
|
|
304
|
-
`${pc.cyan('npx exagent status')} Check agent connection`,
|
|
305
|
-
'',
|
|
306
|
-
`${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
|
|
307
|
-
]);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
printBanner();
|
|
312
|
-
|
|
313
|
-
clack.intro(pc.bold('Agent Setup'));
|
|
314
|
-
|
|
315
|
-
// Step 1: Bootstrap
|
|
316
|
-
printStep(1, 4, 'Bootstrap package');
|
|
317
|
-
const bootstrapPayload = await consumeBootstrapPackage(config);
|
|
318
|
-
if (config.secrets?.bootstrapToken) {
|
|
319
|
-
printDone('Bootstrap package consumed');
|
|
320
|
-
} else {
|
|
321
|
-
printInfo('No bootstrap token — manual configuration');
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Step 2: Wallet
|
|
325
|
-
printStep(2, 4, 'Wallet setup');
|
|
326
|
-
const walletPrivateKey = await setupWallet(config);
|
|
327
|
-
|
|
328
|
-
// Step 3: LLM
|
|
329
|
-
printStep(3, 4, 'LLM configuration');
|
|
330
|
-
const llm = await setupLlm(config);
|
|
331
|
-
|
|
332
|
-
// Step 4: Encryption
|
|
333
|
-
printStep(4, 4, 'Device encryption');
|
|
334
|
-
const password = await setupEncryption();
|
|
335
|
-
|
|
336
|
-
// Build secrets and write
|
|
337
|
-
const secrets: LocalSecretPayload = {
|
|
338
|
-
apiToken: bootstrapPayload.apiToken || config.apiToken || '',
|
|
339
|
-
walletPrivateKey,
|
|
340
|
-
llmApiKey: llm.apiKey,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
if (!secrets.apiToken) {
|
|
344
|
-
if (isNonInteractive()) {
|
|
345
|
-
const token = process.env.EXAGENT_API_TOKEN;
|
|
346
|
-
if (!token) throw new Error('EXAGENT_API_TOKEN required in non-interactive mode (no relay token from bootstrap)');
|
|
347
|
-
secrets.apiToken = token;
|
|
348
|
-
} else {
|
|
349
|
-
const token = await clack.password({
|
|
350
|
-
message: 'Agent relay token:',
|
|
351
|
-
validate: (val) => {
|
|
352
|
-
if (!val.trim()) return 'Relay token is required.';
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
if (clack.isCancel(token)) cancelled();
|
|
356
|
-
secrets.apiToken = token;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const nextConfig = structuredClone(config);
|
|
361
|
-
nextConfig.llm = {
|
|
362
|
-
...nextConfig.llm,
|
|
363
|
-
provider: llm.provider as RuntimeConfigFile['llm']['provider'],
|
|
364
|
-
model: llm.model,
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Strip plaintext secrets from config file
|
|
368
|
-
delete nextConfig.apiToken;
|
|
369
|
-
delete nextConfig.wallet;
|
|
370
|
-
delete nextConfig.llm.apiKey;
|
|
371
|
-
|
|
372
|
-
const secureStorePath = writeSecureStore(
|
|
373
|
-
nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
|
|
374
|
-
secrets,
|
|
375
|
-
password,
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
nextConfig.secrets = { secureStorePath };
|
|
379
|
-
writeConfigFile(configPath, nextConfig);
|
|
380
|
-
|
|
381
|
-
printDone(`Encrypted store: ${pc.dim(secureStorePath)}`);
|
|
382
|
-
|
|
383
|
-
clack.outro(pc.green('Setup complete'));
|
|
384
|
-
|
|
385
|
-
printSuccess('Ready', [
|
|
386
|
-
`${pc.cyan('npx exagent run')} Start the agent`,
|
|
387
|
-
`${pc.cyan('npx exagent config')} Change LLM API key or model`,
|
|
388
|
-
`${pc.cyan('npx exagent status')} Check agent connection`,
|
|
389
|
-
'',
|
|
390
|
-
`${pc.dim('Dashboard:')} ${pc.cyan('https://exagent.io')}`,
|
|
391
|
-
]);
|
|
392
|
-
}
|
package/src/signal.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import type { TradeSignal } from '@exagent/sdk';
|
|
2
|
-
import type { RelayClient } from './relay.js';
|
|
3
|
-
import type { FileStore } from './store.js';
|
|
4
|
-
import { getLogger } from './logger.js';
|
|
5
|
-
|
|
6
|
-
const SIGNAL_QUEUE_KEY = 'pending_trade_signals';
|
|
7
|
-
const MAX_QUEUE_SIZE = 1000;
|
|
8
|
-
|
|
9
|
-
interface QueuedSignal {
|
|
10
|
-
signal: TradeSignal;
|
|
11
|
-
reportType: 'trade' | 'perp_fill' | 'prediction_fill' | 'spot_fill' | 'bridge_fill';
|
|
12
|
-
queuedAt: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class SignalReporter {
|
|
16
|
-
private relay: RelayClient;
|
|
17
|
-
private store: FileStore | null;
|
|
18
|
-
private pendingSignals: QueuedSignal[] = [];
|
|
19
|
-
|
|
20
|
-
constructor(relay: RelayClient, store?: FileStore) {
|
|
21
|
-
this.relay = relay;
|
|
22
|
-
this.store = store ?? null;
|
|
23
|
-
|
|
24
|
-
// Load persisted queue from store (survives process crashes)
|
|
25
|
-
if (this.store) {
|
|
26
|
-
const persisted = this.store.get<QueuedSignal[]>(SIGNAL_QUEUE_KEY);
|
|
27
|
-
if (persisted && Array.isArray(persisted)) {
|
|
28
|
-
this.pendingSignals = persisted;
|
|
29
|
-
if (this.pendingSignals.length > 0) {
|
|
30
|
-
getLogger().info('signal', 'Loaded queued signals from disk', { count: this.pendingSignals.length });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Flush all queued signals to the relay. Called on reconnect. */
|
|
37
|
-
flushQueue(): void {
|
|
38
|
-
if (this.pendingSignals.length === 0) return;
|
|
39
|
-
if (!this.relay.isConnected) return;
|
|
40
|
-
|
|
41
|
-
getLogger().info('signal', 'Flushing queued signals', { count: this.pendingSignals.length });
|
|
42
|
-
const signals = [...this.pendingSignals];
|
|
43
|
-
this.pendingSignals = [];
|
|
44
|
-
this.persistQueue();
|
|
45
|
-
|
|
46
|
-
for (const queued of signals) {
|
|
47
|
-
this.relay.sendTradeSignal(queued.signal);
|
|
48
|
-
|
|
49
|
-
// Also send the human-readable message
|
|
50
|
-
const sig = queued.signal;
|
|
51
|
-
switch (queued.reportType) {
|
|
52
|
-
case 'trade':
|
|
53
|
-
this.relay.sendMessage(
|
|
54
|
-
'trade_executed', 'success',
|
|
55
|
-
`${sig.side.toUpperCase()} ${sig.symbol}`,
|
|
56
|
-
`${sig.side} ${sig.size} ${sig.symbol} @ $${sig.price.toFixed(2)} on ${sig.venue} (queued)`,
|
|
57
|
-
{ signal: sig },
|
|
58
|
-
);
|
|
59
|
-
break;
|
|
60
|
-
case 'perp_fill':
|
|
61
|
-
this.relay.sendMessage(
|
|
62
|
-
'perp_fill', 'success',
|
|
63
|
-
`Perp ${sig.side.toUpperCase()} ${sig.symbol}`,
|
|
64
|
-
`${sig.side} ${sig.size} ${sig.symbol} @ $${sig.price.toFixed(2)} (${sig.leverage ?? 1}x) (queued)`,
|
|
65
|
-
{ signal: sig },
|
|
66
|
-
);
|
|
67
|
-
break;
|
|
68
|
-
case 'prediction_fill':
|
|
69
|
-
this.relay.sendMessage(
|
|
70
|
-
'prediction_fill', 'success',
|
|
71
|
-
`Prediction ${sig.side.toUpperCase()}`,
|
|
72
|
-
`${sig.side} $${sig.size.toFixed(2)} on ${sig.symbol} @ ${sig.price.toFixed(4)} (queued)`,
|
|
73
|
-
{ signal: sig },
|
|
74
|
-
);
|
|
75
|
-
break;
|
|
76
|
-
case 'spot_fill':
|
|
77
|
-
this.relay.sendMessage(
|
|
78
|
-
'spot_fill', 'success',
|
|
79
|
-
`Spot ${sig.side.toUpperCase()} ${sig.symbol}`,
|
|
80
|
-
`${sig.side} ${sig.size} ${sig.symbol} @ $${sig.price.toFixed(4)} on ${sig.venue} (${sig.chain ?? 'unknown'}) (queued)`,
|
|
81
|
-
{ signal: sig },
|
|
82
|
-
);
|
|
83
|
-
break;
|
|
84
|
-
case 'bridge_fill':
|
|
85
|
-
this.relay.sendMessage(
|
|
86
|
-
'bridge_fill', 'success',
|
|
87
|
-
`Bridge ${sig.symbol}`,
|
|
88
|
-
`Bridged ${sig.size} ${sig.symbol} to ${sig.chain ?? 'unknown'} via ${sig.venue} (queued)`,
|
|
89
|
-
{ signal: sig },
|
|
90
|
-
);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getLogger().info('signal', 'Queue flushed successfully');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
get queueSize(): number {
|
|
99
|
-
return this.pendingSignals.length;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private enqueue(signal: TradeSignal, reportType: QueuedSignal['reportType']): void {
|
|
103
|
-
this.pendingSignals.push({ signal, reportType, queuedAt: Date.now() });
|
|
104
|
-
|
|
105
|
-
// Evict oldest if over cap
|
|
106
|
-
while (this.pendingSignals.length > MAX_QUEUE_SIZE) {
|
|
107
|
-
this.pendingSignals.shift();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
this.persistQueue();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private persistQueue(): void {
|
|
114
|
-
if (!this.store) return;
|
|
115
|
-
if (this.pendingSignals.length === 0) {
|
|
116
|
-
this.store.delete(SIGNAL_QUEUE_KEY);
|
|
117
|
-
} else {
|
|
118
|
-
this.store.set(SIGNAL_QUEUE_KEY, this.pendingSignals);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
reportTrade(signal: TradeSignal): void {
|
|
123
|
-
if (!this.relay.isConnected) {
|
|
124
|
-
getLogger().warn('signal', 'Not connected to relay — trade signal queued locally', { symbol: signal.symbol });
|
|
125
|
-
this.enqueue(signal, 'trade');
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
this.relay.sendTradeSignal(signal);
|
|
130
|
-
this.relay.sendMessage(
|
|
131
|
-
'trade_executed',
|
|
132
|
-
'success',
|
|
133
|
-
`${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
134
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} on ${signal.venue}`,
|
|
135
|
-
{ signal },
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
reportPerpFill(signal: TradeSignal): void {
|
|
140
|
-
if (!this.relay.isConnected) {
|
|
141
|
-
this.enqueue(signal, 'perp_fill');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.relay.sendTradeSignal(signal);
|
|
146
|
-
this.relay.sendMessage(
|
|
147
|
-
'perp_fill',
|
|
148
|
-
'success',
|
|
149
|
-
`Perp ${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
150
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(2)} (${signal.leverage ?? 1}x)`,
|
|
151
|
-
{ signal },
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
reportPredictionFill(signal: TradeSignal): void {
|
|
156
|
-
if (!this.relay.isConnected) {
|
|
157
|
-
this.enqueue(signal, 'prediction_fill');
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.relay.sendTradeSignal(signal);
|
|
162
|
-
this.relay.sendMessage(
|
|
163
|
-
'prediction_fill',
|
|
164
|
-
'success',
|
|
165
|
-
`Prediction ${signal.side.toUpperCase()}`,
|
|
166
|
-
`${signal.side} $${signal.size.toFixed(2)} on ${signal.symbol} @ ${signal.price.toFixed(4)}`,
|
|
167
|
-
{ signal },
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
reportSpotFill(signal: TradeSignal): void {
|
|
172
|
-
if (!this.relay.isConnected) {
|
|
173
|
-
this.enqueue(signal, 'spot_fill');
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
this.relay.sendTradeSignal(signal);
|
|
178
|
-
this.relay.sendMessage(
|
|
179
|
-
'spot_fill',
|
|
180
|
-
'success',
|
|
181
|
-
`Spot ${signal.side.toUpperCase()} ${signal.symbol}`,
|
|
182
|
-
`${signal.side} ${signal.size} ${signal.symbol} @ $${signal.price.toFixed(4)} on ${signal.venue} (${signal.chain ?? 'unknown'})`,
|
|
183
|
-
{ signal },
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
reportBridgeFill(signal: TradeSignal): void {
|
|
188
|
-
if (!this.relay.isConnected) {
|
|
189
|
-
this.enqueue(signal, 'bridge_fill');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this.relay.sendTradeSignal(signal);
|
|
194
|
-
this.relay.sendMessage(
|
|
195
|
-
'bridge_fill',
|
|
196
|
-
'success',
|
|
197
|
-
`Bridge ${signal.symbol}`,
|
|
198
|
-
`Bridged ${signal.size} ${signal.symbol} to ${signal.chain ?? 'unknown'} via ${signal.venue}`,
|
|
199
|
-
{ signal },
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
reportError(title: string, body: string, data?: Record<string, unknown>): void {
|
|
204
|
-
if (!this.relay.isConnected) return;
|
|
205
|
-
this.relay.sendMessage('error', 'error', title, body, data);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
reportInfo(title: string, body: string, data?: Record<string, unknown>): void {
|
|
209
|
-
if (!this.relay.isConnected) return;
|
|
210
|
-
this.relay.sendMessage('info', 'info', title, body, data);
|
|
211
|
-
}
|
|
212
|
-
}
|