@exagent/agent 0.3.0 → 0.3.2
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-VDK4XPAC.js +6318 -0
- package/dist/cli.js +252 -129
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1 -1
- package/package.json +9 -2
- package/src/cli.ts +33 -15
- package/src/config.ts +6 -0
- package/src/runtime.ts +370 -37
- package/src/scrub-secrets.ts +39 -0
- package/src/setup.ts +228 -129
- package/src/strategy/loader.ts +35 -2
- package/src/ui.ts +75 -0
|
@@ -0,0 +1,39 @@
|
|
|
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
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
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
4
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
import * as clack from '@clack/prompts';
|
|
7
6
|
import {
|
|
8
7
|
encryptSecretPayload,
|
|
9
8
|
getDefaultSecureStorePath,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
type RuntimeConfigFile,
|
|
13
12
|
writeConfigFile,
|
|
14
13
|
} from './config.js';
|
|
14
|
+
import { printBanner, printStep, printDone, printInfo, printError, printSuccess, pc } from './ui.js';
|
|
15
15
|
|
|
16
16
|
interface BootstrapPayload {
|
|
17
17
|
apiToken: string;
|
|
@@ -28,106 +28,23 @@ function expandHomeDir(path: string): string {
|
|
|
28
28
|
return resolve(homedir(), path.slice(2));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
31
|
+
function cancelled(): never {
|
|
32
|
+
clack.cancel('Setup cancelled.');
|
|
33
|
+
process.exit(0);
|
|
64
34
|
}
|
|
65
35
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Step 1: Bootstrap
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
125
39
|
|
|
126
40
|
async function consumeBootstrapPackage(config: RuntimeConfigFile): Promise<BootstrapPayload> {
|
|
127
41
|
if (!config.secrets?.bootstrapToken) {
|
|
128
42
|
return { apiToken: '' };
|
|
129
43
|
}
|
|
130
44
|
|
|
45
|
+
const apiHost = new URL(config.apiUrl).host;
|
|
46
|
+
printInfo(`Connecting to ${pc.cyan(apiHost)}...`);
|
|
47
|
+
|
|
131
48
|
const res = await fetch(`${config.apiUrl}/v1/agents/bootstrap/consume`, {
|
|
132
49
|
method: 'POST',
|
|
133
50
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -139,52 +56,157 @@ async function consumeBootstrapPackage(config: RuntimeConfigFile): Promise<Boots
|
|
|
139
56
|
|
|
140
57
|
if (!res.ok) {
|
|
141
58
|
const body = await res.text();
|
|
142
|
-
throw new Error(`Failed to consume
|
|
59
|
+
throw new Error(`Failed to consume bootstrap package: ${body}`);
|
|
143
60
|
}
|
|
144
61
|
|
|
145
62
|
const data = await res.json() as { payload: BootstrapPayload };
|
|
146
63
|
return data.payload;
|
|
147
64
|
}
|
|
148
65
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Step 2: Wallet
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
152
69
|
|
|
153
|
-
|
|
154
|
-
|
|
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;
|
|
155
75
|
}
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
158
130
|
}
|
|
159
131
|
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
|
162
146
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
158
|
}
|
|
168
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
|
+
}
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
llmApiKey: bootstrapPayload.llm?.apiKey || nextConfig.llm.apiKey || await promptSecret('Agent LLM API key: '),
|
|
174
|
-
};
|
|
174
|
+
printDone('LLM configured');
|
|
175
|
+
return { provider, model, apiKey };
|
|
176
|
+
}
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Step 4: Encryption
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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();
|
|
184
186
|
|
|
185
|
-
|
|
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;
|
|
186
204
|
}
|
|
187
205
|
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Secure store
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
188
210
|
function writeSecureStore(path: string, secrets: LocalSecretPayload, password: string): string {
|
|
189
211
|
const secureStorePath = expandHomeDir(path);
|
|
190
212
|
const encrypted = encryptSecretPayload(secrets, password);
|
|
@@ -196,11 +218,25 @@ function writeSecureStore(path: string, secrets: LocalSecretPayload, password: s
|
|
|
196
218
|
try {
|
|
197
219
|
chmodSync(secureStorePath, 0o600);
|
|
198
220
|
} catch {
|
|
199
|
-
// Best effort
|
|
221
|
+
// Best effort — some platforms ignore chmod semantics.
|
|
200
222
|
}
|
|
201
223
|
return secureStorePath;
|
|
202
224
|
}
|
|
203
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
|
+
|
|
204
240
|
export async function ensureLocalSetup(configPath: string): Promise<void> {
|
|
205
241
|
const config = readConfigFile(configPath);
|
|
206
242
|
const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
|
|
@@ -215,19 +251,82 @@ export async function ensureLocalSetup(configPath: string): Promise<void> {
|
|
|
215
251
|
return;
|
|
216
252
|
}
|
|
217
253
|
|
|
254
|
+
printBanner();
|
|
255
|
+
|
|
256
|
+
clack.intro(pc.bold('Agent Setup'));
|
|
257
|
+
|
|
258
|
+
// Step 1: Bootstrap
|
|
259
|
+
printStep(1, 4, 'Bootstrap package');
|
|
218
260
|
const bootstrapPayload = await consumeBootstrapPackage(config);
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
|
|
221
313
|
const secureStorePath = writeSecureStore(
|
|
222
314
|
nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
|
|
223
315
|
secrets,
|
|
224
316
|
password,
|
|
225
317
|
);
|
|
226
318
|
|
|
227
|
-
nextConfig.secrets = {
|
|
228
|
-
secureStorePath,
|
|
229
|
-
};
|
|
230
|
-
|
|
319
|
+
nextConfig.secrets = { secureStorePath };
|
|
231
320
|
writeConfigFile(configPath, nextConfig);
|
|
232
|
-
|
|
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
|
+
]);
|
|
233
332
|
}
|
package/src/strategy/loader.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { StrategyFunction, StrategyContext, TradeSignal } from '@exagent/sdk';
|
|
5
5
|
import { getTemplate } from './templates.js';
|
|
6
|
+
import { scrubSecrets } from '../scrub-secrets.js';
|
|
6
7
|
|
|
7
8
|
const promptSignalSchema = z.object({
|
|
8
9
|
symbol: z.string().min(1),
|
|
@@ -24,6 +25,7 @@ const promptSignalArraySchema = z.array(promptSignalSchema);
|
|
|
24
25
|
|
|
25
26
|
export async function loadStrategy(config: {
|
|
26
27
|
file?: string;
|
|
28
|
+
code?: string;
|
|
27
29
|
template?: string;
|
|
28
30
|
prompt?: {
|
|
29
31
|
name?: string;
|
|
@@ -35,6 +37,10 @@ export async function loadStrategy(config: {
|
|
|
35
37
|
return loadFromFile(config.file);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
if (config.code) {
|
|
41
|
+
return loadFromCode(config.code);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
if (config.prompt) {
|
|
39
45
|
return loadFromPrompt(config.prompt);
|
|
40
46
|
}
|
|
@@ -62,7 +68,31 @@ async function loadFromFile(filePath: string): Promise<StrategyFunction> {
|
|
|
62
68
|
const fn = mod.default || mod.strategy;
|
|
63
69
|
|
|
64
70
|
if (typeof fn !== 'function') {
|
|
65
|
-
|
|
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`);
|
|
66
96
|
}
|
|
67
97
|
|
|
68
98
|
return fn as StrategyFunction;
|
|
@@ -109,7 +139,10 @@ function loadFromPrompt(config: {
|
|
|
109
139
|
},
|
|
110
140
|
]);
|
|
111
141
|
|
|
112
|
-
|
|
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]*\]/);
|
|
113
146
|
if (!match) {
|
|
114
147
|
context.log('Prompt strategy returned a non-JSON response; no signals emitted.');
|
|
115
148
|
return [];
|
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 };
|