helius-mcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -52
- package/LICENSE +21 -21
- package/README.md +132 -132
- package/dist/http.d.ts +1 -1
- package/dist/index.js +55 -55
- package/dist/scripts/test-htmltotext.d.ts +5 -0
- package/dist/scripts/test-htmltotext.js +67 -0
- package/dist/scripts/test-solana-knowledge.d.ts +9 -0
- package/dist/scripts/test-solana-knowledge.js +272 -0
- package/dist/scripts/validate-templates.d.ts +12 -0
- package/dist/scripts/validate-templates.js +94 -0
- package/dist/tools/auth.js +45 -14
- package/dist/tools/network.js +35 -2
- package/dist/tools/shared.js +12 -12
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +64 -64
- package/system-prompts/helius/claude.system.md +170 -169
- package/system-prompts/helius/full.md +2869 -2868
- package/system-prompts/helius/openai.developer.md +170 -169
- package/system-prompts/helius-dflow/claude.system.md +290 -289
- package/system-prompts/helius-dflow/full.md +3648 -3647
- package/system-prompts/helius-dflow/openai.developer.md +290 -289
- package/system-prompts/helius-phantom/claude.system.md +333 -332
- package/system-prompts/helius-phantom/full.md +5473 -5472
- package/system-prompts/helius-phantom/openai.developer.md +333 -332
- package/system-prompts/svm/claude.system.md +160 -159
- package/system-prompts/svm/full.md +633 -632
- package/system-prompts/svm/openai.developer.md +160 -159
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick test: fetch a real blog post and check htmlToText output quality.
|
|
3
|
+
* Run: node dist/scripts/test-htmltotext.js
|
|
4
|
+
*/
|
|
5
|
+
function htmlToText(html) {
|
|
6
|
+
let content = html;
|
|
7
|
+
const articleMatch = html.match(/<article[^>]*>([\s\S]*)<\/article>/i);
|
|
8
|
+
if (articleMatch) {
|
|
9
|
+
content = articleMatch[1];
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
const mainMatch = html.match(/<main[^>]*>([\s\S]*)<\/main>/i);
|
|
13
|
+
if (mainMatch) {
|
|
14
|
+
content = mainMatch[1];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return (content
|
|
18
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
19
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
20
|
+
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
21
|
+
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
22
|
+
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
|
|
23
|
+
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '')
|
|
24
|
+
.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
|
|
25
|
+
.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n# $1\n')
|
|
26
|
+
.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n')
|
|
27
|
+
.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n### $1\n')
|
|
28
|
+
.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '\n#### $1\n')
|
|
29
|
+
.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, '\n```\n$1\n```\n')
|
|
30
|
+
.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`')
|
|
31
|
+
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n')
|
|
32
|
+
.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '\n$1\n')
|
|
33
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
34
|
+
.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)')
|
|
35
|
+
.replace(/<(?:strong|b)[^>]*>([\s\S]*?)<\/(?:strong|b)>/gi, '**$1**')
|
|
36
|
+
.replace(/<(?:em|i)[^>]*>([\s\S]*?)<\/(?:em|i)>/gi, '*$1*')
|
|
37
|
+
.replace(/<[^>]+>/g, '')
|
|
38
|
+
.replace(/&/g, '&')
|
|
39
|
+
.replace(/</g, '<')
|
|
40
|
+
.replace(/>/g, '>')
|
|
41
|
+
.replace(/"/g, '"')
|
|
42
|
+
.replace(/'/g, "'")
|
|
43
|
+
.replace(/ /g, ' ')
|
|
44
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)))
|
|
45
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
46
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
47
|
+
.trim());
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const res = await fetch('https://www.helius.dev/blog/alpenglow');
|
|
51
|
+
const html = await res.text();
|
|
52
|
+
const text = htmlToText(html);
|
|
53
|
+
console.log('=== htmlToText Output (first 3000 chars) ===\n');
|
|
54
|
+
console.log(text.slice(0, 3000));
|
|
55
|
+
console.log('\n=== Stats ===');
|
|
56
|
+
console.log(`HTML length: ${html.length}`);
|
|
57
|
+
console.log(`Text length: ${text.length}`);
|
|
58
|
+
console.log(`Compression ratio: ${((1 - text.length / html.length) * 100).toFixed(1)}%`);
|
|
59
|
+
console.log(`Has ## headers: ${/^## /m.test(text)}`);
|
|
60
|
+
console.log(`Has ### headers: ${/^### /m.test(text)}`);
|
|
61
|
+
console.log(`Has **bold**: ${/\*\*[^*]+\*\*/.test(text)}`);
|
|
62
|
+
console.log(`Has [links](url): ${/\[[^\]]+\]\([^)]+\)/.test(text)}`);
|
|
63
|
+
console.log(`Has bullet lists: ${/^- /m.test(text)}`);
|
|
64
|
+
console.log(`Residual HTML tags: ${(text.match(/<[^>]+>/g) || []).length}`);
|
|
65
|
+
}
|
|
66
|
+
main();
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test for solana-knowledge tools.
|
|
3
|
+
*
|
|
4
|
+
* Run after build:
|
|
5
|
+
* node dist/scripts/test-solana-knowledge.js
|
|
6
|
+
*
|
|
7
|
+
* Tests each tool against live endpoints and validates the response shape.
|
|
8
|
+
*/
|
|
9
|
+
const PASS = '\x1b[32m✅\x1b[0m';
|
|
10
|
+
const FAIL = '\x1b[31m❌\x1b[0m';
|
|
11
|
+
const WARN = '\x1b[33m⚠️\x1b[0m';
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
function assert(condition, label, detail) {
|
|
15
|
+
if (condition) {
|
|
16
|
+
console.log(`${PASS} ${label}`);
|
|
17
|
+
passed++;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(`${FAIL} ${label}${detail ? ` — ${detail}` : ''}`);
|
|
21
|
+
failed++;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers — minimal reimplementation to test without starting an MCP server
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const SIMD_REPO = 'solana-foundation/solana-improvement-documents';
|
|
28
|
+
const SIMD_API_URL = `https://api.github.com/repos/${SIMD_REPO}/contents/proposals`;
|
|
29
|
+
const SIMD_RAW_BASE = `https://raw.githubusercontent.com/${SIMD_REPO}/main/proposals`;
|
|
30
|
+
const UA = 'helius-mcp-test';
|
|
31
|
+
function ghHeaders() {
|
|
32
|
+
const h = { Accept: 'application/vnd.github.v3+json' };
|
|
33
|
+
if (process.env.GITHUB_TOKEN)
|
|
34
|
+
h['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
|
|
35
|
+
return h;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Test: getSIMD
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
async function testGetSIMD() {
|
|
41
|
+
console.log('\n--- getSIMD ---');
|
|
42
|
+
// 1. Fetch SIMD index
|
|
43
|
+
const indexRes = await fetch(SIMD_API_URL, { headers: { 'User-Agent': UA, ...ghHeaders() } });
|
|
44
|
+
assert(indexRes.ok, 'SIMD index fetch succeeds', `status=${indexRes.status}`);
|
|
45
|
+
const files = (await indexRes.json());
|
|
46
|
+
const mdFiles = files.filter((f) => f.name.endsWith('.md'));
|
|
47
|
+
assert(mdFiles.length > 50, `SIMD index has >50 proposals (got ${mdFiles.length})`);
|
|
48
|
+
// 2. Parse filenames
|
|
49
|
+
const parsed = mdFiles
|
|
50
|
+
.map((f) => {
|
|
51
|
+
const match = f.name.match(/^(\d+)-(.+)\.md$/);
|
|
52
|
+
return match ? { number: match[1], slug: match[2], filename: f.name } : null;
|
|
53
|
+
})
|
|
54
|
+
.filter((x) => x !== null);
|
|
55
|
+
assert(parsed.length === mdFiles.length, `All ${mdFiles.length} filenames parsed correctly`);
|
|
56
|
+
// 3. Fetch a known SIMD (SIMD-0096)
|
|
57
|
+
const entry96 = parsed.find((e) => e.number === '0096');
|
|
58
|
+
assert(!!entry96, 'SIMD-0096 exists in index');
|
|
59
|
+
if (entry96) {
|
|
60
|
+
const rawRes = await fetch(`${SIMD_RAW_BASE}/${entry96.filename}`, { headers: { 'User-Agent': UA } });
|
|
61
|
+
assert(rawRes.ok, 'SIMD-0096 raw fetch succeeds');
|
|
62
|
+
const content = await rawRes.text();
|
|
63
|
+
assert(content.length > 500, `SIMD-0096 has substantial content (${content.length} chars)`);
|
|
64
|
+
assert(content.includes('---'), 'SIMD-0096 has YAML frontmatter delimiter');
|
|
65
|
+
}
|
|
66
|
+
// 4. Test number padding logic
|
|
67
|
+
const padTest = (input, expected) => {
|
|
68
|
+
const result = input.replace(/^0+/, '').padStart(4, '0');
|
|
69
|
+
assert(result === expected, `Padding "${input}" → "${result}" (expected "${expected}")`);
|
|
70
|
+
};
|
|
71
|
+
padTest('228', '0228');
|
|
72
|
+
padTest('0096', '0096');
|
|
73
|
+
padTest('1', '0001');
|
|
74
|
+
padTest('0001', '0001');
|
|
75
|
+
// 5. Test not-found with nearby suggestions
|
|
76
|
+
const entry9999 = parsed.find((e) => e.number === '9999');
|
|
77
|
+
assert(!entry9999, 'SIMD-9999 does not exist (good — tests not-found path)');
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Test: listSIMDs
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
async function testListSIMDs() {
|
|
83
|
+
console.log('\n--- listSIMDs ---');
|
|
84
|
+
// Already tested via getSIMD index fetch — just verify table format
|
|
85
|
+
const indexRes = await fetch(SIMD_API_URL, { headers: { 'User-Agent': UA, ...ghHeaders() } });
|
|
86
|
+
const files = (await indexRes.json());
|
|
87
|
+
const parsed = files
|
|
88
|
+
.filter((f) => f.name.endsWith('.md'))
|
|
89
|
+
.map((f) => {
|
|
90
|
+
const match = f.name.match(/^(\d+)-(.+)\.md$/);
|
|
91
|
+
return match ? { number: match[1], slug: match[2] } : null;
|
|
92
|
+
})
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
const tableRow = `| ${parsed[0].number} | ${parsed[0].slug.replace(/-/g, ' ')} |`;
|
|
95
|
+
assert(tableRow.includes('|'), 'Table row format is valid markdown');
|
|
96
|
+
assert(parsed.length > 0, `listSIMDs would return ${parsed.length} entries`);
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Test: readSolanaSourceFile
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
async function testReadSolanaSourceFile() {
|
|
102
|
+
console.log('\n--- readSolanaSourceFile ---');
|
|
103
|
+
// 1. Agave — known file
|
|
104
|
+
const agaveUrl = 'https://raw.githubusercontent.com/anza-xyz/agave/master/svm/src/lib.rs';
|
|
105
|
+
const agaveRes = await fetch(agaveUrl, { headers: { 'User-Agent': UA } });
|
|
106
|
+
assert(agaveRes.ok, `Agave svm/src/lib.rs exists (status=${agaveRes.status})`);
|
|
107
|
+
const agaveContent = await agaveRes.text();
|
|
108
|
+
assert(agaveContent.length > 0, `Agave svm/src/lib.rs has content (${agaveContent.length} chars)`);
|
|
109
|
+
// 2. Firedancer — test with correct default branch ("main")
|
|
110
|
+
const fdUrl = 'https://raw.githubusercontent.com/firedancer-io/firedancer/main/README.md';
|
|
111
|
+
const fdRes = await fetch(fdUrl, { headers: { 'User-Agent': UA } });
|
|
112
|
+
assert(fdRes.ok, `Firedancer README.md on "main" branch exists (status=${fdRes.status})`);
|
|
113
|
+
// 3. Firedancer — verify default branch is "main" via GitHub API
|
|
114
|
+
const fdApiRes = await fetch('https://api.github.com/repos/firedancer-io/firedancer', {
|
|
115
|
+
headers: { 'User-Agent': UA, ...ghHeaders() },
|
|
116
|
+
});
|
|
117
|
+
if (fdApiRes.ok) {
|
|
118
|
+
const fdRepo = (await fdApiRes.json());
|
|
119
|
+
assert(fdRepo.default_branch === 'main', `Firedancer default branch is "${fdRepo.default_branch}" (expected "main")`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(`${WARN} Could not check Firedancer default branch (status=${fdApiRes.status})`);
|
|
123
|
+
}
|
|
124
|
+
// 4. Non-existent file returns error
|
|
125
|
+
const badUrl = 'https://raw.githubusercontent.com/anza-xyz/agave/master/this/does/not/exist.rs';
|
|
126
|
+
const badRes = await fetch(badUrl, { headers: { 'User-Agent': UA } });
|
|
127
|
+
assert(!badRes.ok, `Non-existent path returns error (status=${badRes.status})`);
|
|
128
|
+
// 5. Truncation logic
|
|
129
|
+
const longContent = 'x'.repeat(60_000);
|
|
130
|
+
const MAX_CHARS = 50_000;
|
|
131
|
+
const truncated = longContent.length > MAX_CHARS;
|
|
132
|
+
assert(truncated, 'Content >50k chars is detected as needing truncation');
|
|
133
|
+
const display = longContent.slice(0, MAX_CHARS);
|
|
134
|
+
assert(display.length === MAX_CHARS, `Truncated to exactly ${MAX_CHARS} chars`);
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Test: searchSolanaDocs
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
async function testSearchSolanaDocs() {
|
|
140
|
+
console.log('\n--- searchSolanaDocs ---');
|
|
141
|
+
// 1. Fetch the docs
|
|
142
|
+
const docsRes = await fetch('https://solana.com/llms-full.txt', { headers: { 'User-Agent': UA } });
|
|
143
|
+
assert(docsRes.ok, `Solana llms-full.txt fetch succeeds (status=${docsRes.status})`);
|
|
144
|
+
const docsContent = await docsRes.text();
|
|
145
|
+
assert(docsContent.length > 10_000, `Solana docs has substantial content (${docsContent.length} chars)`);
|
|
146
|
+
// 2. Test section extraction for "Accounts"
|
|
147
|
+
const lines = docsContent.split('\n');
|
|
148
|
+
const accountSections = [];
|
|
149
|
+
let inSection = false;
|
|
150
|
+
let sectionDepth = 0;
|
|
151
|
+
let sectionLines = [];
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const headerMatch = line.match(/^(#{1,4})\s+(.+)/);
|
|
154
|
+
if (headerMatch) {
|
|
155
|
+
const depth = headerMatch[1].length;
|
|
156
|
+
const title = headerMatch[2].toLowerCase();
|
|
157
|
+
if (inSection && depth <= sectionDepth) {
|
|
158
|
+
accountSections.push(sectionLines.join('\n'));
|
|
159
|
+
sectionLines = [];
|
|
160
|
+
inSection = false;
|
|
161
|
+
}
|
|
162
|
+
if (title.includes('accounts')) {
|
|
163
|
+
inSection = true;
|
|
164
|
+
sectionDepth = depth;
|
|
165
|
+
sectionLines.push(line);
|
|
166
|
+
}
|
|
167
|
+
else if (inSection) {
|
|
168
|
+
sectionLines.push(line);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (inSection) {
|
|
172
|
+
sectionLines.push(line);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (inSection)
|
|
176
|
+
accountSections.push(sectionLines.join('\n'));
|
|
177
|
+
assert(accountSections.length > 0, `Found ${accountSections.length} sections matching "accounts"`);
|
|
178
|
+
// 3. Test empty queryWords edge case (the bug we fixed)
|
|
179
|
+
const shortQuery = 'a b';
|
|
180
|
+
const shortQueryLower = shortQuery.toLowerCase();
|
|
181
|
+
const shortQueryWords = shortQueryLower.split(/\s+/).filter((w) => w.length > 2);
|
|
182
|
+
assert(shortQueryWords.length === 0, 'Short query "a b" produces empty queryWords');
|
|
183
|
+
// With our fix, the condition `queryWords.length > 0 && queryWords.every(...)` prevents vacuous truth
|
|
184
|
+
const wouldMatchAll = shortQueryWords.length > 0 && shortQueryWords.every(() => true);
|
|
185
|
+
assert(!wouldMatchAll, 'Empty queryWords does NOT vacuously match all headers (bug fixed)');
|
|
186
|
+
// 4. Test "PDAs" query
|
|
187
|
+
const pdaLines = lines.filter((l) => l.toLowerCase().includes('pda'));
|
|
188
|
+
assert(pdaLines.length > 0, `"PDAs" finds ${pdaLines.length} matching lines in Solana docs`);
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Test: fetchHeliusBlog
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
async function testFetchHeliusBlog() {
|
|
194
|
+
console.log('\n--- fetchHeliusBlog ---');
|
|
195
|
+
// 1. Test blog index size
|
|
196
|
+
const BLOG_INDEX_SIZE = 55; // approximate — we have ~55 curated posts
|
|
197
|
+
// We can't import the actual index, so just validate the fetch works
|
|
198
|
+
// 2. Fetch a known blog post
|
|
199
|
+
const blogUrl = 'https://www.helius.dev/blog/solana-virtual-machine';
|
|
200
|
+
const blogRes = await fetch(blogUrl, { headers: { 'User-Agent': UA } });
|
|
201
|
+
assert(blogRes.ok, `Blog post "solana-virtual-machine" fetch succeeds (status=${blogRes.status})`);
|
|
202
|
+
const html = await blogRes.text();
|
|
203
|
+
assert(html.length > 1000, `Blog HTML has content (${html.length} chars)`);
|
|
204
|
+
// 3. Test htmlToText conversion
|
|
205
|
+
const hasArticle = /<article/i.test(html);
|
|
206
|
+
const hasMain = /<main/i.test(html);
|
|
207
|
+
assert(hasArticle || hasMain, `Blog HTML has <article> or <main> tag for content extraction`);
|
|
208
|
+
// Basic HTML-to-text test
|
|
209
|
+
const simpleHtml = '<h2>Test Header</h2><p>Some <strong>bold</strong> text.</p>';
|
|
210
|
+
const converted = simpleHtml
|
|
211
|
+
.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n')
|
|
212
|
+
.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '\n$1\n')
|
|
213
|
+
.replace(/<(?:strong|b)[^>]*>([\s\S]*?)<\/(?:strong|b)>/gi, '**$1**')
|
|
214
|
+
.replace(/<[^>]+>/g, '')
|
|
215
|
+
.trim();
|
|
216
|
+
assert(converted.includes('## Test Header'), 'h2 converted to ## markdown');
|
|
217
|
+
assert(converted.includes('**bold**'), 'strong converted to **bold**');
|
|
218
|
+
// 4. Test a non-existent slug returns error
|
|
219
|
+
const badBlogUrl = 'https://www.helius.dev/blog/this-post-does-not-exist-12345';
|
|
220
|
+
const badBlogRes = await fetch(badBlogUrl, { headers: { 'User-Agent': UA } });
|
|
221
|
+
// Helius might return 200 with a "not found" page or 404 — either way test completes
|
|
222
|
+
console.log(`${WARN} Non-existent blog slug returns status=${badBlogRes.status} (expected 404 or redirect)`);
|
|
223
|
+
// 5. Fetch another post to verify consistency
|
|
224
|
+
const blog2Url = 'https://www.helius.dev/blog/alpenglow';
|
|
225
|
+
const blog2Res = await fetch(blog2Url, { headers: { 'User-Agent': UA } });
|
|
226
|
+
assert(blog2Res.ok, `Blog post "alpenglow" fetch succeeds (status=${blog2Res.status})`);
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Run all tests
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
async function main() {
|
|
232
|
+
console.log('=== Solana Knowledge Tools — Integration Tests ===\n');
|
|
233
|
+
try {
|
|
234
|
+
await testGetSIMD();
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
console.log(`${FAIL} getSIMD test threw: ${e}`);
|
|
238
|
+
failed++;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
await testListSIMDs();
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.log(`${FAIL} listSIMDs test threw: ${e}`);
|
|
245
|
+
failed++;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
await testReadSolanaSourceFile();
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
console.log(`${FAIL} readSolanaSourceFile test threw: ${e}`);
|
|
252
|
+
failed++;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await testSearchSolanaDocs();
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
console.log(`${FAIL} searchSolanaDocs test threw: ${e}`);
|
|
259
|
+
failed++;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
await testFetchHeliusBlog();
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
console.log(`${FAIL} fetchHeliusBlog test threw: ${e}`);
|
|
266
|
+
failed++;
|
|
267
|
+
}
|
|
268
|
+
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
|
269
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
270
|
+
}
|
|
271
|
+
main();
|
|
272
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validates recommendStack template data for correctness.
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* 1. MCP tool names exist in KNOWN_TOOLS
|
|
7
|
+
* 2. Reference paths exist on disk
|
|
8
|
+
* 3. Plan-feature compatibility (e.g., Laserstream mainnet requires professional)
|
|
9
|
+
* 4. Tier ordering (budget <= standard <= production plan rank)
|
|
10
|
+
* 5. No empty arrays (products, limitations, references)
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validates recommendStack template data for correctness.
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* 1. MCP tool names exist in KNOWN_TOOLS
|
|
7
|
+
* 2. Reference paths exist on disk
|
|
8
|
+
* 3. Plan-feature compatibility (e.g., Laserstream mainnet requires professional)
|
|
9
|
+
* 4. Tier ordering (budget <= standard <= production plan rank)
|
|
10
|
+
* 5. No empty arrays (products, limitations, references)
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { PROJECT_TEMPLATES, KNOWN_TOOLS } from '../tools/recommend.js';
|
|
15
|
+
import { HELIUS_PLANS } from '../tools/plans.js';
|
|
16
|
+
const PLAN_RANK = { free: 0, developer: 1, business: 2, professional: 3 };
|
|
17
|
+
const TIER_RANK = { budget: 0, standard: 1, production: 2 };
|
|
18
|
+
// Resolve skill references relative to repo root
|
|
19
|
+
const REPO_ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
|
|
20
|
+
const SKILL_DIR = path.join(REPO_ROOT, 'helius-skills', 'helius');
|
|
21
|
+
let errors = [];
|
|
22
|
+
function error(templateName, msg) {
|
|
23
|
+
errors.push(`[${templateName}] ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
for (const [key, template] of Object.entries(PROJECT_TEMPLATES)) {
|
|
26
|
+
// ── Check tier ordering ──
|
|
27
|
+
const sorted = [...template.tiers].sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier]);
|
|
28
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
29
|
+
const prev = sorted[i - 1];
|
|
30
|
+
const curr = sorted[i];
|
|
31
|
+
if ((PLAN_RANK[curr.minimumPlan] ?? 0) < (PLAN_RANK[prev.minimumPlan] ?? 0)) {
|
|
32
|
+
error(key, `Tier ordering violation: ${curr.tier} (${curr.minimumPlan}) has lower plan rank than ${prev.tier} (${prev.minimumPlan})`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const tier of template.tiers) {
|
|
36
|
+
const tierLabel = `${key}/${tier.tier}`;
|
|
37
|
+
// ── No empty arrays ──
|
|
38
|
+
if (tier.products.length === 0) {
|
|
39
|
+
error(tierLabel, 'products array is empty');
|
|
40
|
+
}
|
|
41
|
+
if (tier.limitations.length === 0) {
|
|
42
|
+
error(tierLabel, 'limitations array is empty');
|
|
43
|
+
}
|
|
44
|
+
if (tier.references.length === 0) {
|
|
45
|
+
error(tierLabel, 'references array is empty');
|
|
46
|
+
}
|
|
47
|
+
// ── Validate each product ──
|
|
48
|
+
for (const product of tier.products) {
|
|
49
|
+
// MCP tool name validation
|
|
50
|
+
for (const tool of product.mcpTools) {
|
|
51
|
+
if (!KNOWN_TOOLS.has(tool)) {
|
|
52
|
+
error(tierLabel, `Unknown MCP tool "${tool}" in product "${product.product}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Plan-feature compatibility
|
|
56
|
+
if (!(product.minimumPlan in PLAN_RANK)) {
|
|
57
|
+
error(tierLabel, `Unknown plan "${product.minimumPlan}" in product "${product.product}"`);
|
|
58
|
+
}
|
|
59
|
+
if (!(product.minimumPlan in HELIUS_PLANS)) {
|
|
60
|
+
error(tierLabel, `Plan "${product.minimumPlan}" not found in HELIUS_PLANS for product "${product.product}"`);
|
|
61
|
+
}
|
|
62
|
+
// Laserstream mainnet must require professional
|
|
63
|
+
const nameLower = product.product.toLowerCase();
|
|
64
|
+
if (nameLower.includes('laserstream') && nameLower.includes('mainnet') && product.minimumPlan !== 'professional') {
|
|
65
|
+
error(tierLabel, `Laserstream mainnet in product "${product.product}" requires professional plan, but has "${product.minimumPlan}"`);
|
|
66
|
+
}
|
|
67
|
+
// Enhanced WebSockets must require business+
|
|
68
|
+
if (nameLower.includes('enhanced websocket') && (PLAN_RANK[product.minimumPlan] ?? 0) < PLAN_RANK['business']) {
|
|
69
|
+
error(tierLabel, `Enhanced WebSockets in product "${product.product}" requires business+ plan, but has "${product.minimumPlan}"`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── Reference path validation ──
|
|
73
|
+
for (const ref of tier.references) {
|
|
74
|
+
const refPath = path.join(SKILL_DIR, ref);
|
|
75
|
+
if (!fs.existsSync(refPath)) {
|
|
76
|
+
error(tierLabel, `Reference file not found: ${ref} (looked at ${refPath})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ── Report ──
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
console.error(`\n❌ Template validation failed with ${errors.length} error(s):\n`);
|
|
84
|
+
for (const err of errors) {
|
|
85
|
+
console.error(` • ${err}`);
|
|
86
|
+
}
|
|
87
|
+
console.error('');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const templateCount = Object.keys(PROJECT_TEMPLATES).length;
|
|
92
|
+
const tierCount = Object.values(PROJECT_TEMPLATES).reduce((sum, t) => sum + t.tiers.length, 0);
|
|
93
|
+
console.log(`✅ All templates valid (${templateCount} templates, ${tierCount} tiers)`);
|
|
94
|
+
}
|
package/dist/tools/auth.js
CHANGED
|
@@ -16,6 +16,9 @@ import { sendFeedbackEvent, captureWalletAddress } from '../utils/feedback.js';
|
|
|
16
16
|
import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk } from '../utils/config.js';
|
|
17
17
|
import { HELIUS_PLANS } from './plans.js';
|
|
18
18
|
const PAID_PLAN_ORDER = ['developer', 'business', 'professional'];
|
|
19
|
+
/** Tracks consecutive insufficient-balance checks to prevent agent polling loops. */
|
|
20
|
+
let insufficientBalanceChecks = 0;
|
|
21
|
+
const MAX_BALANCE_CHECKS_BEFORE_STOP = 3;
|
|
19
22
|
export function registerAuthTools(server) {
|
|
20
23
|
// ── Getting Started Guide ──
|
|
21
24
|
server.tool('getStarted', 'Get setup instructions for Helius. Checks whether an API key is configured (not validated), whether a keypair exists on disk, and whether a JWT session is present, then tells you exactly what to do next. Call this when a user asks "how do I get started?" or needs onboarding help.', {}, async () => {
|
|
@@ -48,6 +51,8 @@ export function registerAuthTools(server) {
|
|
|
48
51
|
// ── Keypair Generation ──
|
|
49
52
|
server.tool('generateKeypair', 'Generate a new Solana keypair for Helius account signup. Returns the wallet address. The user must fund this wallet with ~0.001 SOL + 1 USDC (basic plan) or more USDC (for paid plans) before calling agenticSignup.', {}, async () => {
|
|
50
53
|
try {
|
|
54
|
+
// Reset balance-check counter for fresh signup flow
|
|
55
|
+
insufficientBalanceChecks = 0;
|
|
51
56
|
// Check disk first — reuse existing keypair if available
|
|
52
57
|
const existingKey = loadKeypairFromDisk();
|
|
53
58
|
if (existingKey) {
|
|
@@ -105,22 +110,48 @@ export function registerAuthTools(server) {
|
|
|
105
110
|
const usdcAmount = Number(usdcBalance) / 1_000_000;
|
|
106
111
|
const solOk = solBalance >= 1000000n;
|
|
107
112
|
const usdcOk = usdcBalance >= 1000000n; // 1 USDC for basic plan
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
const funded = solOk && usdcOk;
|
|
114
|
+
// Reset counter when balance is sufficient
|
|
115
|
+
if (funded) {
|
|
116
|
+
insufficientBalanceChecks = 0;
|
|
117
|
+
return mcpText(`**Signup Wallet Balance** (\`${address}\`)\n\n` +
|
|
118
|
+
`- **SOL:** ${solAmount.toFixed(6)} (sufficient)\n` +
|
|
119
|
+
`- **USDC:** ${usdcAmount.toFixed(2)} (sufficient for basic)\n\n` +
|
|
120
|
+
`**Status:** Ready for signup (basic plan). For paid plans, ensure sufficient USDC for the plan price.\n\n` +
|
|
121
|
+
`Call \`agenticSignup\` to proceed.`);
|
|
122
|
+
}
|
|
123
|
+
// Insufficient — increment counter and escalate guidance
|
|
124
|
+
insufficientBalanceChecks++;
|
|
125
|
+
const missing = [];
|
|
126
|
+
if (!solOk)
|
|
127
|
+
missing.push(`~0.001 SOL (have ${solAmount.toFixed(6)})`);
|
|
128
|
+
if (!usdcOk)
|
|
129
|
+
missing.push(`1 USDC (have ${usdcAmount.toFixed(2)})`);
|
|
130
|
+
let balanceBlock = `**Signup Wallet Balance** (\`${address}\`)\n\n` +
|
|
121
131
|
`- **SOL:** ${solAmount.toFixed(6)} ${solOk ? '(sufficient)' : '(insufficient)'}\n` +
|
|
122
132
|
`- **USDC:** ${usdcAmount.toFixed(2)} ${usdcOk ? '(sufficient for basic)' : '(insufficient)'}\n\n` +
|
|
123
|
-
`**Status:** ${
|
|
133
|
+
`**Status:** Need more funds: ${missing.join(', ')}`;
|
|
134
|
+
if (insufficientBalanceChecks === 1) {
|
|
135
|
+
// First check — normal guidance
|
|
136
|
+
balanceBlock +=
|
|
137
|
+
`\n\n**Action required:** Ask the user to send the missing funds to \`${address}\`. ` +
|
|
138
|
+
`Do **not** call \`checkSignupBalance\` again until the user confirms they have sent the funds.`;
|
|
139
|
+
}
|
|
140
|
+
else if (insufficientBalanceChecks < MAX_BALANCE_CHECKS_BEFORE_STOP) {
|
|
141
|
+
// Second check — firmer nudge
|
|
142
|
+
balanceBlock +=
|
|
143
|
+
`\n\n**⚠ Balance still insufficient (check ${insufficientBalanceChecks}/${MAX_BALANCE_CHECKS_BEFORE_STOP}).** ` +
|
|
144
|
+
`The wallet has not been funded yet. Ask the user to confirm they have sent funds to \`${address}\` before calling this tool again.`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Third+ check — hard stop
|
|
148
|
+
balanceBlock +=
|
|
149
|
+
`\n\n**🛑 Balance checked ${insufficientBalanceChecks} times — still insufficient. Stop polling.** ` +
|
|
150
|
+
`The wallet \`${address}\` has not received funds. ` +
|
|
151
|
+
`Tell the user the exact amounts needed and the wallet address, then **wait for the user to explicitly confirm** they have sent funds before calling \`checkSignupBalance\` again. ` +
|
|
152
|
+
`Do not retry automatically.`;
|
|
153
|
+
}
|
|
154
|
+
return mcpText(balanceBlock);
|
|
124
155
|
}
|
|
125
156
|
catch (err) {
|
|
126
157
|
return handleToolError(err, 'Error checking balances');
|
package/dist/tools/network.js
CHANGED
|
@@ -3,17 +3,18 @@ import { formatSolCompact } from '../utils/formatters.js';
|
|
|
3
3
|
import { noApiKeyResponse } from './shared.js';
|
|
4
4
|
import { mcpText, handleToolError } from '../utils/errors.js';
|
|
5
5
|
export function registerNetworkTools(server) {
|
|
6
|
-
server.tool('getNetworkStatus', 'BEST FOR: quick Solana network health check — epoch, slot, supply, version. Get current Solana network status including epoch info (current epoch, slot, progress), total SOL supply, cluster version, and current block height. No parameters needed — gives a quick overview of blockchain health and state. Credit cost:
|
|
6
|
+
server.tool('getNetworkStatus', 'BEST FOR: quick Solana network health check — epoch, slot, TPS, supply, version. Get current Solana network status including epoch info (current epoch, slot, progress), current TPS (transactions per second), total SOL supply, cluster version, and current block height. No parameters needed — gives a quick overview of blockchain health and state. Credit cost: 4 credits (4 standard RPC calls).', {}, async () => {
|
|
7
7
|
if (!hasApiKey())
|
|
8
8
|
return noApiKeyResponse();
|
|
9
9
|
try {
|
|
10
10
|
const helius = getHeliusClient();
|
|
11
11
|
// Fire all requests in parallel — Kit returns bigint for numeric fields
|
|
12
12
|
// wrapAutoSend in the SDK already calls .send() on pending RPC requests
|
|
13
|
-
const [epochInfo, supplyResult, version] = await Promise.all([
|
|
13
|
+
const [epochInfo, supplyResult, version, perfSamples] = await Promise.all([
|
|
14
14
|
helius.getEpochInfo(),
|
|
15
15
|
helius.getSupply(),
|
|
16
16
|
helius.getVersion(),
|
|
17
|
+
helius.getRecentPerformanceSamples(4),
|
|
17
18
|
]);
|
|
18
19
|
const lines = ['**Solana Network Status**', ''];
|
|
19
20
|
// Epoch info — all fields are bigint from Kit
|
|
@@ -35,6 +36,38 @@ export function registerNetworkTools(server) {
|
|
|
35
36
|
}
|
|
36
37
|
lines.push('');
|
|
37
38
|
}
|
|
39
|
+
// TPS — averaged from 4 recent performance samples (~60s each, ~4 min window)
|
|
40
|
+
// numTransactions includes vote + non-vote; numNonVoteTransactions is real user TPS
|
|
41
|
+
if (Array.isArray(perfSamples) && perfSamples.length > 0) {
|
|
42
|
+
let totalTx = 0;
|
|
43
|
+
let totalNonVoteTx = 0;
|
|
44
|
+
let totalSeconds = 0;
|
|
45
|
+
let hasNonVoteData = false;
|
|
46
|
+
for (const sample of perfSamples) {
|
|
47
|
+
const samplePeriod = Number(sample.samplePeriodSecs);
|
|
48
|
+
if (samplePeriod > 0) {
|
|
49
|
+
totalTx += Number(sample.numTransactions);
|
|
50
|
+
totalSeconds += samplePeriod;
|
|
51
|
+
if (sample.numNonVoteTransactions != null) {
|
|
52
|
+
totalNonVoteTx += Number(sample.numNonVoteTransactions);
|
|
53
|
+
hasNonVoteData = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (totalSeconds > 0) {
|
|
58
|
+
const totalTps = Math.round(totalTx / totalSeconds);
|
|
59
|
+
lines.push('**TPS (avg. last ~4 min):**');
|
|
60
|
+
if (hasNonVoteData) {
|
|
61
|
+
const nonVoteTps = Math.round(totalNonVoteTx / totalSeconds);
|
|
62
|
+
lines.push(`- Real (non-vote): ~${nonVoteTps.toLocaleString()} tx/sec`);
|
|
63
|
+
lines.push(`- Total (incl. vote): ~${totalTps.toLocaleString()} tx/sec`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push(`- Total: ~${totalTps.toLocaleString()} tx/sec (includes vote transactions)`);
|
|
67
|
+
}
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
38
71
|
// Supply — value fields are bigint from Kit
|
|
39
72
|
if (supplyResult?.value) {
|
|
40
73
|
const supply = supplyResult.value;
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const NO_API_KEY_MESSAGE = `**Helius API Key Required**
|
|
2
|
-
|
|
3
|
-
I need a Helius API key to query the Solana blockchain.
|
|
4
|
-
|
|
5
|
-
**Option 1 — Sign up here (recommended):**
|
|
6
|
-
1. Call \`generateKeypair\` to create a signup wallet
|
|
7
|
-
2. Fund the wallet with ~0.001 SOL + 1 USDC
|
|
8
|
-
3. Call \`agenticSignup\` to create your account — the API key will be configured automatically
|
|
9
|
-
|
|
10
|
-
**Option 2 — Use an existing key:**
|
|
11
|
-
1. Go to https://dashboard.helius.dev/api-keys
|
|
12
|
-
2. Copy your API key
|
|
1
|
+
const NO_API_KEY_MESSAGE = `**Helius API Key Required**
|
|
2
|
+
|
|
3
|
+
I need a Helius API key to query the Solana blockchain.
|
|
4
|
+
|
|
5
|
+
**Option 1 — Sign up here (recommended):**
|
|
6
|
+
1. Call \`generateKeypair\` to create a signup wallet
|
|
7
|
+
2. Fund the wallet with ~0.001 SOL + 1 USDC
|
|
8
|
+
3. Call \`agenticSignup\` to create your account — the API key will be configured automatically
|
|
9
|
+
|
|
10
|
+
**Option 2 — Use an existing key:**
|
|
11
|
+
1. Go to https://dashboard.helius.dev/api-keys
|
|
12
|
+
2. Copy your API key
|
|
13
13
|
3. Call \`setHeliusApiKey\` with your key`;
|
|
14
14
|
export function noApiKeyResponse() {
|
|
15
15
|
return {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "1.
|
|
1
|
+
export declare const version = "1.3.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '1.
|
|
1
|
+
export const version = '1.3.0';
|