balchemy 0.1.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/README.md +59 -0
- package/assets/bcrow.png +0 -0
- package/dist/agent-store.d.ts +40 -0
- package/dist/agent-store.d.ts.map +1 -0
- package/dist/agent-store.js +206 -0
- package/dist/agent-store.js.map +1 -0
- package/dist/config-loader.d.ts +8 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +106 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/docker-gen.d.ts +6 -0
- package/dist/docker-gen.d.ts.map +1 -0
- package/dist/docker-gen.js +40 -0
- package/dist/docker-gen.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/openai-oauth.d.ts +28 -0
- package/dist/openai-oauth.d.ts.map +1 -0
- package/dist/openai-oauth.js +215 -0
- package/dist/openai-oauth.js.map +1 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +63 -0
- package/dist/runner.js.map +1 -0
- package/dist/terminal-logo.d.ts +15 -0
- package/dist/terminal-logo.d.ts.map +1 -0
- package/dist/terminal-logo.js +121 -0
- package/dist/terminal-logo.js.map +1 -0
- package/dist/tui/AgentBridge.d.ts +35 -0
- package/dist/tui/AgentBridge.d.ts.map +1 -0
- package/dist/tui/AgentBridge.js +235 -0
- package/dist/tui/AgentBridge.js.map +1 -0
- package/dist/tui/App.d.ts +8 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +118 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/ChatAgent.d.ts +41 -0
- package/dist/tui/ChatAgent.d.ts.map +1 -0
- package/dist/tui/ChatAgent.js +312 -0
- package/dist/tui/ChatAgent.js.map +1 -0
- package/dist/tui/ChatPanel.d.ts +10 -0
- package/dist/tui/ChatPanel.d.ts.map +1 -0
- package/dist/tui/ChatPanel.js +43 -0
- package/dist/tui/ChatPanel.js.map +1 -0
- package/dist/tui/StatusPanel.d.ts +8 -0
- package/dist/tui/StatusPanel.d.ts.map +1 -0
- package/dist/tui/StatusPanel.js +25 -0
- package/dist/tui/StatusPanel.js.map +1 -0
- package/dist/tui/start.d.ts +3 -0
- package/dist/tui/start.d.ts.map +1 -0
- package/dist/tui/start.js +14 -0
- package/dist/tui/start.js.map +1 -0
- package/dist/tui/types.d.ts +61 -0
- package/dist/tui/types.d.ts.map +1 -0
- package/dist/tui/types.js +3 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/wizard.d.ts +16 -0
- package/dist/wizard.d.ts.map +1 -0
- package/dist/wizard.js +716 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +57 -0
- package/templates/.env.example +19 -0
- package/templates/Dockerfile +20 -0
- package/templates/agent.config.yaml +71 -0
- package/templates/docker-compose.yml +27 -0
package/dist/wizard.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balchemy Agent Setup Wizard
|
|
3
|
+
*
|
|
4
|
+
* Full onboarding flow:
|
|
5
|
+
* 1. BCrow welcome + LLM requirement notice
|
|
6
|
+
* 2. LLM provider selection (Anthropic, OpenAI, Gemini, Grok, OpenRouter)
|
|
7
|
+
* 3. API key input
|
|
8
|
+
* 4. Model selection (per-provider model list)
|
|
9
|
+
* 5. New agent or existing agent
|
|
10
|
+
* 6. Walletless onboarding (auto) or MCP endpoint entry
|
|
11
|
+
* 7. MCP setup_agent wizard (wallets, slippage, autonomous strategy)
|
|
12
|
+
* 8. Write agent.config.yaml + .env
|
|
13
|
+
* 9. Offer to start the agent loop
|
|
14
|
+
*/
|
|
15
|
+
import * as readline from "readline";
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { exec } from "child_process";
|
|
19
|
+
import { loginWithOpenAI } from "./openai-oauth.js";
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { renderLogo } from "./terminal-logo.js";
|
|
22
|
+
import { saveAgent } from "./agent-store.js";
|
|
23
|
+
// ── Brand Colors ──────────────────────────────────────────────────────────────
|
|
24
|
+
const G = "\x1b[38;2;186;115;6m"; // gold
|
|
25
|
+
const W = "\x1b[1;37m"; // white bold
|
|
26
|
+
const D = "\x1b[38;5;245m"; // dim
|
|
27
|
+
const R = "\x1b[0m"; // reset
|
|
28
|
+
const T = "\x1b[38;2;0;172;176m"; // teal
|
|
29
|
+
const WELCOME_TEXT = `
|
|
30
|
+
${G}B${T}alchemy ${W}Agent${R} ${D}v0.1.0${R}
|
|
31
|
+
|
|
32
|
+
${D}To deploy a fully autonomous trading bot, you must provide${R}
|
|
33
|
+
${D}your own LLM. Your agent's strategy, every decision, and the${R}
|
|
34
|
+
${D}entire execution flow are driven by the model you choose.${R}
|
|
35
|
+
${D}Select the right provider and model for your trading style${R}
|
|
36
|
+
${D}${G}— everything is in your hands.${R}
|
|
37
|
+
`;
|
|
38
|
+
const PROVIDERS = [
|
|
39
|
+
{
|
|
40
|
+
name: "anthropic",
|
|
41
|
+
label: "Anthropic (Claude)",
|
|
42
|
+
baseUrl: "https://api.anthropic.com",
|
|
43
|
+
sdkProvider: "anthropic",
|
|
44
|
+
authHeader: "x-api-key",
|
|
45
|
+
keyUrl: "https://console.anthropic.com/settings/keys",
|
|
46
|
+
models: [
|
|
47
|
+
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", tier: "fast", costHint: "$1/1M in · $5/1M out" },
|
|
48
|
+
{ id: "claude-sonnet-4-6-20260217", label: "Claude Sonnet 4.6", tier: "balanced", costHint: "$3/1M in · $15/1M out" },
|
|
49
|
+
{ id: "claude-opus-4-6-20260205", label: "Claude Opus 4.6", tier: "powerful", costHint: "$5/1M in · $25/1M out" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "openai",
|
|
54
|
+
label: "OpenAI (GPT)",
|
|
55
|
+
baseUrl: "https://api.openai.com/v1",
|
|
56
|
+
sdkProvider: "openai",
|
|
57
|
+
authHeader: "Authorization",
|
|
58
|
+
keyUrl: "https://platform.openai.com/api-keys",
|
|
59
|
+
models: [
|
|
60
|
+
{ id: "gpt-5.4-nano", label: "GPT-5.4 Nano", tier: "fast", costHint: "$0.10/1M in · $0.40/1M out" },
|
|
61
|
+
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini", tier: "fast", costHint: "$0.30/1M in · $1.20/1M out" },
|
|
62
|
+
{ id: "gpt-5.4", label: "GPT-5.4", tier: "balanced", costHint: "$2.50/1M in · $10/1M out" },
|
|
63
|
+
{ id: "o4-mini", label: "o4-mini (reasoning)", tier: "powerful", costHint: "$1.10/1M in · $4.40/1M out" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "gemini",
|
|
68
|
+
label: "Google (Gemini)",
|
|
69
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
70
|
+
sdkProvider: "openai",
|
|
71
|
+
authHeader: "Authorization",
|
|
72
|
+
keyUrl: "https://aistudio.google.com/apikey",
|
|
73
|
+
models: [
|
|
74
|
+
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite", tier: "fast", costHint: "$0.02/1M in · $0.10/1M out" },
|
|
75
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", tier: "fast", costHint: "$0.15/1M in · $0.60/1M out" },
|
|
76
|
+
{ id: "gemini-3.1-pro", label: "Gemini 3.1 Pro", tier: "balanced", costHint: "$1.25/1M in · $10/1M out" },
|
|
77
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", tier: "powerful", costHint: "$1.25/1M in · $10/1M out" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "grok",
|
|
82
|
+
label: "xAI (Grok)",
|
|
83
|
+
baseUrl: "https://api.x.ai/v1",
|
|
84
|
+
sdkProvider: "openai",
|
|
85
|
+
authHeader: "Authorization",
|
|
86
|
+
keyUrl: "https://console.x.ai/",
|
|
87
|
+
models: [
|
|
88
|
+
{ id: "grok-4.1-fast", label: "Grok 4.1 Fast", tier: "fast", costHint: "$0.20/1M in · $0.50/1M out" },
|
|
89
|
+
{ id: "grok-4", label: "Grok 4", tier: "balanced", costHint: "$2/1M in · $6/1M out" },
|
|
90
|
+
{ id: "grok-4.20", label: "Grok 4.20", tier: "powerful", costHint: "$2/1M in · $6/1M out" },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "openrouter",
|
|
95
|
+
label: "OpenRouter (multi-provider)",
|
|
96
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
97
|
+
sdkProvider: "openai",
|
|
98
|
+
authHeader: "Authorization",
|
|
99
|
+
keyUrl: "https://openrouter.ai/keys",
|
|
100
|
+
models: [
|
|
101
|
+
{ id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash", tier: "fast", costHint: "~$0.15/1M in" },
|
|
102
|
+
{ id: "x-ai/grok-4.1-fast", label: "Grok 4.1 Fast", tier: "fast", costHint: "~$0.20/1M in" },
|
|
103
|
+
{ id: "openai/gpt-5.4-mini", label: "GPT-5.4 Mini", tier: "fast", costHint: "~$0.30/1M in" },
|
|
104
|
+
{ id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet 4.6", tier: "balanced", costHint: "~$3/1M in" },
|
|
105
|
+
{ id: "anthropic/claude-opus-4-6", label: "Claude Opus 4.6", tier: "powerful", costHint: "~$5/1M in" },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const STRATEGIES = [
|
|
110
|
+
{
|
|
111
|
+
name: "memecoin-sniper",
|
|
112
|
+
label: "Memecoin Sniper",
|
|
113
|
+
description: "Fast entry on new PumpFun launches, quick exits on pump",
|
|
114
|
+
naturalLanguageRules: "Act fast on new token launches. Max 5% portfolio per trade. Stop loss at 30%. Take profit at 50% (sell 25%), 100% (sell 25%), 500% (sell 50%). Max 5 concurrent positions. Prioritize highest volume token when multiple signals fire.",
|
|
115
|
+
preset: "memecoin_sniper",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "dca-accumulator",
|
|
119
|
+
label: "DCA Accumulator",
|
|
120
|
+
description: "Dollar-cost average into tokens at regular intervals",
|
|
121
|
+
naturalLanguageRules: "Buy fixed amounts at regular intervals. Max 3% portfolio per trade. Stop loss at 20%. Never buy if 24h volume < $50K. Pause on 30% portfolio drawdown.",
|
|
122
|
+
preset: "dca_accumulator",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "swing-trader",
|
|
126
|
+
label: "Swing Trader",
|
|
127
|
+
description: "Hold positions 2-72h, exit on momentum signals",
|
|
128
|
+
naturalLanguageRules: "Hold positions 2-72 hours. Max 10% portfolio per trade. Stop loss at 10%. Take profit at 20%. Only enter tokens with > $100K liquidity and verified contracts.",
|
|
129
|
+
preset: "swing_trader",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "custom",
|
|
133
|
+
label: "Custom Strategy",
|
|
134
|
+
description: "Define your own rules in natural language",
|
|
135
|
+
naturalLanguageRules: "",
|
|
136
|
+
preset: "memecoin_sniper",
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
// ── Spinner ───────────────────────────────────────────────────────────────────
|
|
140
|
+
const SPINNER_FRAMES = [" ", " ", " ", " ", "", " ", " ", " "];
|
|
141
|
+
class Spinner {
|
|
142
|
+
interval = null;
|
|
143
|
+
frame = 0;
|
|
144
|
+
message;
|
|
145
|
+
constructor(message) {
|
|
146
|
+
this.message = message;
|
|
147
|
+
}
|
|
148
|
+
start() {
|
|
149
|
+
this.frame = 0;
|
|
150
|
+
process.stdout.write(` ${T}${SPINNER_FRAMES[0]}${R} ${this.message}`);
|
|
151
|
+
this.interval = setInterval(() => {
|
|
152
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
153
|
+
process.stdout.write(`\r ${T}${SPINNER_FRAMES[this.frame]}${R} ${this.message}`);
|
|
154
|
+
}, 100);
|
|
155
|
+
}
|
|
156
|
+
succeed(msg) {
|
|
157
|
+
this.stop();
|
|
158
|
+
process.stdout.write(`\r ${T}*${R} ${msg ?? this.message}\n`);
|
|
159
|
+
}
|
|
160
|
+
fail(msg) {
|
|
161
|
+
this.stop();
|
|
162
|
+
process.stdout.write(`\r \x1b[1;31mx${R} ${msg ?? this.message}\n`);
|
|
163
|
+
}
|
|
164
|
+
stop() {
|
|
165
|
+
if (this.interval) {
|
|
166
|
+
clearInterval(this.interval);
|
|
167
|
+
this.interval = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function spin(message) {
|
|
172
|
+
const s = new Spinner(message);
|
|
173
|
+
s.start();
|
|
174
|
+
return s;
|
|
175
|
+
}
|
|
176
|
+
// ── Readline Helpers ──────────────────────────────────────────────────────────
|
|
177
|
+
function ask(rl, question, defaultVal = "") {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const hint = defaultVal ? ` \x1b[38;5;245m[${defaultVal}]\x1b[0m` : "";
|
|
180
|
+
rl.question(` ${question}${hint}: `, (answer) => {
|
|
181
|
+
resolve(answer.trim() || defaultVal);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function askSecret(rl, question) {
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
rl.question(` ${question}: `, (answer) => {
|
|
188
|
+
resolve(answer.trim());
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function askNumber(rl, question, defaultVal) {
|
|
193
|
+
return ask(rl, question, String(defaultVal)).then((v) => {
|
|
194
|
+
const n = parseFloat(v);
|
|
195
|
+
return isNaN(n) ? defaultVal : n;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function printChoices(items, extraInfo) {
|
|
199
|
+
items.forEach((item, i) => {
|
|
200
|
+
const extra = extraInfo ? ` \x1b[38;5;245m${extraInfo(item, i)}\x1b[0m` : "";
|
|
201
|
+
process.stdout.write(` \x1b[1;37m${i + 1}.\x1b[0m ${item.label}${extra}\n`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function askChoice(rl, question, items, extraInfo) {
|
|
205
|
+
process.stdout.write(`\n \x1b[1;36m${question}\x1b[0m\n`);
|
|
206
|
+
printChoices(items, extraInfo);
|
|
207
|
+
const answer = await ask(rl, `Choose [1-${items.length}]`, "1");
|
|
208
|
+
const idx = parseInt(answer, 10) - 1;
|
|
209
|
+
if (idx >= 0 && idx < items.length)
|
|
210
|
+
return items[idx];
|
|
211
|
+
return items[0];
|
|
212
|
+
}
|
|
213
|
+
function printStep(step, total, label) {
|
|
214
|
+
process.stdout.write(`\n \x1b[1;35m[${step}/${total}]\x1b[0m \x1b[1;37m${label}\x1b[0m\n`);
|
|
215
|
+
}
|
|
216
|
+
function printSuccess(msg) {
|
|
217
|
+
process.stdout.write(` \x1b[1;32m✓\x1b[0m ${msg}\n`);
|
|
218
|
+
}
|
|
219
|
+
function printInfo(msg) {
|
|
220
|
+
process.stdout.write(` \x1b[38;5;245m${msg}\x1b[0m\n`);
|
|
221
|
+
}
|
|
222
|
+
function printError(msg) {
|
|
223
|
+
process.stdout.write(` \x1b[1;31m✗\x1b[0m ${msg}\n`);
|
|
224
|
+
}
|
|
225
|
+
// ── MCP Call Helper ───────────────────────────────────────────────────────────
|
|
226
|
+
async function mcpCall(endpoint, apiKey, method, params) {
|
|
227
|
+
const nonce = `nonce-${Date.now()}-${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
228
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
229
|
+
const res = await fetch(endpoint, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
Accept: "application/json, text/event-stream",
|
|
234
|
+
Authorization: `Bearer ${apiKey}`,
|
|
235
|
+
"x-request-nonce": nonce,
|
|
236
|
+
"x-request-timestamp": timestamp,
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
jsonrpc: "2.0",
|
|
240
|
+
id: randomUUID(),
|
|
241
|
+
method,
|
|
242
|
+
params,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
const raw = await res.text();
|
|
246
|
+
let jsonStr = raw;
|
|
247
|
+
if (raw.includes("\ndata: ")) {
|
|
248
|
+
const dataLine = raw.split("\n").find((l) => l.startsWith("data: "));
|
|
249
|
+
jsonStr = dataLine ? dataLine.slice(6) : raw;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(jsonStr);
|
|
253
|
+
if (parsed.error)
|
|
254
|
+
return { error: parsed.error };
|
|
255
|
+
return { result: parsed.result };
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return { error: { message: `Invalid response: ${raw.slice(0, 200)}` } };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function callSetupTool(endpoint, apiKey, args) {
|
|
262
|
+
const resp = await mcpCall(endpoint, apiKey, "tools/call", {
|
|
263
|
+
name: "setup_agent",
|
|
264
|
+
arguments: args,
|
|
265
|
+
});
|
|
266
|
+
if (resp.error) {
|
|
267
|
+
printError(resp.error.message);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const content = resp.result?.content;
|
|
271
|
+
const text = content?.[0]?.text ?? "";
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(text);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return { reply: text };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function extractAddress(toolResult) {
|
|
280
|
+
if (!toolResult)
|
|
281
|
+
return "(failed)";
|
|
282
|
+
// Try structured.address first
|
|
283
|
+
const structured = toolResult.structured;
|
|
284
|
+
if (structured?.address && typeof structured.address === "string")
|
|
285
|
+
return structured.address;
|
|
286
|
+
// Try top-level reply text — extract address pattern
|
|
287
|
+
const reply = String(toolResult.reply ?? JSON.stringify(toolResult));
|
|
288
|
+
const solMatch = reply.match(/([1-9A-HJ-NP-Za-km-z]{32,44})/);
|
|
289
|
+
if (solMatch)
|
|
290
|
+
return solMatch[1];
|
|
291
|
+
const evmMatch = reply.match(/(0x[a-fA-F0-9]{40})/);
|
|
292
|
+
if (evmMatch)
|
|
293
|
+
return evmMatch[1];
|
|
294
|
+
return "(check agent status for address)";
|
|
295
|
+
}
|
|
296
|
+
function extractField(toolResult, field) {
|
|
297
|
+
if (!toolResult)
|
|
298
|
+
return null;
|
|
299
|
+
const structured = toolResult.structured;
|
|
300
|
+
if (structured?.[field] != null)
|
|
301
|
+
return String(structured[field]);
|
|
302
|
+
if (toolResult[field] != null)
|
|
303
|
+
return String(toolResult[field]);
|
|
304
|
+
const reply = String(toolResult.reply ?? "");
|
|
305
|
+
return reply || null;
|
|
306
|
+
}
|
|
307
|
+
// ── Walletless Onboarding ─────────────────────────────────────────────────────
|
|
308
|
+
const API_BASE = "https://api.balchemy.ai/api";
|
|
309
|
+
async function walletlessOnboard(agentName) {
|
|
310
|
+
// Step 1: Init
|
|
311
|
+
const initRes = await fetch(`${API_BASE}/public/erc8004/onboarding/walletless/init`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { "Content-Type": "application/json" },
|
|
314
|
+
body: JSON.stringify({ agentId: agentName }),
|
|
315
|
+
});
|
|
316
|
+
const initData = (await initRes.json());
|
|
317
|
+
if (!initData.success || !initData.data?.tempId) {
|
|
318
|
+
printError(`Onboarding init failed: ${JSON.stringify(initData.error ?? initData)}`);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
// Step 2: Provision
|
|
322
|
+
const provRes = await fetch(`${API_BASE}/public/erc8004/onboarding/walletless/provision`, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { "Content-Type": "application/json" },
|
|
325
|
+
body: JSON.stringify({ tempId: initData.data.tempId }),
|
|
326
|
+
});
|
|
327
|
+
const provData = (await provRes.json());
|
|
328
|
+
if (!provData.success || !provData.data) {
|
|
329
|
+
printError(`Provisioning failed: ${JSON.stringify(provData.error ?? provData)}`);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return provData.data;
|
|
333
|
+
}
|
|
334
|
+
function generateYaml(r) {
|
|
335
|
+
const baseUrlLine = r.provider.sdkProvider === "openai" && r.provider.name !== "openai"
|
|
336
|
+
? ` base_url: "${r.provider.baseUrl}"\n`
|
|
337
|
+
: "";
|
|
338
|
+
return [
|
|
339
|
+
`# Balchemy Agent Configuration`,
|
|
340
|
+
`# Generated by create-balchemy-agent`,
|
|
341
|
+
`# Agent: ${r.publicId} | Provider: ${r.provider.label} | Model: ${r.model.id}`,
|
|
342
|
+
``,
|
|
343
|
+
`mcp_endpoint: "\${MCP_ENDPOINT}"`,
|
|
344
|
+
`api_key: "\${BALCHEMY_API_KEY}"`,
|
|
345
|
+
``,
|
|
346
|
+
`llm:`,
|
|
347
|
+
` provider: ${r.provider.sdkProvider}`,
|
|
348
|
+
` api_key: "\${LLM_API_KEY}"`,
|
|
349
|
+
` model: ${r.model.id}`,
|
|
350
|
+
baseUrlLine ? baseUrlLine.trimEnd() : null,
|
|
351
|
+
` max_daily_usd: ${r.maxDailyLlmCost}`,
|
|
352
|
+
` timeout_ms: 15000`,
|
|
353
|
+
``,
|
|
354
|
+
`strategy: ${r.strategy.name}`,
|
|
355
|
+
`shadow_mode: ${r.shadowMode}`,
|
|
356
|
+
``,
|
|
357
|
+
`behavior_rules:`,
|
|
358
|
+
` version: "1"`,
|
|
359
|
+
` preset: ${r.strategy.preset}`,
|
|
360
|
+
r.strategy.naturalLanguageRules
|
|
361
|
+
? ` rules: "${r.strategy.naturalLanguageRules}"`
|
|
362
|
+
: ` # Define your rules here`,
|
|
363
|
+
``,
|
|
364
|
+
]
|
|
365
|
+
.filter((l) => l !== null)
|
|
366
|
+
.join("\n");
|
|
367
|
+
}
|
|
368
|
+
function generateDotEnv(r) {
|
|
369
|
+
return [
|
|
370
|
+
`# Balchemy Agent — ${r.publicId}`,
|
|
371
|
+
`# Keep this file private — never commit to git`,
|
|
372
|
+
``,
|
|
373
|
+
`MCP_ENDPOINT=${r.mcpEndpoint}`,
|
|
374
|
+
`BALCHEMY_API_KEY=${r.apiKey}`,
|
|
375
|
+
`LLM_API_KEY=${r.llmApiKey}`,
|
|
376
|
+
``,
|
|
377
|
+
].join("\n");
|
|
378
|
+
}
|
|
379
|
+
// ── Main Wizard ───────────────────────────────────────────────────────────────
|
|
380
|
+
export async function runWizard(outDir) {
|
|
381
|
+
const rl = readline.createInterface({
|
|
382
|
+
input: process.stdin,
|
|
383
|
+
output: process.stdout,
|
|
384
|
+
});
|
|
385
|
+
process.stdout.write(renderLogo(20));
|
|
386
|
+
process.stdout.write(WELCOME_TEXT);
|
|
387
|
+
const TOTAL_STEPS = 8;
|
|
388
|
+
try {
|
|
389
|
+
// ── Step 1: LLM Provider ──────────────────────────────────────────────
|
|
390
|
+
printStep(1, TOTAL_STEPS, "LLM Provider");
|
|
391
|
+
const provider = await askChoice(rl, "Select your LLM provider:", PROVIDERS, (p) => (p.name === "openrouter" ? "(access all models via one key)" : ""));
|
|
392
|
+
printSuccess(`Provider: ${provider.label}`);
|
|
393
|
+
// ── Step 2: Authentication ──────────────────────────────────────────
|
|
394
|
+
printStep(2, TOTAL_STEPS, "Authentication");
|
|
395
|
+
let llmApiKey = "";
|
|
396
|
+
let useOAuth = false;
|
|
397
|
+
let llmBaseUrlOverride;
|
|
398
|
+
// OpenAI is the only provider with official OAuth support for subscriptions
|
|
399
|
+
if (provider.name === "openai") {
|
|
400
|
+
const authChoice = await askChoice(rl, "How do you want to connect?", [
|
|
401
|
+
{ label: "ChatGPT Subscription (log in with browser)" },
|
|
402
|
+
{ label: "API Key (pay-per-use)" },
|
|
403
|
+
]);
|
|
404
|
+
useOAuth = authChoice.label.includes("Subscription");
|
|
405
|
+
}
|
|
406
|
+
if (useOAuth) {
|
|
407
|
+
// OpenAI OAuth PKCE flow
|
|
408
|
+
printInfo("Opening browser — log in with your ChatGPT account...\n");
|
|
409
|
+
try {
|
|
410
|
+
const tokens = await loginWithOpenAI();
|
|
411
|
+
llmApiKey = tokens.accessToken;
|
|
412
|
+
// ChatGPT subscription uses the Codex API endpoint
|
|
413
|
+
llmBaseUrlOverride = "https://api.openai.com/v1";
|
|
414
|
+
printSuccess(`Logged in${tokens.accountId ? ` (${tokens.accountId})` : ""}`);
|
|
415
|
+
// Store refresh token for auto-refresh
|
|
416
|
+
const authCachePath = path.join(process.env.HOME ?? "~", ".balchemy", "auth.json");
|
|
417
|
+
const authDir = path.dirname(authCachePath);
|
|
418
|
+
if (!fs.existsSync(authDir))
|
|
419
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
420
|
+
fs.writeFileSync(authCachePath, JSON.stringify({
|
|
421
|
+
provider: "openai",
|
|
422
|
+
accessToken: tokens.accessToken,
|
|
423
|
+
refreshToken: tokens.refreshToken,
|
|
424
|
+
expiresAt: tokens.expiresAt,
|
|
425
|
+
accountId: tokens.accountId,
|
|
426
|
+
}, null, 2), "utf8");
|
|
427
|
+
printInfo(`Credentials saved to ${authCachePath}`);
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
printError(`OAuth failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
431
|
+
printInfo("Falling back to API key...\n");
|
|
432
|
+
useOAuth = false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!useOAuth) {
|
|
436
|
+
// API key flow — open browser to key page
|
|
437
|
+
printInfo(`Opening ${provider.label} API key page...`);
|
|
438
|
+
openBrowser(provider.keyUrl);
|
|
439
|
+
printInfo(`${D}${provider.keyUrl}${R}`);
|
|
440
|
+
printInfo("Copy your API key and paste it below.\n");
|
|
441
|
+
llmApiKey = await askSecret(rl, "Paste your API key");
|
|
442
|
+
if (!llmApiKey) {
|
|
443
|
+
printError("API key is required.");
|
|
444
|
+
rl.close();
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
// Validate
|
|
448
|
+
const keySpinner = spin("Validating API key...");
|
|
449
|
+
const keyValid = await validateApiKey(provider, llmApiKey);
|
|
450
|
+
if (keyValid) {
|
|
451
|
+
keySpinner.succeed("API key validated");
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
keySpinner.succeed("Could not validate (continuing anyway)");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// ── Step 3: Model Selection ───────────────────────────────────────────
|
|
458
|
+
printStep(3, TOTAL_STEPS, "Model Selection");
|
|
459
|
+
printInfo("Faster models cost less but may make simpler decisions.");
|
|
460
|
+
printInfo("Powerful models cost more but analyze deeper.\n");
|
|
461
|
+
const availableModels = provider.models;
|
|
462
|
+
const model = await askChoice(rl, "Select model:", availableModels, (m) => `${m.tier} — ${m.costHint}`);
|
|
463
|
+
printSuccess(`Model: ${model.label} (${model.id})`);
|
|
464
|
+
const maxDailyLlmCost = await askNumber(rl, "Max daily LLM spend (USD)", 5);
|
|
465
|
+
// ── Step 4: Agent ─────────────────────────────────────────────────────
|
|
466
|
+
printStep(4, TOTAL_STEPS, "Agent Setup");
|
|
467
|
+
const agentChoice = await askChoice(rl, "Agent:", [
|
|
468
|
+
{ label: "Create new agent", value: "new" },
|
|
469
|
+
{ label: "Connect existing agent (I have MCP endpoint + API key)", value: "existing" },
|
|
470
|
+
]);
|
|
471
|
+
let mcpEndpoint;
|
|
472
|
+
let apiKey;
|
|
473
|
+
let publicId;
|
|
474
|
+
let masterKey = null;
|
|
475
|
+
let solAddr = "";
|
|
476
|
+
let baseAddr = "";
|
|
477
|
+
if (agentChoice.value === "new") {
|
|
478
|
+
const agentName = await ask(rl, "Agent name", `agent-${Date.now().toString(36)}`);
|
|
479
|
+
const onboardSpinner = spin("Creating agent via walletless onboarding...");
|
|
480
|
+
const onboard = await walletlessOnboard(agentName);
|
|
481
|
+
if (!onboard) {
|
|
482
|
+
onboardSpinner.fail("Onboarding failed. Check your network and try again.");
|
|
483
|
+
rl.close();
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
mcpEndpoint = onboard.endpoint;
|
|
487
|
+
apiKey = onboard.apiKey;
|
|
488
|
+
publicId = onboard.publicId;
|
|
489
|
+
onboardSpinner.succeed(`Agent created: ${publicId}`);
|
|
490
|
+
printSuccess(`MCP endpoint: ${mcpEndpoint}`);
|
|
491
|
+
printSuccess(`API key: ${apiKey}`);
|
|
492
|
+
// ── MCP Setup (same flow as MCP onboarding docs) ──────────────────
|
|
493
|
+
// Step A: Bind developer wallet
|
|
494
|
+
process.stdout.write("\n");
|
|
495
|
+
printStep(5, 8, "Developer Wallet");
|
|
496
|
+
printInfo("Your EVM wallet is used for recovery, Hub access, and withdrawals.");
|
|
497
|
+
printInfo("When you connect this wallet to balchemy.ai, you'll see this bot in your Hub dashboard.\n");
|
|
498
|
+
const walletAddr = await ask(rl, "Your EVM wallet address (0x...)");
|
|
499
|
+
if (walletAddr && walletAddr.startsWith("0x") && walletAddr.length === 42) {
|
|
500
|
+
const bindSpinner = spin("Binding developer wallet...");
|
|
501
|
+
const bindResult = await callSetupTool(mcpEndpoint, apiKey, {
|
|
502
|
+
action: "bind_developer_wallet",
|
|
503
|
+
walletAddress: walletAddr,
|
|
504
|
+
walletAddressConfirm: walletAddr,
|
|
505
|
+
});
|
|
506
|
+
if (bindResult) {
|
|
507
|
+
const reply = String(bindResult.reply ?? "");
|
|
508
|
+
const masterKeyMatch = reply.match(/balc_mk_[A-Za-z0-9_-]+/);
|
|
509
|
+
bindSpinner.succeed("Developer wallet bound");
|
|
510
|
+
if (masterKeyMatch) {
|
|
511
|
+
masterKey = masterKeyMatch[0];
|
|
512
|
+
printSuccess(`Master key: ${masterKey}`);
|
|
513
|
+
printInfo("\x1b[1;33mSave this master key! It cannot be shown again.\x1b[0m");
|
|
514
|
+
printInfo("Use it for: key rotation, wallet changes, bot deletion, recovery.\n");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
bindSpinner.fail("Could not bind wallet (you can do this later via chat)");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
printInfo("Skipped — you can bind a wallet later via chat.");
|
|
523
|
+
}
|
|
524
|
+
// Step B: Create trading wallets
|
|
525
|
+
printStep(6, 8, "Trading Wallets");
|
|
526
|
+
const solSpinner = spin("Creating Solana wallet...");
|
|
527
|
+
const solResult = await callSetupTool(mcpEndpoint, apiKey, {
|
|
528
|
+
action: "create_wallet",
|
|
529
|
+
chain: "solana",
|
|
530
|
+
});
|
|
531
|
+
solAddr = extractAddress(solResult);
|
|
532
|
+
solSpinner.succeed(`Solana wallet: ${solAddr}`);
|
|
533
|
+
const baseSpinner = spin("Creating Base wallet...");
|
|
534
|
+
const baseResult = await callSetupTool(mcpEndpoint, apiKey, {
|
|
535
|
+
action: "create_wallet",
|
|
536
|
+
chain: "base",
|
|
537
|
+
});
|
|
538
|
+
baseAddr = extractAddress(baseResult);
|
|
539
|
+
baseSpinner.succeed(`Base wallet: ${baseAddr}`);
|
|
540
|
+
printInfo(`\n\x1b[1;33mFund your Solana wallet (${solAddr}) with at least 0.05 SOL to start trading.\x1b[0m\n`);
|
|
541
|
+
// Step C: Configure slippage
|
|
542
|
+
printStep(7, 8, "Slippage");
|
|
543
|
+
printInfo("Default: 200bps (2%). Memecoin trading usually needs 300-500bps (3-5%).");
|
|
544
|
+
const slippageInput = await askNumber(rl, "Slippage in basis points", 300);
|
|
545
|
+
const slipSpinner = spin("Configuring slippage...");
|
|
546
|
+
await callSetupTool(mcpEndpoint, apiKey, {
|
|
547
|
+
action: "configure_slippage",
|
|
548
|
+
slippageBps: slippageInput,
|
|
549
|
+
});
|
|
550
|
+
slipSpinner.succeed(`Slippage: ${slippageInput}bps (${(slippageInput / 100).toFixed(1)}%)`);
|
|
551
|
+
// Step D: Trading strategy (natural language)
|
|
552
|
+
printStep(8, 8, "Trading Strategy");
|
|
553
|
+
printInfo("Describe your strategy in natural language. Your LLM will execute it.");
|
|
554
|
+
printInfo('Example: "PumpFun\'dan yeni tokenleri tara, hacmi 10K+ olanlari al,');
|
|
555
|
+
printInfo(' max 0.01 SOL per trade, 2x\'de yarisini sat, max 1 pozisyon"\n');
|
|
556
|
+
const strategyRules = await ask(rl, "Your strategy", "Max 0.01 SOL per trade, stop loss 30%, take profit 100%, max 1 position");
|
|
557
|
+
const stratSpinner = spin("Configuring autonomous mode (LIVE)...");
|
|
558
|
+
await callSetupTool(mcpEndpoint, apiKey, {
|
|
559
|
+
action: "configure_autonomous",
|
|
560
|
+
preset: "memecoin_sniper",
|
|
561
|
+
shadowMode: false,
|
|
562
|
+
naturalLanguageRules: strategyRules,
|
|
563
|
+
});
|
|
564
|
+
stratSpinner.succeed("Strategy configured (LIVE mode)");
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
mcpEndpoint = await ask(rl, "MCP endpoint", "https://api.balchemy.ai/mcp/YOUR_PUBLIC_ID");
|
|
568
|
+
apiKey = await askSecret(rl, "Balchemy API key");
|
|
569
|
+
publicId = mcpEndpoint.split("/").filter(Boolean).pop() ?? "unknown";
|
|
570
|
+
if (!apiKey) {
|
|
571
|
+
printError("API key is required.");
|
|
572
|
+
rl.close();
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
// Verify connection
|
|
576
|
+
const healthSpinner = spin("Verifying MCP connection...");
|
|
577
|
+
try {
|
|
578
|
+
const healthRes = await fetch(`${mcpEndpoint}/health`, {
|
|
579
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
580
|
+
});
|
|
581
|
+
const health = (await healthRes.json());
|
|
582
|
+
if (health.ok) {
|
|
583
|
+
healthSpinner.succeed("MCP connected");
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
healthSpinner.succeed("Unexpected response (continuing)");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
healthSpinner.succeed("Could not reach endpoint (continuing)");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const strategy = STRATEGIES[0];
|
|
594
|
+
const shadowMode = false;
|
|
595
|
+
// ── Save & Launch ─────────────────────────────────────────────────────
|
|
596
|
+
process.stdout.write("\n");
|
|
597
|
+
const wizardResult = {
|
|
598
|
+
provider,
|
|
599
|
+
model,
|
|
600
|
+
llmApiKey,
|
|
601
|
+
mcpEndpoint,
|
|
602
|
+
apiKey,
|
|
603
|
+
publicId,
|
|
604
|
+
strategy,
|
|
605
|
+
maxDailyLlmCost,
|
|
606
|
+
shadowMode,
|
|
607
|
+
};
|
|
608
|
+
const yamlContent = generateYaml(wizardResult);
|
|
609
|
+
const envContent = generateDotEnv(wizardResult);
|
|
610
|
+
const yamlPath = path.join(outDir, "agent.config.yaml");
|
|
611
|
+
const envPath = path.join(outDir, ".env");
|
|
612
|
+
fs.writeFileSync(yamlPath, yamlContent, "utf8");
|
|
613
|
+
fs.writeFileSync(envPath, envContent, "utf8");
|
|
614
|
+
// .gitignore
|
|
615
|
+
const gitignorePath = path.join(outDir, ".gitignore");
|
|
616
|
+
let gitignore = "";
|
|
617
|
+
if (fs.existsSync(gitignorePath)) {
|
|
618
|
+
gitignore = fs.readFileSync(gitignorePath, "utf8");
|
|
619
|
+
}
|
|
620
|
+
if (!gitignore.includes(".env")) {
|
|
621
|
+
fs.appendFileSync(gitignorePath, "\n.env\n");
|
|
622
|
+
}
|
|
623
|
+
printSuccess(`Config saved: ${yamlPath}`);
|
|
624
|
+
printSuccess(`Secrets saved: ${envPath}`);
|
|
625
|
+
// Save agent credentials for resume
|
|
626
|
+
saveAgent({
|
|
627
|
+
publicId,
|
|
628
|
+
mcpEndpoint,
|
|
629
|
+
apiKey,
|
|
630
|
+
masterKey: masterKey ?? undefined,
|
|
631
|
+
llmProvider: provider.sdkProvider,
|
|
632
|
+
llmApiKey,
|
|
633
|
+
llmModel: model.id,
|
|
634
|
+
llmBaseUrl: provider.name !== "openai" && provider.name !== "anthropic" ? provider.baseUrl : undefined,
|
|
635
|
+
maxDailyLlmCost,
|
|
636
|
+
strategy: strategy.name,
|
|
637
|
+
shadowMode,
|
|
638
|
+
wallets: { solana: solAddr, base: baseAddr },
|
|
639
|
+
createdAt: new Date().toISOString(),
|
|
640
|
+
});
|
|
641
|
+
printSuccess(`Agent cached to ~/.balchemy/agent.json`);
|
|
642
|
+
// ── Done ──────────────────────────────────────────────────────────────
|
|
643
|
+
process.stdout.write(`
|
|
644
|
+
\x1b[1;32m━━━ Setup Complete ━━━\x1b[0m
|
|
645
|
+
|
|
646
|
+
Agent: ${publicId}
|
|
647
|
+
Provider: ${provider.label}
|
|
648
|
+
Model: ${model.label}
|
|
649
|
+
Mode: LIVE
|
|
650
|
+
|
|
651
|
+
Starting agent...
|
|
652
|
+
|
|
653
|
+
`);
|
|
654
|
+
// Auto-start TUI — no extra step needed
|
|
655
|
+
{
|
|
656
|
+
rl.close();
|
|
657
|
+
const { startTui } = await import("./tui/start.js");
|
|
658
|
+
await startTui({
|
|
659
|
+
mcpEndpoint,
|
|
660
|
+
apiKey,
|
|
661
|
+
llmProvider: provider.sdkProvider,
|
|
662
|
+
llmApiKey,
|
|
663
|
+
llmModel: model.id,
|
|
664
|
+
llmBaseUrl: provider.name !== "openai" && provider.name !== "anthropic" ? provider.baseUrl : llmBaseUrlOverride,
|
|
665
|
+
maxDailyLlmCost,
|
|
666
|
+
publicId,
|
|
667
|
+
strategy: strategy.name,
|
|
668
|
+
shadowMode,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
rl.close();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// ── Browser Helper ────────────────────────────────────────────────────────────
|
|
677
|
+
function openBrowser(url) {
|
|
678
|
+
const cmd = process.platform === "darwin"
|
|
679
|
+
? `open "${url}"`
|
|
680
|
+
: process.platform === "win32"
|
|
681
|
+
? `start "${url}"`
|
|
682
|
+
: `xdg-open "${url}"`;
|
|
683
|
+
exec(cmd, () => { });
|
|
684
|
+
}
|
|
685
|
+
// ── API Key Validation ────────────────────────────────────────────────────────
|
|
686
|
+
async function validateApiKey(provider, apiKey) {
|
|
687
|
+
try {
|
|
688
|
+
if (provider.sdkProvider === "anthropic") {
|
|
689
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: {
|
|
692
|
+
"Content-Type": "application/json",
|
|
693
|
+
"x-api-key": apiKey,
|
|
694
|
+
"anthropic-version": "2023-06-01",
|
|
695
|
+
},
|
|
696
|
+
body: JSON.stringify({
|
|
697
|
+
model: "claude-haiku-4-5-20251001",
|
|
698
|
+
max_tokens: 1,
|
|
699
|
+
messages: [{ role: "user", content: "hi" }],
|
|
700
|
+
}),
|
|
701
|
+
});
|
|
702
|
+
// 200 = valid, 401 = invalid, anything else = network issue
|
|
703
|
+
return res.status !== 401;
|
|
704
|
+
}
|
|
705
|
+
// OpenAI-compatible providers
|
|
706
|
+
const url = `${provider.baseUrl}/models`;
|
|
707
|
+
const res = await fetch(url.endsWith("/models") ? url : `${url}/models`, {
|
|
708
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
709
|
+
});
|
|
710
|
+
return res.status !== 401;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return false; // Network error, don't block
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
//# sourceMappingURL=wizard.js.map
|