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.
Files changed (67) hide show
  1. package/README.md +59 -0
  2. package/assets/bcrow.png +0 -0
  3. package/dist/agent-store.d.ts +40 -0
  4. package/dist/agent-store.d.ts.map +1 -0
  5. package/dist/agent-store.js +206 -0
  6. package/dist/agent-store.js.map +1 -0
  7. package/dist/config-loader.d.ts +8 -0
  8. package/dist/config-loader.d.ts.map +1 -0
  9. package/dist/config-loader.js +106 -0
  10. package/dist/config-loader.js.map +1 -0
  11. package/dist/docker-gen.d.ts +6 -0
  12. package/dist/docker-gen.d.ts.map +1 -0
  13. package/dist/docker-gen.js +40 -0
  14. package/dist/docker-gen.js.map +1 -0
  15. package/dist/index.d.ts +16 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +143 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/openai-oauth.d.ts +28 -0
  20. package/dist/openai-oauth.d.ts.map +1 -0
  21. package/dist/openai-oauth.js +215 -0
  22. package/dist/openai-oauth.js.map +1 -0
  23. package/dist/runner.d.ts +6 -0
  24. package/dist/runner.d.ts.map +1 -0
  25. package/dist/runner.js +63 -0
  26. package/dist/runner.js.map +1 -0
  27. package/dist/terminal-logo.d.ts +15 -0
  28. package/dist/terminal-logo.d.ts.map +1 -0
  29. package/dist/terminal-logo.js +121 -0
  30. package/dist/terminal-logo.js.map +1 -0
  31. package/dist/tui/AgentBridge.d.ts +35 -0
  32. package/dist/tui/AgentBridge.d.ts.map +1 -0
  33. package/dist/tui/AgentBridge.js +235 -0
  34. package/dist/tui/AgentBridge.js.map +1 -0
  35. package/dist/tui/App.d.ts +8 -0
  36. package/dist/tui/App.d.ts.map +1 -0
  37. package/dist/tui/App.js +118 -0
  38. package/dist/tui/App.js.map +1 -0
  39. package/dist/tui/ChatAgent.d.ts +41 -0
  40. package/dist/tui/ChatAgent.d.ts.map +1 -0
  41. package/dist/tui/ChatAgent.js +312 -0
  42. package/dist/tui/ChatAgent.js.map +1 -0
  43. package/dist/tui/ChatPanel.d.ts +10 -0
  44. package/dist/tui/ChatPanel.d.ts.map +1 -0
  45. package/dist/tui/ChatPanel.js +43 -0
  46. package/dist/tui/ChatPanel.js.map +1 -0
  47. package/dist/tui/StatusPanel.d.ts +8 -0
  48. package/dist/tui/StatusPanel.d.ts.map +1 -0
  49. package/dist/tui/StatusPanel.js +25 -0
  50. package/dist/tui/StatusPanel.js.map +1 -0
  51. package/dist/tui/start.d.ts +3 -0
  52. package/dist/tui/start.d.ts.map +1 -0
  53. package/dist/tui/start.js +14 -0
  54. package/dist/tui/start.js.map +1 -0
  55. package/dist/tui/types.d.ts +61 -0
  56. package/dist/tui/types.d.ts.map +1 -0
  57. package/dist/tui/types.js +3 -0
  58. package/dist/tui/types.js.map +1 -0
  59. package/dist/wizard.d.ts +16 -0
  60. package/dist/wizard.d.ts.map +1 -0
  61. package/dist/wizard.js +716 -0
  62. package/dist/wizard.js.map +1 -0
  63. package/package.json +57 -0
  64. package/templates/.env.example +19 -0
  65. package/templates/Dockerfile +20 -0
  66. package/templates/agent.config.yaml +71 -0
  67. 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