@vault77/summon 2.0.1

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/summon-cli.js ADDED
@@ -0,0 +1,797 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import {
4
+ getConfigPath,
5
+ loadConfig,
6
+ saveConfig,
7
+ editConfig,
8
+ CONFIG_KEYS,
9
+ DEFAULT_CONFIG,
10
+ parseConfigValue,
11
+ normalizeConfigValue,
12
+ PRIORITY_FEE_LEVELS,
13
+ TX_VERSIONS,
14
+ } from "./lib/config.js";
15
+ import { storePrivateKey, getPrivateKey, deletePrivateKey, hasPrivateKey } from "./utils/keychain.js";
16
+ import readline from "readline";
17
+ import { notify } from "./utils/notify.js";
18
+ import { runDoctor } from "./lib/doctor.js";
19
+
20
+ const program = new Command();
21
+ program
22
+ .name("summon")
23
+ .description("Summon Solana CLI")
24
+ .showHelpAfterError(); // show help after invalid flags/args
25
+
26
+ const CONFIG_KEY_SET = new Set([
27
+ ...CONFIG_KEYS.filter((key) => key !== "jito"),
28
+ "jito.enabled",
29
+ "jito.tip",
30
+ ]);
31
+ const CONFIG_HELP = [
32
+ { key: "rpcUrl", type: "string", note: "RPC URL (advancedTx=true is enforced)" },
33
+ { key: "slippage", type: "number | auto", note: "Max slippage percentage" },
34
+ { key: "priorityFee", type: "number | auto", note: "Priority fee in SOL" },
35
+ {
36
+ key: "priorityFeeLevel",
37
+ type: PRIORITY_FEE_LEVELS.join(" | "),
38
+ note: "Required when priorityFee=auto",
39
+ },
40
+ { key: "txVersion", type: TX_VERSIONS.join(" | "), note: "Transaction version" },
41
+ { key: "showQuoteDetails", type: "true | false", note: "Print quote details after swaps" },
42
+ { key: "DEBUG_MODE", type: "true | false", note: "Enable verbose SDK logs" },
43
+ { key: "notificationsEnabled", type: "true | false", note: "Enable macOS notifications" },
44
+ { key: "jito.enabled", type: "true | false", note: "Enable Jito bundles" },
45
+ { key: "jito.tip", type: "number", note: "Tip in SOL when Jito enabled" },
46
+ ];
47
+
48
+ const askQuestion = (rl, prompt) =>
49
+ new Promise((resolve) => rl.question(prompt, (answer) => resolve(answer.trim())));
50
+
51
+ const COLOR_ENABLED = process.stdout.isTTY;
52
+ const ANSI = {
53
+ reset: "\x1b[0m",
54
+ blue: "\x1b[34m",
55
+ purple: "\x1b[35m",
56
+ green: "\x1b[32m",
57
+ yellow: "\x1b[33m",
58
+ red: "\x1b[31m",
59
+ };
60
+
61
+ const paint = (text, color) => (COLOR_ENABLED ? `${color}${text}${ANSI.reset}` : text);
62
+
63
+ function clearScreen() {
64
+ if (process.stdout.isTTY) {
65
+ process.stdout.write("\x1Bc");
66
+ } else {
67
+ console.clear();
68
+ }
69
+ }
70
+
71
+ function renderWizardHeader() {
72
+ console.log("⚙️ Config Wizard");
73
+ console.log("Press Enter to keep the current value.\n");
74
+ }
75
+
76
+ function toDisplayValue(value) {
77
+ if (value === undefined || value === null) return "";
78
+ if (typeof value === "object") return JSON.stringify(value);
79
+ return String(value);
80
+ }
81
+
82
+ function formatBox({ title, rows }) {
83
+ const normalizedRows = rows.map(([label, value]) => [String(label), String(value)]);
84
+ const labelWidth = Math.max(...normalizedRows.map(([label]) => label.length), 0);
85
+ const valueWidth = Math.max(...normalizedRows.map(([, value]) => value.length), 0);
86
+ const titleText = title ? ` ${title} ` : "";
87
+ const innerWidth = Math.max(labelWidth + 3 + valueWidth, titleText.length);
88
+ const totalWidth = innerWidth + 2;
89
+
90
+ let topBorder = `┌${"─".repeat(totalWidth)}┐`;
91
+ if (titleText) {
92
+ const left = Math.floor((totalWidth - titleText.length) / 2);
93
+ const right = totalWidth - titleText.length - left;
94
+ topBorder = `┌${"─".repeat(left)}${paint(titleText, ANSI.purple)}${"─".repeat(right)}┐`;
95
+ }
96
+
97
+ const lines = normalizedRows.map(([label, value]) => {
98
+ const labelText = label.padEnd(labelWidth);
99
+ const content = `${paint(labelText, ANSI.blue)} : ${paint(value, ANSI.green)}`;
100
+ const contentLength = labelText.length + 3 + value.length;
101
+ const padding = " ".repeat(Math.max(0, innerWidth - contentLength));
102
+ return `│ ${content}${padding} │`;
103
+ });
104
+
105
+ const bottomBorder = `└${"─".repeat(totalWidth)}┘`;
106
+ return [topBorder, ...lines, bottomBorder].join("\n");
107
+ }
108
+
109
+ function formatPlainBox({ title, rows }) {
110
+ const normalizedRows = rows.map(([label, value]) => [String(label), String(value)]);
111
+ const labelWidth = Math.max(...normalizedRows.map(([label]) => label.length), 0);
112
+ const valueWidth = Math.max(...normalizedRows.map(([, value]) => value.length), 0);
113
+ const titleText = title ? ` ${title} ` : "";
114
+ const innerWidth = Math.max(labelWidth + 3 + valueWidth, titleText.length);
115
+ const totalWidth = innerWidth + 2;
116
+
117
+ let topBorder = `┌${"─".repeat(totalWidth)}┐`;
118
+ if (titleText) {
119
+ const left = Math.floor((totalWidth - titleText.length) / 2);
120
+ const right = totalWidth - titleText.length - left;
121
+ topBorder = `┌${"─".repeat(left)}${titleText}${"─".repeat(right)}┐`;
122
+ }
123
+
124
+ const lines = normalizedRows.map(([label, value]) => {
125
+ const labelText = label.padEnd(labelWidth);
126
+ const content = `${labelText} : ${value}`;
127
+ const contentLength = labelText.length + 3 + value.length;
128
+ const padding = " ".repeat(Math.max(0, innerWidth - contentLength));
129
+ return `│ ${content}${padding} │`;
130
+ });
131
+
132
+ const bottomBorder = `└${"─".repeat(totalWidth)}┘`;
133
+ return [topBorder, ...lines, bottomBorder].join("\n");
134
+ }
135
+
136
+ function renderStatusBox({ title, rows, tone }) {
137
+ const box = formatPlainBox({ title, rows });
138
+ const colored = box
139
+ .split("\n")
140
+ .map((line) => paint(line, tone))
141
+ .join("\n");
142
+ console.log(colored);
143
+ }
144
+
145
+ function renderConfigSummary(cfg, configPath, title = "CONFIG") {
146
+ const jitoEnabled = cfg.jito?.enabled ? "true" : "false";
147
+ const jitoTip = cfg.jito?.enabled ? cfg.jito.tip : "-";
148
+ const rows = [
149
+ ["Config path", configPath],
150
+ ["RPC URL", cfg.rpcUrl],
151
+ ["Slippage", cfg.slippage],
152
+ ["Priority fee", cfg.priorityFee],
153
+ ["Priority level", cfg.priorityFeeLevel],
154
+ ["Tx version", cfg.txVersion],
155
+ ["Show quote", cfg.showQuoteDetails],
156
+ ["Debug mode", cfg.DEBUG_MODE],
157
+ ["Notifications", cfg.notificationsEnabled],
158
+ ["Jito enabled", jitoEnabled],
159
+ ["Jito tip (SOL)", jitoTip],
160
+ ];
161
+ console.log(formatBox({ title, rows }));
162
+ }
163
+
164
+ async function promptSelect(rl, label, options, { current, required = false } = {}) {
165
+ const menu = options.map((opt, index) => ` ${index + 1}) ${opt}`).join("\n");
166
+ while (true) {
167
+ console.log(`\n${label}`);
168
+ console.log(menu);
169
+ const suffix = current ? ` [${current}]` : "";
170
+ const answer = await askQuestion(rl, `Select${suffix}: `);
171
+ if (!answer) {
172
+ if (required) {
173
+ console.log("⚠️ Selection required.");
174
+ continue;
175
+ }
176
+ return current;
177
+ }
178
+ const normalized = answer.trim();
179
+ const index = Number(normalized);
180
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
181
+ return options[index - 1];
182
+ }
183
+ const match = options.find((opt) => opt.toLowerCase() === normalized.toLowerCase());
184
+ if (match) return match;
185
+ console.log("⚠️ Invalid selection. Choose a number or value from the list.");
186
+ }
187
+ }
188
+
189
+ async function promptNormalized(rl, label, key, { current, required = false } = {}) {
190
+ while (true) {
191
+ const suffix = current !== undefined ? ` [${toDisplayValue(current)}]` : "";
192
+ const answer = await askQuestion(rl, `${label}${suffix}: `);
193
+ if (!answer) {
194
+ if (required) {
195
+ console.log("⚠️ Value required.");
196
+ continue;
197
+ }
198
+ return current;
199
+ }
200
+ try {
201
+ return normalizeConfigValue(key, parseConfigValue(answer), { strict: true });
202
+ } catch (err) {
203
+ console.log(`⚠️ ${err.message}`);
204
+ }
205
+ }
206
+ }
207
+
208
+ async function promptNumber(rl, label, { current, required = false } = {}) {
209
+ while (true) {
210
+ const suffix = current !== undefined ? ` [${toDisplayValue(current)}]` : "";
211
+ const answer = await askQuestion(rl, `${label}${suffix}: `);
212
+ if (!answer) {
213
+ if (required) {
214
+ console.log("⚠️ Value required.");
215
+ continue;
216
+ }
217
+ return current;
218
+ }
219
+ const num = Number(answer);
220
+ if (Number.isFinite(num) && num >= 0) {
221
+ return num;
222
+ }
223
+ console.log("⚠️ Invalid number. Use a non-negative value.");
224
+ }
225
+ }
226
+
227
+ async function runConfigWizard({ cfg, rl }) {
228
+ const nextCfg = { ...cfg, jito: { ...DEFAULT_CONFIG.jito, ...(cfg.jito || {}) } };
229
+
230
+ clearScreen();
231
+ renderWizardHeader();
232
+ console.log("RPC URL should be the SolanaTracker endpoint assigned to you.");
233
+ console.log("advancedTx=true is enforced automatically.\n");
234
+ nextCfg.rpcUrl = await promptNormalized(rl, "RPC URL", "rpcUrl", { current: nextCfg.rpcUrl });
235
+
236
+ clearScreen();
237
+ renderWizardHeader();
238
+ nextCfg.slippage = await promptNormalized(rl, "Max slippage (number or \"auto\")", "slippage", {
239
+ current: nextCfg.slippage,
240
+ });
241
+
242
+ clearScreen();
243
+ renderWizardHeader();
244
+ nextCfg.priorityFee = await promptNormalized(rl, "Priority fee (number or \"auto\")", "priorityFee", {
245
+ current: nextCfg.priorityFee,
246
+ });
247
+
248
+ clearScreen();
249
+ renderWizardHeader();
250
+ nextCfg.priorityFeeLevel = await promptSelect(
251
+ rl,
252
+ "Priority fee level (used when priorityFee is auto)",
253
+ PRIORITY_FEE_LEVELS,
254
+ {
255
+ current: nextCfg.priorityFeeLevel,
256
+ required: true,
257
+ }
258
+ );
259
+
260
+ clearScreen();
261
+ renderWizardHeader();
262
+ nextCfg.txVersion = await promptSelect(rl, "Transaction version", TX_VERSIONS, {
263
+ current: nextCfg.txVersion,
264
+ });
265
+
266
+ clearScreen();
267
+ renderWizardHeader();
268
+ const showQuoteDetails = await promptSelect(rl, "Show quote details", ["true", "false"], {
269
+ current: nextCfg.showQuoteDetails ? "true" : "false",
270
+ });
271
+ nextCfg.showQuoteDetails = showQuoteDetails === "true";
272
+
273
+ clearScreen();
274
+ renderWizardHeader();
275
+ const debugMode = await promptSelect(rl, "Enable debug mode", ["true", "false"], {
276
+ current: nextCfg.DEBUG_MODE ? "true" : "false",
277
+ });
278
+ nextCfg.DEBUG_MODE = debugMode === "true";
279
+
280
+ clearScreen();
281
+ renderWizardHeader();
282
+ const notificationsEnabled = await promptSelect(rl, "Enable notifications", ["true", "false"], {
283
+ current: nextCfg.notificationsEnabled ? "true" : "false",
284
+ });
285
+ nextCfg.notificationsEnabled = notificationsEnabled === "true";
286
+
287
+ clearScreen();
288
+ renderWizardHeader();
289
+ const jitoEnabled = await promptSelect(rl, "Enable Jito bundles", ["true", "false"], {
290
+ current: nextCfg.jito.enabled ? "true" : "false",
291
+ });
292
+ nextCfg.jito.enabled = jitoEnabled === "true";
293
+ if (nextCfg.jito.enabled) {
294
+ clearScreen();
295
+ renderWizardHeader();
296
+ const requireTip = nextCfg.jito.tip === undefined || nextCfg.jito.tip === null;
297
+ nextCfg.jito.tip = await promptNumber(rl, "Jito tip (SOL)", {
298
+ current: nextCfg.jito.tip,
299
+ required: requireTip,
300
+ });
301
+ }
302
+
303
+ return nextCfg;
304
+ }
305
+
306
+ let tradeModulePromise;
307
+ const getTradeModule = async () => {
308
+ if (!tradeModulePromise) {
309
+ tradeModulePromise = import("./lib/trades.js");
310
+ }
311
+ return tradeModulePromise;
312
+ };
313
+
314
+ async function executeTrade(type, mint, amountArg) {
315
+ const cfg = await loadConfig();
316
+
317
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) {
318
+ console.error("⚠️ Invalid mint format. Expected base58 address (32–44 chars).");
319
+ process.exit(1);
320
+ }
321
+
322
+ let amountParam = amountArg.toString().trim().toLowerCase().replace(/\s+/g, "");
323
+
324
+ if (amountParam !== "auto" && !amountParam.endsWith("%")) {
325
+ const num = parseFloat(amountParam);
326
+ if (isNaN(num) || num <= 0) {
327
+ console.error("⚠️ Invalid amount. Use a positive number, 'auto' during a sell, or '<percent>%'.");
328
+ process.exit(1);
329
+ }
330
+ amountParam = num;
331
+ }
332
+
333
+ try {
334
+ const mintDisplay = `${mint.slice(0, 4)}…${mint.slice(-4)}`;
335
+ const amountDisplay = String(amountParam);
336
+ const baseRows = [
337
+ ["Action", type === "buy" ? "Buy" : "Sell"],
338
+ ["Mint", mintDisplay],
339
+ ["Amount", amountDisplay],
340
+ ];
341
+ clearScreen();
342
+ renderStatusBox({
343
+ title: "PENDING",
344
+ tone: ANSI.yellow,
345
+ rows: [
346
+ ...baseRows,
347
+ ["TXID", "-"],
348
+ ["Explorer", "-"],
349
+ ["Info", "Submitting swap..."],
350
+ ],
351
+ });
352
+
353
+ if (type === "buy") {
354
+ if (amountParam === "auto") {
355
+ console.error("⚠️ Buying with 'auto' isn’t supported. Use a number or '<percent>%'.");
356
+ process.exit(1);
357
+ }
358
+
359
+ const { buyToken } = await getTradeModule();
360
+ const result = await buyToken(mint, amountParam);
361
+ clearScreen();
362
+ const info = `Received ${result.tokensReceivedDecimal} tokens | Fees ${result.totalFees} | Impact ${result.priceImpact}`;
363
+ const buyRows = [
364
+ ...baseRows,
365
+ ["TXID", result.txid],
366
+ ["Explorer", `https://orbmarkets.io/tx/${result.txid}`],
367
+ ["Info", info],
368
+ ["Verification", result.verificationStatus],
369
+ ];
370
+ renderStatusBox({ title: "SUCCESS", rows: buyRows, tone: ANSI.green });
371
+ if (cfg.showQuoteDetails) {
372
+ console.log(` • Quote Details : ${JSON.stringify(result.quote, null, 2)}`);
373
+ }
374
+ } else if (type === "sell") {
375
+ const { sellToken } = await getTradeModule();
376
+ const result = await sellToken(mint, amountParam);
377
+ clearScreen();
378
+ const info = `Received ${result.solReceivedDecimal} SOL | Fees ${result.totalFees} | Impact ${result.priceImpact}`;
379
+ const sellRows = [
380
+ ...baseRows,
381
+ ["TXID", result.txid],
382
+ ["Explorer", `https://orbmarkets.io/tx/${result.txid}`],
383
+ ["Info", info],
384
+ ["Verification", result.verificationStatus],
385
+ ];
386
+ renderStatusBox({ title: "SUCCESS", rows: sellRows, tone: ANSI.green });
387
+ if (cfg.showQuoteDetails) {
388
+ console.log(` • Quote Details : ${JSON.stringify(result.quote, null, 2)}`);
389
+ }
390
+ }
391
+ process.exit(0);
392
+ } catch (err) {
393
+ clearScreen();
394
+ const errorMessage = err?.message || "Unknown error";
395
+ const txidMatch = errorMessage.match(/[1-9A-HJ-NP-Za-km-z]{32,}/);
396
+ const txid = txidMatch ? txidMatch[0] : "-";
397
+ const explorer = txidMatch ? `https://orbmarkets.io/tx/${txid}` : "-";
398
+ const mintDisplay = `${mint.slice(0, 4)}…${mint.slice(-4)}`;
399
+ const amountDisplay = String(amountParam);
400
+ renderStatusBox({
401
+ title: "FAILED",
402
+ tone: ANSI.red,
403
+ rows: [
404
+ ["Action", type === "buy" ? "Buy" : "Sell"],
405
+ ["Mint", mintDisplay],
406
+ ["Amount", amountDisplay],
407
+ ["TXID", txid],
408
+ ["Explorer", explorer],
409
+ ["Error", errorMessage],
410
+ ],
411
+ });
412
+ process.exit(1);
413
+ }
414
+ }
415
+
416
+ // CONFIG subcommands
417
+ const configCmd = program.command("config").description("Manage CLI configuration");
418
+
419
+ configCmd
420
+ .command("view")
421
+ .description("Show current config")
422
+ .action(async () => {
423
+ const configPath = getConfigPath();
424
+ const cfg = await loadConfig();
425
+ console.log(`Config file: ${configPath}\n`);
426
+ renderConfigSummary(cfg, configPath);
427
+ });
428
+
429
+ configCmd
430
+ .command("edit")
431
+ .description("Edit config in your $EDITOR")
432
+ .action(async () => {
433
+ await editConfig();
434
+ });
435
+
436
+ configCmd
437
+ .command("set <key> <value>")
438
+ .description("Set a single config key")
439
+ .action(async (key, value) => {
440
+ const configPath = getConfigPath();
441
+ const cfg = await loadConfig();
442
+ const parsedValue = parseConfigValue(value);
443
+ if (!CONFIG_KEY_SET.has(key)) {
444
+ console.error(`⚠️ Unknown config key: ${key}`);
445
+ console.error("Run `summon config list` to see valid keys.");
446
+ process.exit(1);
447
+ }
448
+ try {
449
+ if (key.startsWith("jito.")) {
450
+ const field = key.split(".")[1];
451
+ const nextJito = { ...(cfg.jito || DEFAULT_CONFIG.jito), [field]: parsedValue };
452
+ cfg.jito = normalizeConfigValue("jito", nextJito, { strict: true });
453
+ } else {
454
+ const normalizedValue = normalizeConfigValue(key, parsedValue, { strict: true });
455
+ cfg[key] = normalizedValue;
456
+ if (key === "priorityFee" && normalizedValue === "auto") {
457
+ console.log(
458
+ `ℹ️ priorityFeeLevel is required when priorityFee is auto. Current level: ${cfg.priorityFeeLevel}`
459
+ );
460
+ }
461
+ }
462
+ } catch (err) {
463
+ console.error(`⚠️ ${err.message}`);
464
+ process.exit(1);
465
+ }
466
+ await saveConfig(cfg);
467
+ console.log(`✅ Updated ${key} → ${value} in ${configPath}`);
468
+ renderConfigSummary(cfg, configPath);
469
+ });
470
+
471
+ configCmd
472
+ .command("list")
473
+ .description("List available config keys and types")
474
+ .action(() => {
475
+ console.log("Available config keys:");
476
+ for (const entry of CONFIG_HELP) {
477
+ console.log(` • ${entry.key} (${entry.type}) — ${entry.note}`);
478
+ }
479
+ });
480
+
481
+ configCmd
482
+ .command("wizard")
483
+ .description("Interactive config editor with type validation")
484
+ .action(async () => {
485
+ const rl = readline.createInterface({
486
+ input: process.stdin,
487
+ output: process.stdout,
488
+ });
489
+ const cfg = await loadConfig();
490
+ const updated = await runConfigWizard({ cfg, rl });
491
+ await saveConfig(updated);
492
+ rl.close();
493
+ const configPath = getConfigPath();
494
+ console.log("✅ Config updated.");
495
+ renderConfigSummary(updated, configPath);
496
+ });
497
+
498
+ // SETUP command – interactive setup wizard
499
+ program
500
+ .command("setup")
501
+ .description("Run interactive setup for config and keychain")
502
+ .action(async () => {
503
+ const rl = readline.createInterface({
504
+ input: process.stdin,
505
+ output: process.stdout,
506
+ });
507
+
508
+ const configPath = getConfigPath();
509
+ const cfg = await loadConfig();
510
+
511
+ console.log("⚙️ Summon CLI Setup\n");
512
+ const updated = await runConfigWizard({ cfg, rl });
513
+ await saveConfig(updated);
514
+ console.log(`✅ Config saved to ${configPath}`);
515
+
516
+ // Private key
517
+ try {
518
+ if (await hasPrivateKey()) {
519
+ const updateKey = await askQuestion(
520
+ rl,
521
+ "🔓 Private key already stored in Keychain. Would you like to replace it? (y/N): "
522
+ );
523
+ if (updateKey.toLowerCase() === "y") {
524
+ const privKey = await askQuestion(rl, "Paste your new private key: ");
525
+ await storePrivateKey(privKey);
526
+ console.log("🔐 Private key updated.");
527
+ } else {
528
+ console.log("✅ Keeping existing private key.");
529
+ }
530
+ } else {
531
+ const storeKey = await askQuestion(
532
+ rl,
533
+ "Would you like to store your private key in the macOS Keychain now? (y/N): "
534
+ );
535
+ if (storeKey.toLowerCase() === "y") {
536
+ const privKey = await askQuestion(rl, "Paste your private key: ");
537
+ await storePrivateKey(privKey);
538
+ console.log("🔐 Private key stored securely.");
539
+ } else {
540
+ console.log("⚠️ No private key stored. You can add one later with `summon keychain store`.");
541
+ }
542
+ }
543
+ } catch (e) {
544
+ console.error("❌ Keychain error:", e.message);
545
+ }
546
+
547
+ rl.close();
548
+ console.log("🧠 Setup complete.");
549
+
550
+ // Test macOS notifications so users can allow permissions now
551
+ if (updated.notificationsEnabled !== false) {
552
+ try {
553
+ notify({
554
+ title: "summonTheWarlord",
555
+ subtitle: "Setup complete",
556
+ message: "If you see this, notifications are enabled.",
557
+ sound: "Ping",
558
+ });
559
+ console.log("🔔 Test notification sent. If you see it, notifications are enabled.");
560
+ } catch {
561
+ console.warn("⚠️ Unable to send test notification. You may need to enable notifications for your terminal.");
562
+ }
563
+ } else {
564
+ console.log("🔕 Notifications are disabled in config.");
565
+ }
566
+ });
567
+
568
+ // KEYCHAIN subcommands
569
+ const keychainCmd = program.command("keychain").description("Manage private key storage in macOS Keychain");
570
+
571
+ keychainCmd
572
+ .command("store")
573
+ .description("Store private key securely in macOS Keychain")
574
+ .action(() => {
575
+ const rl = readline.createInterface({
576
+ input: process.stdin,
577
+ output: process.stdout,
578
+ });
579
+ rl.question("Paste your wallet private key: ", async (input) => {
580
+ rl.close();
581
+ try {
582
+ await storePrivateKey(input.trim());
583
+ console.log("🔐 Private key securely stored in macOS Keychain.");
584
+ } catch (err) {
585
+ console.error("❌ Failed to store key:", err.message);
586
+ process.exitCode = 1;
587
+ }
588
+ });
589
+ });
590
+
591
+ keychainCmd
592
+ .command("unlock")
593
+ .description("Test retrieval of private key from macOS Keychain")
594
+ .action(async () => {
595
+ try {
596
+ const key = await getPrivateKey();
597
+ if (key) console.log("🔓 Private key retrieved successfully.");
598
+ } catch (err) {
599
+ console.error("❌ Failed to retrieve key:", err.message);
600
+ }
601
+ });
602
+
603
+ keychainCmd
604
+ .command("delete")
605
+ .description("Delete the private key from macOS Keychain")
606
+ .action(async () => {
607
+ await deletePrivateKey();
608
+ console.log("💥 Private key deleted from macOS Keychain.");
609
+ });
610
+
611
+ program
612
+ .command("buy <mint> <amount>")
613
+ .description("Buy a token with SOL")
614
+ .action(async (mint, amount) => {
615
+ await executeTrade("buy", mint, amount);
616
+ });
617
+
618
+ program
619
+ .command("sell <mint> <amount>")
620
+ .description("Sell a token for SOL")
621
+ .action(async (mint, amount) => {
622
+ await executeTrade("sell", mint, amount);
623
+ });
624
+
625
+ // Trade command with options for buy and sell (deprecated)
626
+ program
627
+ .command("trade <mint>", { hidden: true })
628
+ .description("DEPRECATED: Trade a specific token")
629
+ .option("-b, --buy <amount>", "Spend <amount> SOL (number or '<percent>%') to buy token")
630
+ .option("-s, --sell <amount>", "Sell <amount> tokens (number, 'auto', or '<percent>%')")
631
+ .action(async (mint, options) => {
632
+ console.log("⚠️ 'summon trade' is deprecated. Use 'summon buy' or 'summon sell' instead.");
633
+ if (options.buy) {
634
+ await executeTrade("buy", mint, options.buy);
635
+ } else if (options.sell) {
636
+ await executeTrade("sell", mint, options.sell);
637
+ } else {
638
+ console.log("⚠️ Please specify --buy <amount> or --sell <amount>");
639
+ process.exit(1);
640
+ }
641
+ });
642
+
643
+ program
644
+ .command("wallet")
645
+ .alias("w")
646
+ .description("Open your wallet in the browser via SolanaTracker.io")
647
+ .action(async () => {
648
+ // Lazy-load heavier deps only when wallet command runs
649
+ const [{ Keypair }, { default: bs58 }, { default: open }] = await Promise.all([
650
+ import("@solana/web3.js"),
651
+ import("bs58"),
652
+ import("open"),
653
+ ]);
654
+ try {
655
+ const rawKey = await getPrivateKey();
656
+ let keypair;
657
+
658
+ try {
659
+ // Try base58 format
660
+ const bytes = bs58.decode(rawKey);
661
+ keypair = Keypair.fromSecretKey(bytes);
662
+ } catch (err) {
663
+ try {
664
+ // Try JSON array format
665
+ const arr = JSON.parse(rawKey);
666
+ if (!Array.isArray(arr)) throw new Error("Not an array");
667
+ keypair = Keypair.fromSecretKey(Uint8Array.from(arr));
668
+ } catch (jsonErr) {
669
+ throw new Error("Private key is neither base58 nor valid JSON array.");
670
+ }
671
+ }
672
+
673
+ const pubkey = keypair.publicKey.toBase58();
674
+ const url = `https://www.solanatracker.io/wallet/${pubkey}`;
675
+ console.log(`🌐 Opening wallet in browser: ${url}`);
676
+ await open(url);
677
+ } catch (err) {
678
+ console.error("❌ Failed to load key from Keychain:", err.message);
679
+ }
680
+ });
681
+
682
+ // DOCTOR command
683
+ program
684
+ .command("doctor")
685
+ .description("Run environment and connectivity checks")
686
+ .option("-v, --verbose", "Show verbose output")
687
+ .action(async (options) => {
688
+ const results = await runDoctor({ verbose: Boolean(options.verbose) });
689
+ for (const result of results) {
690
+ const icon = result.status === "ok" ? "✅" : result.status === "skip" ? "⚠️" : "❌";
691
+ console.log(`${icon} ${result.name}: ${result.message}`);
692
+ if (options.verbose && result.details) {
693
+ console.log(` • ${result.details}`);
694
+ }
695
+ }
696
+ const hasFailure = results.some((item) => item.status === "fail");
697
+ process.exit(hasFailure ? 1 : 0);
698
+ });
699
+
700
+ // MANUAL command
701
+ program
702
+ .command("man")
703
+ .alias("m")
704
+ .description("Display usage and help information")
705
+ .action(() => {
706
+ console.log(`
707
+ 📖 Summon CLI Manual
708
+
709
+ FIRST TIME QUICKSTART:
710
+ 1) summon setup
711
+ Saves config + stores your private key in Keychain.
712
+ 2) summon config wizard
713
+ Review RPC, fees, slippage, notifications, and Jito.
714
+ 3) summon doctor
715
+ Confirms RPC + swap API are healthy.
716
+ 4) summon buy <mint> 0.01
717
+ Start small while you learn.
718
+
719
+ TERMS:
720
+ • Mint = token address (base58). Copy it from a Solana explorer or DEX listing.
721
+ • Amounts:
722
+ - Buy uses SOL amount (e.g. 0.1)
723
+ - Sell uses token amount, percent (50%), or auto for full balance
724
+
725
+ USAGE:
726
+ summon setup
727
+ Run initial setup wizard (RPC, slippage, priority fees, Jito, etc.)
728
+
729
+ summon config view
730
+ View current configuration
731
+
732
+ summon config edit
733
+ Edit config in your $EDITOR
734
+
735
+ summon config set <key> <value>
736
+ Set a single config key
737
+
738
+ summon config wizard
739
+ Interactive config editor with type validation
740
+
741
+ summon config list
742
+ List available config keys and types
743
+
744
+ summon keychain store
745
+ Store your private key in the macOS Keychain (recommended)
746
+ • Paste either a base58-encoded string OR a JSON array like [12, 34, ...]
747
+
748
+ summon keychain unlock
749
+ Retrieve and verify your stored key
750
+
751
+ summon keychain delete
752
+ Delete the private key from macOS Keychain
753
+
754
+ summon buy <mint> <amount>
755
+ summon sell <mint> <amount>
756
+ Buy or sell a token. Amount formats:
757
+ • Fixed amount (e.g. 0.5 or 100)
758
+ • Percent of holdings (e.g. 50%)
759
+ • "auto" (sell only — sells your full balance)
760
+
761
+ summon wallet
762
+ Open your wallet on SolanaTracker.io
763
+
764
+ summon doctor
765
+ Run diagnostics for config, Keychain, RPC, swap API, and notifications
766
+
767
+ summon man
768
+ Display this manual
769
+
770
+ NOTES:
771
+ • This tool relies on SolanaTracker.io as its backend and won't work without them.
772
+ You can use the default RPC URL, but may see errors and issues because it’s free & public.
773
+ Signup for a free account here: https://www.solanatracker.io/solana-rpc
774
+ Use the new URL you are assigned in the config file.
775
+ • You may see errors about rate limits. This is largely due to using the free endpoint,
776
+ but they do happen occasionally. Your trade may still go through because those errors happen
777
+ while we're waiting for trade confirmation.
778
+ • Use summon buy or summon sell for trades
779
+ • Buying with "auto" is NOT supported — use a number or percent
780
+ • Your private key is never stored in plain text — use the Keychain for secure access
781
+ • Notifications are optional. Toggle notificationsEnabled in config if you want silence.
782
+ • Swaps show Pending → Success/Failed panes. If Verification is pending, open:
783
+ https://orbmarkets.io/tx/<txid>
784
+ • Quote details can be toggled in config or during setup
785
+ • Always confirm transactions via returned TXID and fees
786
+
787
+ Enjoy the chaos. 🪖
788
+ `);
789
+ });
790
+
791
+ // If no subcommand provided, show help
792
+ if (!process.argv.slice(2).length) {
793
+ program.outputHelp();
794
+ process.exit(0);
795
+ }
796
+
797
+ program.parse(process.argv);