kinetic-mcp 1.1.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/build/index.js +15 -0
- package/build/index.js.map +1 -1
- package/package.json +5 -1
- package/scripts/setup.mjs +599 -263
package/README.md
CHANGED
|
@@ -18,11 +18,11 @@ npx kinetic-mcp setup
|
|
|
18
18
|
|
|
19
19
|
Add this to your Claude Desktop config file and restart Claude Desktop:
|
|
20
20
|
|
|
21
|
-
| Platform | Config file path
|
|
22
|
-
| -------- |
|
|
21
|
+
| Platform | Config file path |
|
|
22
|
+
| -------- | ----------------------------------------------------------------- |
|
|
23
23
|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
24
|
-
| Windows | `%APPDATA%\Claude\claude_desktop_config.json`
|
|
25
|
-
| Linux | `~/.config/Claude/claude_desktop_config.json`
|
|
24
|
+
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
25
|
+
| Linux | `~/.config/Claude/claude_desktop_config.json` |
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
@@ -48,7 +48,7 @@ You need a Solana keypair file — a JSON array of 64 integers in [Solana CLI fo
|
|
|
48
48
|
|
|
49
49
|
### Exporting from Phantom
|
|
50
50
|
|
|
51
|
-
Open Phantom > Settings >
|
|
51
|
+
Open Phantom > Settings > Manage Accounts > [Your Account] > Show Private Key. Copy the base58 string.
|
|
52
52
|
|
|
53
53
|
### Converting to Keypair File
|
|
54
54
|
|
|
@@ -101,13 +101,13 @@ Claude will ask for permission the first time it uses a tool. Click "Allow for T
|
|
|
101
101
|
|
|
102
102
|
## Troubleshooting
|
|
103
103
|
|
|
104
|
-
| Problem | Solution
|
|
105
|
-
| --------------------------------- |
|
|
106
|
-
| No hammer icon appears | Check that your config JSON is valid and the path is absolute. Fully restart Claude Desktop.
|
|
107
|
-
| "Unable to connect to MCP server" | Check Claude Desktop logs for errors. Verify Node.js 20+ is installed.
|
|
108
|
-
| Tools appear but return errors | Verify `SOLANA_KEYPAIR_PATH` points to a valid 64-byte keypair file. Test network access.
|
|
109
|
-
| "Cannot find module" errors | Run `npm install && npm run build` (if running from source). Make sure you're using Node.js 20+.
|
|
110
|
-
| Balance shows mint addresses | Token not in well-known table. Use the mint address directly for swaps.
|
|
104
|
+
| Problem | Solution |
|
|
105
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
106
|
+
| No hammer icon appears | Check that your config JSON is valid and the path is absolute. Fully restart Claude Desktop. |
|
|
107
|
+
| "Unable to connect to MCP server" | Check Claude Desktop logs for errors. Verify Node.js 20+ is installed. |
|
|
108
|
+
| Tools appear but return errors | Verify `SOLANA_KEYPAIR_PATH` points to a valid 64-byte keypair file. Test network access. |
|
|
109
|
+
| "Cannot find module" errors | Run `npm install && npm run build` (if running from source). Make sure you're using Node.js 20+. |
|
|
110
|
+
| Balance shows mint addresses | Token not in well-known table. Use the mint address directly for swaps. |
|
|
111
111
|
|
|
112
112
|
**Where to find Claude Desktop logs:**
|
|
113
113
|
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { resolve, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
8
|
import { z } from "zod";
|
|
@@ -10,6 +13,18 @@ import { initApiClient } from "./utils/api-client.js";
|
|
|
10
13
|
import { initTokenResolver } from "./utils/token-resolver.js";
|
|
11
14
|
import { initSwap, swapTokens } from "./tools/swap.js";
|
|
12
15
|
import { getWalletBalance } from "./tools/balance.js";
|
|
16
|
+
// Route `kinetic-mcp setup` to the interactive setup wizard
|
|
17
|
+
if (process.argv[2] === "setup") {
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const setupScript = resolve(__dirname, "..", "scripts", "setup.mjs");
|
|
20
|
+
try {
|
|
21
|
+
execSync(`"${process.execPath}" "${setupScript}"`, { stdio: "inherit" });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
13
28
|
const require = createRequire(import.meta.url);
|
|
14
29
|
const { version } = require("../package.json");
|
|
15
30
|
async function main() {
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kinetic-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "MCP server for Solana trading via Kinetic API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"setup": "node scripts/setup.mjs"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^1.0.1",
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
33
34
|
"@solana/spl-token": "^0.4.9",
|
|
34
35
|
"@solana/web3.js": "^1.95.0",
|
|
36
|
+
"bs58": "^6.0.0",
|
|
37
|
+
"gradient-string": "^3.0.0",
|
|
38
|
+
"picocolors": "^1.1.1",
|
|
35
39
|
"pino": "^10.3.1",
|
|
36
40
|
"zod": "^3.23.0"
|
|
37
41
|
},
|
package/scripts/setup.mjs
CHANGED
|
@@ -1,42 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Interactive setup wizard for
|
|
4
|
+
* Interactive setup wizard for Kinetic MCP Server.
|
|
5
5
|
*
|
|
6
|
-
* Run:
|
|
6
|
+
* Run: npx kinetic-mcp setup
|
|
7
7
|
*
|
|
8
8
|
* This is a standalone .mjs file — no compilation required.
|
|
9
|
-
* Uses
|
|
9
|
+
* Uses @clack/prompts for beautiful terminal UI.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
} from "node:fs";
|
|
14
20
|
import { homedir } from "node:os";
|
|
15
21
|
import { join, resolve, dirname } from "node:path";
|
|
16
22
|
import { execSync } from "node:child_process";
|
|
17
|
-
import {
|
|
23
|
+
import { platform, execPath } from "node:process";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
18
25
|
import { Keypair } from "@solana/web3.js";
|
|
19
26
|
import bs58 from "bs58";
|
|
27
|
+
import * as p from "@clack/prompts";
|
|
28
|
+
import pc from "picocolors";
|
|
20
29
|
|
|
21
30
|
// ---------------------------------------------------------------------------
|
|
22
|
-
//
|
|
31
|
+
// Version
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const { version } = require("../package.json");
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Non-TTY check
|
|
23
39
|
// ---------------------------------------------------------------------------
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
if (!process.stdin.isTTY) {
|
|
42
|
+
console.error("Error: Setup wizard requires an interactive terminal.");
|
|
43
|
+
console.error("Run: npx kinetic-mcp setup");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// ASCII Banner
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
26
50
|
|
|
27
|
-
const
|
|
51
|
+
const BANNER = `
|
|
52
|
+
██╗ ██╗██╗███╗ ██╗███████╗████████╗██╗ ██████╗
|
|
53
|
+
██║ ██╔╝██║████╗ ██║██╔════╝╚══██╔══╝██║██╔════╝
|
|
54
|
+
█████╔╝ ██║██╔██╗ ██║█████╗ ██║ ██║██║
|
|
55
|
+
██╔═██╗ ██║██║╚██╗██║██╔══╝ ██║ ██║██║
|
|
56
|
+
██║ ██╗██║██║ ╚████║███████╗ ██║ ██║╚██████╗
|
|
57
|
+
╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝`;
|
|
28
58
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
async function renderBanner() {
|
|
60
|
+
let rendered;
|
|
61
|
+
try {
|
|
62
|
+
const gradient = (await import("gradient-string")).default;
|
|
63
|
+
const kineticGradient = gradient(["#9945FF", "#14F195"]);
|
|
64
|
+
rendered = kineticGradient.multiline(BANNER);
|
|
65
|
+
} catch {
|
|
66
|
+
rendered = pc.bold(pc.magenta(BANNER));
|
|
32
67
|
}
|
|
33
|
-
|
|
68
|
+
|
|
69
|
+
console.log(rendered);
|
|
70
|
+
console.log(pc.dim(` v${version} — Solana Trading via Claude Desktop`));
|
|
71
|
+
console.log();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function handleCancel(value) {
|
|
79
|
+
if (p.isCancel(value)) {
|
|
80
|
+
p.cancel("Setup cancelled.");
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function expandHome(filePath) {
|
|
87
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
88
|
+
return resolve(homedir(), filePath.slice(2));
|
|
89
|
+
}
|
|
90
|
+
return filePath;
|
|
34
91
|
}
|
|
35
92
|
|
|
36
93
|
function isWSL() {
|
|
37
94
|
try {
|
|
38
|
-
const
|
|
39
|
-
return /microsoft|wsl/i.test(
|
|
95
|
+
const ver = readFileSync("/proc/version", "utf-8");
|
|
96
|
+
return /microsoft|wsl/i.test(ver);
|
|
40
97
|
} catch {
|
|
41
98
|
return false;
|
|
42
99
|
}
|
|
@@ -48,11 +105,13 @@ function getClaudeConfigPath() {
|
|
|
48
105
|
case "darwin":
|
|
49
106
|
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
50
107
|
case "win32":
|
|
51
|
-
return join(
|
|
108
|
+
return join(
|
|
109
|
+
process.env.APPDATA || join(home, "AppData", "Roaming"),
|
|
110
|
+
"Claude",
|
|
111
|
+
"claude_desktop_config.json",
|
|
112
|
+
);
|
|
52
113
|
case "linux":
|
|
53
|
-
if (isWSL())
|
|
54
|
-
return null; // WSL handled separately
|
|
55
|
-
}
|
|
114
|
+
if (isWSL()) return null;
|
|
56
115
|
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
57
116
|
default:
|
|
58
117
|
return null;
|
|
@@ -63,81 +122,6 @@ function getDefaultKeypairPath() {
|
|
|
63
122
|
return join(homedir(), ".config", "solana", "trading-keypair.json");
|
|
64
123
|
}
|
|
65
124
|
|
|
66
|
-
/** Read a line with masked input (asterisks). Falls back to plain input on non-TTY. */
|
|
67
|
-
function readMasked(prompt) {
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
stdout.write(prompt);
|
|
70
|
-
|
|
71
|
-
if (!stdin.isTTY) {
|
|
72
|
-
// Non-interactive: read without masking
|
|
73
|
-
const lineRl = createInterface({ input: stdin });
|
|
74
|
-
lineRl.on("line", (line) => {
|
|
75
|
-
lineRl.close();
|
|
76
|
-
resolve(line.trim());
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
stdin.setRawMode(true);
|
|
82
|
-
stdin.resume();
|
|
83
|
-
stdin.setEncoding("utf8");
|
|
84
|
-
let input = "";
|
|
85
|
-
|
|
86
|
-
const onData = (ch) => {
|
|
87
|
-
if (ch === "\r" || ch === "\n") {
|
|
88
|
-
stdin.setRawMode(false);
|
|
89
|
-
stdin.pause();
|
|
90
|
-
stdin.removeListener("data", onData);
|
|
91
|
-
stdout.write("\n");
|
|
92
|
-
resolve(input);
|
|
93
|
-
} else if (ch === "\u0003") {
|
|
94
|
-
// Ctrl+C
|
|
95
|
-
stdin.setRawMode(false);
|
|
96
|
-
stdin.pause();
|
|
97
|
-
stdin.removeListener("data", onData);
|
|
98
|
-
stdout.write("\n");
|
|
99
|
-
reject(new Error("Cancelled by user"));
|
|
100
|
-
} else if (ch === "\u007f" || ch === "\b") {
|
|
101
|
-
// Backspace
|
|
102
|
-
if (input.length > 0) {
|
|
103
|
-
input = input.slice(0, -1);
|
|
104
|
-
stdout.write("\b \b");
|
|
105
|
-
}
|
|
106
|
-
} else if (ch.charCodeAt(0) >= 32) {
|
|
107
|
-
// Printable character
|
|
108
|
-
input += ch;
|
|
109
|
-
stdout.write("*");
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
stdin.on("data", onData);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function askYesNo(question, defaultYes = true) {
|
|
118
|
-
const suffix = defaultYes ? "(Y/n)" : "(y/N)";
|
|
119
|
-
const answer = await rl.question(`${question} ${suffix} `);
|
|
120
|
-
const trimmed = answer.trim().toLowerCase();
|
|
121
|
-
if (trimmed === "") return defaultYes;
|
|
122
|
-
return trimmed === "y" || trimmed === "yes";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function askChoice(question, choices) {
|
|
126
|
-
console.log(`\n${question}`);
|
|
127
|
-
choices.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
|
|
128
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
129
|
-
const answer = await rl.question(`Choice (1-${choices.length}): `);
|
|
130
|
-
const num = parseInt(answer.trim(), 10);
|
|
131
|
-
if (num >= 1 && num <= choices.length) return num;
|
|
132
|
-
console.log(" Invalid choice. Try again.");
|
|
133
|
-
}
|
|
134
|
-
throw new Error("Too many invalid attempts.");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// Keypair handling
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
125
|
function validateKeypairFile(filePath) {
|
|
142
126
|
const expanded = expandHome(filePath);
|
|
143
127
|
if (!existsSync(expanded)) {
|
|
@@ -148,112 +132,318 @@ function validateKeypairFile(filePath) {
|
|
|
148
132
|
try {
|
|
149
133
|
parsed = JSON.parse(raw);
|
|
150
134
|
} catch {
|
|
151
|
-
throw new Error(
|
|
135
|
+
throw new Error(
|
|
136
|
+
"File is not valid JSON. Expected a JSON array of 64 integers (Solana CLI format).",
|
|
137
|
+
);
|
|
152
138
|
}
|
|
153
139
|
if (
|
|
154
140
|
!Array.isArray(parsed) ||
|
|
155
141
|
parsed.length !== 64 ||
|
|
156
142
|
!parsed.every((n) => typeof n === "number" && Number.isInteger(n))
|
|
157
143
|
) {
|
|
158
|
-
throw new Error("
|
|
144
|
+
throw new Error("Invalid Solana keypair. Expected a JSON array of exactly 64 integers.");
|
|
159
145
|
}
|
|
160
146
|
return Keypair.fromSecretKey(Uint8Array.from(parsed));
|
|
161
147
|
}
|
|
162
148
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(" How to export from Phantom:");
|
|
167
|
-
console.log(" 1. Open Phantom > Settings > Security & Privacy > Export Private Key");
|
|
168
|
-
console.log(" 2. Enter your password and copy the base58 string");
|
|
169
|
-
console.log("");
|
|
170
|
-
console.log(" SECURITY: Your key will be masked. It is NOT saved to shell history.");
|
|
171
|
-
console.log("");
|
|
172
|
-
|
|
173
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
174
|
-
try {
|
|
175
|
-
const key = await readMasked(" Paste your private key: ");
|
|
176
|
-
if (!key || key.trim().length === 0) {
|
|
177
|
-
console.log(" No key entered. Try again.");
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Step 2: Build Check
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
180
152
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} catch {
|
|
185
|
-
console.log(" Invalid private key format. Expected a base58-encoded string from Phantom.");
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
153
|
+
async function checkBuild() {
|
|
154
|
+
const buildExists = existsSync(resolve("build", "index.js"));
|
|
155
|
+
if (buildExists) return;
|
|
188
156
|
|
|
189
|
-
|
|
190
|
-
console.log(` Expected 64-byte secret key, got ${decoded.length} bytes.`);
|
|
191
|
-
if (decoded.length === 32) {
|
|
192
|
-
console.log(" This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.");
|
|
193
|
-
}
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
157
|
+
p.log.warn("Project hasn't been built yet — build/index.js not found.");
|
|
196
158
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
159
|
+
const runBuild = handleCancel(
|
|
160
|
+
await p.confirm({ message: "Run npm run build now?", initialValue: true }),
|
|
161
|
+
);
|
|
200
162
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
163
|
+
if (runBuild) {
|
|
164
|
+
const s = p.spinner();
|
|
165
|
+
s.start("Building project...");
|
|
166
|
+
try {
|
|
167
|
+
execSync("npm run build", { stdio: "pipe" });
|
|
168
|
+
s.stop("Build complete");
|
|
169
|
+
} catch {
|
|
170
|
+
s.stop("Build failed");
|
|
171
|
+
p.log.warn("You can try again later with: npm run build");
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
p.log.info("Skipped. Remember to run npm run build before using the server.");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
205
177
|
|
|
206
|
-
|
|
207
|
-
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Step 3: Keypair Setup
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
208
181
|
|
|
209
|
-
|
|
210
|
-
|
|
182
|
+
async function importFromPhantom() {
|
|
183
|
+
p.note(
|
|
184
|
+
[
|
|
185
|
+
`${pc.bold("How to export from Phantom:")}`,
|
|
186
|
+
"",
|
|
187
|
+
"1. Open Phantom → Settings → Manage Accounts → [Your Account] → Show Private Key",
|
|
188
|
+
"2. Enter your password and copy the base58 string",
|
|
189
|
+
"",
|
|
190
|
+
`${pc.dim("Your key will be masked. It is NOT saved to shell history.")}`,
|
|
191
|
+
].join("\n"),
|
|
192
|
+
"Import Private Key",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const key = handleCancel(
|
|
196
|
+
await p.password({
|
|
197
|
+
message: "Paste your private key:",
|
|
198
|
+
mask: "*",
|
|
199
|
+
validate(value) {
|
|
200
|
+
if (!value || value.trim().length === 0) return "Private key is required.";
|
|
201
|
+
|
|
202
|
+
let decoded;
|
|
211
203
|
try {
|
|
212
|
-
|
|
204
|
+
decoded = bs58.decode(value.trim());
|
|
213
205
|
} catch {
|
|
214
|
-
|
|
206
|
+
return "Invalid format. Expected a base58-encoded string from Phantom.";
|
|
215
207
|
}
|
|
216
|
-
}
|
|
217
208
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
209
|
+
if (decoded.length !== 64) {
|
|
210
|
+
if (decoded.length === 32) {
|
|
211
|
+
return "This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.";
|
|
212
|
+
}
|
|
213
|
+
return `Expected 64-byte secret key, got ${decoded.length} bytes.`;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const decoded = bs58.decode(key.trim());
|
|
220
|
+
const keypair = Keypair.fromSecretKey(decoded);
|
|
221
|
+
|
|
222
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
223
|
+
p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
|
|
224
|
+
p.log.message(pc.dim("Verify this matches your Phantom wallet address."));
|
|
225
|
+
|
|
226
|
+
// Save keypair to default path
|
|
227
|
+
const defaultPath = getDefaultKeypairPath();
|
|
228
|
+
|
|
229
|
+
// Check if file exists
|
|
230
|
+
if (existsSync(defaultPath)) {
|
|
231
|
+
const overwrite = handleCancel(
|
|
232
|
+
await p.confirm({
|
|
233
|
+
message: `Keypair file already exists at ${pc.dim(defaultPath)}. Overwrite?`,
|
|
234
|
+
initialValue: false,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (!overwrite) {
|
|
239
|
+
p.log.info("Keeping existing keypair file.");
|
|
240
|
+
return { publicKey, path: defaultPath };
|
|
223
241
|
}
|
|
224
242
|
}
|
|
225
|
-
throw new Error("Too many invalid attempts. Exiting.");
|
|
226
|
-
}
|
|
227
243
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
234
|
-
const filePath = await rl.question(" Path to keypair file: ");
|
|
235
|
-
if (!filePath.trim()) {
|
|
236
|
-
console.log(" No path entered. Try again.");
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
244
|
+
mkdirSync(dirname(defaultPath), { recursive: true, mode: 0o700 });
|
|
245
|
+
writeFileSync(defaultPath, JSON.stringify(Array.from(keypair.secretKey)) + "\n", { mode: 0o600 });
|
|
246
|
+
|
|
247
|
+
if (platform !== "win32") {
|
|
239
248
|
try {
|
|
240
|
-
|
|
241
|
-
const keypair = validateKeypairFile(expanded);
|
|
242
|
-
console.log(`\n Wallet address: ${keypair.publicKey.toBase58()}`);
|
|
243
|
-
return { keypair, path: expanded };
|
|
249
|
+
chmodSync(defaultPath, 0o600);
|
|
244
250
|
} catch (err) {
|
|
245
|
-
|
|
246
|
-
if (attempt < MAX_RETRIES - 1) console.log(" Try again.");
|
|
251
|
+
p.log.warn(`Could not set file permissions: ${err.message}. Run: chmod 600 ${defaultPath}`);
|
|
247
252
|
}
|
|
248
253
|
}
|
|
249
|
-
|
|
254
|
+
|
|
255
|
+
p.log.success(`Keypair saved to ${pc.dim(defaultPath)}`);
|
|
256
|
+
return { publicKey, path: defaultPath };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function useExistingKeypair() {
|
|
260
|
+
const filePath = handleCancel(
|
|
261
|
+
await p.text({
|
|
262
|
+
message: "Path to keypair file:",
|
|
263
|
+
placeholder: "~/.config/solana/id.json",
|
|
264
|
+
validate(value) {
|
|
265
|
+
if (!value || value.trim().length === 0) return "Path is required.";
|
|
266
|
+
try {
|
|
267
|
+
const expanded = expandHome(value.trim());
|
|
268
|
+
validateKeypairFile(expanded);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return err.message;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const expanded = expandHome(filePath.trim());
|
|
277
|
+
const keypair = validateKeypairFile(expanded);
|
|
278
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
279
|
+
|
|
280
|
+
p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
|
|
281
|
+
|
|
282
|
+
return { publicKey, path: expanded };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function setupKeypair() {
|
|
286
|
+
const method = handleCancel(
|
|
287
|
+
await p.select({
|
|
288
|
+
message: "How would you like to set up your Solana keypair?",
|
|
289
|
+
options: [
|
|
290
|
+
{
|
|
291
|
+
value: "phantom",
|
|
292
|
+
label: "Import from Phantom",
|
|
293
|
+
hint: "paste your base58 private key",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
value: "existing",
|
|
297
|
+
label: "Use existing keypair file",
|
|
298
|
+
hint: "Solana CLI JSON format",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (method === "phantom") {
|
|
305
|
+
return importFromPhantom();
|
|
306
|
+
} else {
|
|
307
|
+
return useExistingKeypair();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Step 4: API Key
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
async function setupApiKey() {
|
|
316
|
+
const apiKey = handleCancel(
|
|
317
|
+
await p.password({
|
|
318
|
+
message: "Kinetic API key (press Enter to skip):",
|
|
319
|
+
mask: "*",
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return apiKey?.trim() || null;
|
|
250
324
|
}
|
|
251
325
|
|
|
252
326
|
// ---------------------------------------------------------------------------
|
|
253
|
-
//
|
|
327
|
+
// Step 5: Advanced Settings
|
|
254
328
|
// ---------------------------------------------------------------------------
|
|
255
329
|
|
|
256
|
-
|
|
330
|
+
const DEFAULTS = {
|
|
331
|
+
rpcUrl: "https://api.mainnet-beta.solana.com",
|
|
332
|
+
maxTradeSol: "10",
|
|
333
|
+
priorityFeeLamports: "400000",
|
|
334
|
+
jitoEnabled: false,
|
|
335
|
+
dynamicSlippageMinBps: "1",
|
|
336
|
+
dynamicSlippageMaxBps: "300",
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
async function advancedSettings() {
|
|
340
|
+
const configure = handleCancel(
|
|
341
|
+
await p.confirm({
|
|
342
|
+
message: "Configure advanced settings?",
|
|
343
|
+
initialValue: false,
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (!configure) return {};
|
|
348
|
+
|
|
349
|
+
p.log.info(pc.dim("Press Enter to keep defaults shown in parentheses."));
|
|
350
|
+
|
|
351
|
+
const rpcUrl = handleCancel(
|
|
352
|
+
await p.text({
|
|
353
|
+
message: "Solana RPC URL:",
|
|
354
|
+
placeholder: DEFAULTS.rpcUrl,
|
|
355
|
+
defaultValue: DEFAULTS.rpcUrl,
|
|
356
|
+
validate(value) {
|
|
357
|
+
if (!value) return;
|
|
358
|
+
try {
|
|
359
|
+
const url = new URL(value);
|
|
360
|
+
if (url.protocol !== "https:") return "URL must use https://";
|
|
361
|
+
} catch {
|
|
362
|
+
return "Invalid URL format.";
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const maxTradeSol = handleCancel(
|
|
369
|
+
await p.text({
|
|
370
|
+
message: "Max trade size (SOL):",
|
|
371
|
+
placeholder: DEFAULTS.maxTradeSol,
|
|
372
|
+
defaultValue: DEFAULTS.maxTradeSol,
|
|
373
|
+
validate(value) {
|
|
374
|
+
const num = Number(value);
|
|
375
|
+
if (isNaN(num) || num <= 0) return "Must be a positive number.";
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const priorityFeeLamports = handleCancel(
|
|
381
|
+
await p.text({
|
|
382
|
+
message: `Priority fee (lamports): ${pc.dim("400000 = ~0.0004 SOL")}`,
|
|
383
|
+
placeholder: DEFAULTS.priorityFeeLamports,
|
|
384
|
+
defaultValue: DEFAULTS.priorityFeeLamports,
|
|
385
|
+
validate(value) {
|
|
386
|
+
const num = Number(value);
|
|
387
|
+
if (isNaN(num) || !Number.isInteger(num) || num < 0)
|
|
388
|
+
return "Must be a non-negative integer.";
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const jitoEnabled = handleCancel(
|
|
394
|
+
await p.confirm({
|
|
395
|
+
message: "Enable Jito MEV protection?",
|
|
396
|
+
initialValue: DEFAULTS.jitoEnabled,
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const dynamicSlippageMinBps = handleCancel(
|
|
401
|
+
await p.text({
|
|
402
|
+
message: "Min dynamic slippage (BPS):",
|
|
403
|
+
placeholder: DEFAULTS.dynamicSlippageMinBps,
|
|
404
|
+
defaultValue: DEFAULTS.dynamicSlippageMinBps,
|
|
405
|
+
validate(value) {
|
|
406
|
+
const num = Number(value);
|
|
407
|
+
if (isNaN(num) || !Number.isInteger(num) || num < 0)
|
|
408
|
+
return "Must be a non-negative integer.";
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const dynamicSlippageMaxBps = handleCancel(
|
|
414
|
+
await p.text({
|
|
415
|
+
message: "Max dynamic slippage (BPS):",
|
|
416
|
+
placeholder: DEFAULTS.dynamicSlippageMaxBps,
|
|
417
|
+
defaultValue: DEFAULTS.dynamicSlippageMaxBps,
|
|
418
|
+
validate(value) {
|
|
419
|
+
const num = Number(value);
|
|
420
|
+
if (isNaN(num) || !Number.isInteger(num) || num <= 0) return "Must be a positive integer.";
|
|
421
|
+
if (num > 1000) return "Cannot exceed 1000 BPS (10%).";
|
|
422
|
+
if (Number(dynamicSlippageMinBps) >= num) return "Must be greater than min slippage.";
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Return only non-default values
|
|
428
|
+
const settings = {};
|
|
429
|
+
if (rpcUrl !== DEFAULTS.rpcUrl) settings.SOLANA_RPC_URL = rpcUrl;
|
|
430
|
+
if (maxTradeSol !== DEFAULTS.maxTradeSol) settings.MAX_TRADE_SOL = maxTradeSol;
|
|
431
|
+
if (priorityFeeLamports !== DEFAULTS.priorityFeeLamports)
|
|
432
|
+
settings.PRIORITY_FEE_LAMPORTS = priorityFeeLamports;
|
|
433
|
+
if (jitoEnabled !== DEFAULTS.jitoEnabled) settings.JITO_ENABLED = jitoEnabled ? "true" : "false";
|
|
434
|
+
if (dynamicSlippageMinBps !== DEFAULTS.dynamicSlippageMinBps)
|
|
435
|
+
settings.DYNAMIC_SLIPPAGE_MIN_BPS = dynamicSlippageMinBps;
|
|
436
|
+
if (dynamicSlippageMaxBps !== DEFAULTS.dynamicSlippageMaxBps)
|
|
437
|
+
settings.DYNAMIC_SLIPPAGE_MAX_BPS = dynamicSlippageMaxBps;
|
|
438
|
+
|
|
439
|
+
return settings;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Step 6: Claude Desktop Config
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
function generateConfigEntry(keypairPath, apiKey, advancedEnv) {
|
|
257
447
|
const projectRoot = resolve(".");
|
|
258
448
|
const entry = {
|
|
259
449
|
command: execPath,
|
|
@@ -265,55 +455,84 @@ function generateConfigEntry(keypairPath, apiKey) {
|
|
|
265
455
|
if (apiKey) {
|
|
266
456
|
entry.env.API_KEY = apiKey;
|
|
267
457
|
}
|
|
458
|
+
// Add non-default advanced settings
|
|
459
|
+
if (advancedEnv) {
|
|
460
|
+
Object.assign(entry.env, advancedEnv);
|
|
461
|
+
}
|
|
268
462
|
return entry;
|
|
269
463
|
}
|
|
270
464
|
|
|
271
|
-
function
|
|
465
|
+
function formatConfigSnippet(entry) {
|
|
272
466
|
const snippet = {
|
|
273
467
|
mcpServers: {
|
|
274
468
|
"solana-trading": entry,
|
|
275
469
|
},
|
|
276
470
|
};
|
|
277
|
-
|
|
278
|
-
console.log("");
|
|
279
|
-
console.log(" " + JSON.stringify(snippet, null, 2).split("\n").join("\n "));
|
|
280
|
-
console.log("");
|
|
281
|
-
console.log(" If you already have other MCP servers configured,");
|
|
282
|
-
console.log(' merge the "solana-trading" entry into your existing "mcpServers" object.');
|
|
471
|
+
return JSON.stringify(snippet, null, 2);
|
|
283
472
|
}
|
|
284
473
|
|
|
285
|
-
async function configureClaudeDesktop(keypairPath, apiKey) {
|
|
286
|
-
|
|
474
|
+
async function configureClaudeDesktop(keypairPath, apiKey, advancedEnv) {
|
|
475
|
+
const entry = generateConfigEntry(keypairPath, apiKey, advancedEnv);
|
|
287
476
|
|
|
288
477
|
if (isWSL()) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
478
|
+
p.note(
|
|
479
|
+
[
|
|
480
|
+
"WSL detected. Claude Desktop runs on the Windows host.",
|
|
481
|
+
"",
|
|
482
|
+
`Config file: ${pc.dim("%APPDATA%\\Claude\\claude_desktop_config.json")}`,
|
|
483
|
+
"",
|
|
484
|
+
"Add this to your config:",
|
|
485
|
+
"",
|
|
486
|
+
formatConfigSnippet(entry),
|
|
487
|
+
"",
|
|
488
|
+
pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
|
|
489
|
+
].join("\n"),
|
|
490
|
+
"Claude Desktop Config",
|
|
491
|
+
);
|
|
296
492
|
return;
|
|
297
493
|
}
|
|
298
494
|
|
|
299
495
|
const configPath = getClaudeConfigPath();
|
|
300
496
|
if (!configPath) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
497
|
+
p.note(
|
|
498
|
+
[
|
|
499
|
+
"Could not detect Claude Desktop config path.",
|
|
500
|
+
"",
|
|
501
|
+
"Add this to your claude_desktop_config.json:",
|
|
502
|
+
"",
|
|
503
|
+
formatConfigSnippet(entry),
|
|
504
|
+
].join("\n"),
|
|
505
|
+
"Claude Desktop Config",
|
|
506
|
+
);
|
|
304
507
|
return;
|
|
305
508
|
}
|
|
306
509
|
|
|
307
|
-
|
|
308
|
-
const entry = generateConfigEntry(keypairPath, apiKey);
|
|
510
|
+
p.log.info(`Config file: ${pc.dim(configPath)}`);
|
|
309
511
|
|
|
310
|
-
const autoModify =
|
|
512
|
+
const autoModify = handleCancel(
|
|
513
|
+
await p.confirm({
|
|
514
|
+
message: "Automatically add this MCP server to Claude Desktop config?",
|
|
515
|
+
initialValue: true,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
311
518
|
|
|
312
519
|
if (!autoModify) {
|
|
313
|
-
|
|
520
|
+
p.note(
|
|
521
|
+
[
|
|
522
|
+
"Add this to your claude_desktop_config.json:",
|
|
523
|
+
"",
|
|
524
|
+
formatConfigSnippet(entry),
|
|
525
|
+
"",
|
|
526
|
+
pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
|
|
527
|
+
].join("\n"),
|
|
528
|
+
"Manual Config",
|
|
529
|
+
);
|
|
314
530
|
return;
|
|
315
531
|
}
|
|
316
532
|
|
|
533
|
+
const s = p.spinner();
|
|
534
|
+
s.start("Writing Claude Desktop config...");
|
|
535
|
+
|
|
317
536
|
// Read existing config
|
|
318
537
|
let config = {};
|
|
319
538
|
if (existsSync(configPath)) {
|
|
@@ -321,117 +540,234 @@ async function configureClaudeDesktop(keypairPath, apiKey) {
|
|
|
321
540
|
const raw = readFileSync(configPath, "utf-8");
|
|
322
541
|
config = JSON.parse(raw);
|
|
323
542
|
} catch {
|
|
324
|
-
|
|
325
|
-
|
|
543
|
+
s.stop("Existing config has invalid JSON");
|
|
544
|
+
p.note(
|
|
545
|
+
[
|
|
546
|
+
"Could not parse existing config file.",
|
|
547
|
+
"",
|
|
548
|
+
"Add this manually to your claude_desktop_config.json:",
|
|
549
|
+
"",
|
|
550
|
+
formatConfigSnippet(entry),
|
|
551
|
+
].join("\n"),
|
|
552
|
+
"Manual Config",
|
|
553
|
+
);
|
|
326
554
|
return;
|
|
327
555
|
}
|
|
328
556
|
|
|
329
557
|
// Backup
|
|
330
558
|
const backupPath = configPath + ".bak";
|
|
331
559
|
copyFileSync(configPath, backupPath);
|
|
332
|
-
|
|
560
|
+
p.log.info(`Backup created: ${pc.dim(backupPath)}`);
|
|
333
561
|
}
|
|
334
562
|
|
|
335
|
-
//
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
563
|
+
// Check for existing entry
|
|
564
|
+
if (config.mcpServers?.["solana-trading"]) {
|
|
565
|
+
s.stop("Existing entry found");
|
|
566
|
+
const overwrite = handleCancel(
|
|
567
|
+
await p.confirm({
|
|
568
|
+
message: "A 'solana-trading' entry already exists. Overwrite?",
|
|
569
|
+
initialValue: false,
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
339
572
|
|
|
340
|
-
if (config.mcpServers["solana-trading"]) {
|
|
341
|
-
const overwrite = await askYesNo(" A 'solana-trading' entry already exists. Overwrite?");
|
|
342
573
|
if (!overwrite) {
|
|
343
|
-
|
|
574
|
+
p.log.info("Existing config preserved.");
|
|
344
575
|
return;
|
|
345
576
|
}
|
|
577
|
+
|
|
578
|
+
s.start("Writing Claude Desktop config...");
|
|
346
579
|
}
|
|
347
580
|
|
|
581
|
+
// Merge
|
|
582
|
+
if (!config.mcpServers) {
|
|
583
|
+
config.mcpServers = {};
|
|
584
|
+
}
|
|
348
585
|
config.mcpServers["solana-trading"] = entry;
|
|
349
586
|
|
|
350
587
|
// Write
|
|
351
588
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
352
589
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
353
|
-
|
|
590
|
+
|
|
591
|
+
s.stop("Claude Desktop config updated");
|
|
354
592
|
}
|
|
355
593
|
|
|
356
594
|
// ---------------------------------------------------------------------------
|
|
357
|
-
//
|
|
595
|
+
// Step 7: Verification
|
|
358
596
|
// ---------------------------------------------------------------------------
|
|
359
597
|
|
|
360
|
-
|
|
361
|
-
console.log("");
|
|
362
|
-
console.log(" Solana Trading MCP Server — Setup Wizard");
|
|
363
|
-
console.log(" =========================================");
|
|
364
|
-
console.log("");
|
|
598
|
+
const MAX_VERIFICATION_RETRIES = 3;
|
|
365
599
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
600
|
+
async function runVerification(keypairPath, rpcUrl, apiKey) {
|
|
601
|
+
for (let attempt = 0; attempt <= MAX_VERIFICATION_RETRIES; attempt++) {
|
|
602
|
+
const s = p.spinner();
|
|
603
|
+
let hasFailure = false;
|
|
604
|
+
|
|
605
|
+
// 1. Load keypair
|
|
606
|
+
s.start("Loading keypair...");
|
|
607
|
+
let keypair;
|
|
608
|
+
try {
|
|
609
|
+
keypair = validateKeypairFile(keypairPath);
|
|
610
|
+
s.stop(`Keypair loaded — ${pc.cyan(keypair.publicKey.toBase58())}`);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
s.stop(`Keypair load failed: ${err.message}`);
|
|
613
|
+
hasFailure = true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 2. Check SOL balance (only if keypair loaded)
|
|
617
|
+
if (keypair) {
|
|
618
|
+
const effectiveRpc = rpcUrl || DEFAULTS.rpcUrl;
|
|
619
|
+
s.start("Checking SOL balance...");
|
|
373
620
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
621
|
+
const response = await fetch(effectiveRpc, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: { "Content-Type": "application/json" },
|
|
624
|
+
body: JSON.stringify({
|
|
625
|
+
jsonrpc: "2.0",
|
|
626
|
+
id: 1,
|
|
627
|
+
method: "getBalance",
|
|
628
|
+
params: [keypair.publicKey.toBase58()],
|
|
629
|
+
}),
|
|
630
|
+
signal: AbortSignal.timeout(10_000),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const data = await response.json();
|
|
634
|
+
const lamports = data.result?.value ?? 0;
|
|
635
|
+
const sol = (lamports / 1e9).toFixed(4);
|
|
636
|
+
|
|
637
|
+
if (lamports === 0) {
|
|
638
|
+
s.stop(`Balance: ${pc.yellow("0 SOL")}`);
|
|
639
|
+
p.log.warn("Fund this wallet with SOL before trading.");
|
|
640
|
+
} else {
|
|
641
|
+
s.stop(`Balance: ${pc.green(sol + " SOL")}`);
|
|
642
|
+
}
|
|
643
|
+
} catch (err) {
|
|
644
|
+
const msg =
|
|
645
|
+
err.name === "TimeoutError" || err.name === "AbortError"
|
|
646
|
+
? "Request timed out"
|
|
647
|
+
: err.message;
|
|
648
|
+
s.stop(`Balance check failed: ${pc.yellow(msg)}`);
|
|
649
|
+
hasFailure = true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 3. Test Kinetic API (only if API key provided)
|
|
654
|
+
if (apiKey) {
|
|
655
|
+
s.start("Testing Kinetic API...");
|
|
656
|
+
try {
|
|
657
|
+
const response = await fetch("https://auth.kinetic.xyz/v1/token", {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: { "Content-Type": "application/json" },
|
|
660
|
+
body: JSON.stringify({ apiKey }),
|
|
661
|
+
signal: AbortSignal.timeout(10_000),
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (response.ok) {
|
|
665
|
+
s.stop(`Kinetic API: ${pc.green("connected")}`);
|
|
666
|
+
} else {
|
|
667
|
+
s.stop(`Kinetic API: ${pc.yellow(`${response.status} ${response.statusText}`)}`);
|
|
668
|
+
hasFailure = true;
|
|
669
|
+
}
|
|
670
|
+
} catch (err) {
|
|
671
|
+
const msg =
|
|
672
|
+
err.name === "TimeoutError" || err.name === "AbortError"
|
|
673
|
+
? "Request timed out"
|
|
674
|
+
: err.message;
|
|
675
|
+
s.stop(`Kinetic API: ${pc.yellow(msg)}`);
|
|
676
|
+
hasFailure = true;
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
p.log.info(pc.dim("Skipping Kinetic API test (no API key)."));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (!hasFailure) return;
|
|
683
|
+
|
|
684
|
+
// Retry on failure (up to MAX_VERIFICATION_RETRIES)
|
|
685
|
+
if (attempt < MAX_VERIFICATION_RETRIES) {
|
|
686
|
+
const retry = handleCancel(
|
|
687
|
+
await p.confirm({
|
|
688
|
+
message: "Some checks failed. Retry verification?",
|
|
689
|
+
initialValue: true,
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
if (!retry) {
|
|
694
|
+
p.log.warn("Continuing with warnings. You can verify connectivity later.");
|
|
695
|
+
return;
|
|
379
696
|
}
|
|
380
697
|
} else {
|
|
381
|
-
|
|
698
|
+
p.log.warn("Maximum retries reached. Continuing with warnings.");
|
|
699
|
+
return;
|
|
382
700
|
}
|
|
383
701
|
}
|
|
702
|
+
}
|
|
384
703
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"Use an existing Solana CLI keypair file",
|
|
389
|
-
]);
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
// Main
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
390
707
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
708
|
+
async function main() {
|
|
709
|
+
await renderBanner();
|
|
710
|
+
p.intro(pc.bold("Setup Wizard"));
|
|
711
|
+
|
|
712
|
+
// Step 2: Build check
|
|
713
|
+
await checkBuild();
|
|
714
|
+
|
|
715
|
+
// Step 3: Keypair setup
|
|
716
|
+
const keypairResult = await setupKeypair();
|
|
717
|
+
|
|
718
|
+
// Step 4: API key
|
|
719
|
+
const apiKey = await setupApiKey();
|
|
720
|
+
|
|
721
|
+
// Step 5: Advanced settings
|
|
722
|
+
const advancedEnv = await advancedSettings();
|
|
723
|
+
|
|
724
|
+
// Step 6: Claude Desktop config
|
|
725
|
+
await configureClaudeDesktop(keypairResult.path, apiKey, advancedEnv);
|
|
726
|
+
|
|
727
|
+
// Step 7: Verification
|
|
728
|
+
const rpcUrl = advancedEnv.SOLANA_RPC_URL || null;
|
|
729
|
+
await runVerification(keypairResult.path, rpcUrl, apiKey);
|
|
730
|
+
|
|
731
|
+
// Step 8: Outro
|
|
732
|
+
const summaryLines = [
|
|
733
|
+
`Wallet: ${pc.cyan(keypairResult.publicKey)}`,
|
|
734
|
+
`Keypair: ${pc.dim(keypairResult.path)}`,
|
|
735
|
+
];
|
|
736
|
+
if (apiKey) summaryLines.push(`API Key: ${pc.green("configured")}`);
|
|
737
|
+
|
|
738
|
+
const advancedKeys = Object.keys(advancedEnv);
|
|
739
|
+
if (advancedKeys.length > 0) {
|
|
740
|
+
summaryLines.push("");
|
|
741
|
+
summaryLines.push(pc.bold("Custom settings:"));
|
|
742
|
+
for (const [key, val] of Object.entries(advancedEnv)) {
|
|
743
|
+
summaryLines.push(` ${pc.dim(key)}: ${val}`);
|
|
744
|
+
}
|
|
396
745
|
}
|
|
397
746
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
console.log(" Next steps:");
|
|
416
|
-
console.log(" 1. Restart Claude Desktop (fully quit and reopen)");
|
|
417
|
-
console.log(' 2. Look for the hammer icon in the chat input');
|
|
418
|
-
console.log(' 3. Try: "What\'s my SOL balance?"');
|
|
419
|
-
console.log("");
|
|
420
|
-
console.log(" Safety guardrails:");
|
|
421
|
-
console.log(" - Swaps always show a preview first (confirm=false)");
|
|
422
|
-
console.log(" - Max trade size: 10 SOL (configurable via MAX_TRADE_SOL)");
|
|
423
|
-
console.log(" - Slippage capped at 10% (1000 bps)");
|
|
424
|
-
console.log("");
|
|
425
|
-
|
|
426
|
-
rl.close();
|
|
747
|
+
p.note(summaryLines.join("\n"), "Setup Complete");
|
|
748
|
+
|
|
749
|
+
p.note(
|
|
750
|
+
[
|
|
751
|
+
`1. ${pc.bold("Restart Claude Desktop")} (fully quit and reopen)`,
|
|
752
|
+
`2. Look for the ${pc.bold("hammer icon")} in the chat input`,
|
|
753
|
+
`3. Try: ${pc.cyan('"What\'s my SOL balance?"')}`,
|
|
754
|
+
"",
|
|
755
|
+
pc.dim("Safety guardrails:"),
|
|
756
|
+
pc.dim(` • Swaps always show a preview first`),
|
|
757
|
+
pc.dim(` • Max trade size: ${advancedEnv.MAX_TRADE_SOL || "10"} SOL`),
|
|
758
|
+
pc.dim(` • Slippage capped at ${advancedEnv.DYNAMIC_SLIPPAGE_MAX_BPS || "300"} BPS`),
|
|
759
|
+
].join("\n"),
|
|
760
|
+
"Next Steps",
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
p.outro(pc.green("You're all set! Happy trading."));
|
|
427
764
|
}
|
|
428
765
|
|
|
429
766
|
main().catch((err) => {
|
|
430
767
|
if (err.message === "Cancelled by user") {
|
|
431
|
-
|
|
768
|
+
p.cancel("Setup cancelled.");
|
|
432
769
|
} else {
|
|
433
|
-
|
|
770
|
+
p.log.error(`Setup failed: ${err.message}`);
|
|
434
771
|
}
|
|
435
|
-
rl.close();
|
|
436
772
|
process.exit(1);
|
|
437
773
|
});
|