helius-mcp 1.3.0 → 2.0.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 -79
- package/LICENSE +21 -21
- package/README.md +144 -132
- package/dist/http.d.ts +1 -1
- package/dist/index.js +2 -56
- package/dist/results/store.d.ts +8 -0
- package/dist/results/store.js +72 -0
- package/dist/results/types.d.ts +47 -0
- package/dist/results/types.js +1 -0
- package/dist/router/action-groups.d.ts +6 -0
- package/dist/router/action-groups.js +32 -0
- package/dist/router/action-handlers.d.ts +20 -0
- package/dist/router/action-handlers.js +125 -0
- package/dist/router/actions.d.ts +12 -0
- package/dist/router/actions.js +123 -0
- package/dist/router/catalog.d.ts +6 -0
- package/dist/router/catalog.js +388 -0
- package/dist/router/context.d.ts +5 -0
- package/dist/router/context.js +10 -0
- package/dist/router/dispatch.d.ts +4 -0
- package/dist/router/dispatch.js +276 -0
- package/dist/router/instructions.d.ts +1 -0
- package/dist/router/instructions.js +25 -0
- package/dist/router/register.d.ts +2 -0
- package/dist/router/register.js +15 -0
- package/dist/router/required-params.d.ts +9 -0
- package/dist/router/required-params.js +66 -0
- package/dist/router/responses.d.ts +29 -0
- package/dist/router/responses.js +186 -0
- package/dist/router/schemas.d.ts +216 -0
- package/dist/router/schemas.js +195 -0
- package/dist/router/telemetry.d.ts +27 -0
- package/dist/router/telemetry.js +52 -0
- package/dist/router/types.d.ts +46 -0
- package/dist/router/types.js +1 -0
- package/dist/scripts/validate-catalog.d.ts +2 -2
- package/dist/scripts/validate-catalog.js +10 -10
- package/dist/tools/accounts.js +5 -5
- package/dist/tools/assets.js +5 -5
- package/dist/tools/auth.js +392 -319
- package/dist/tools/config.js +3 -3
- package/dist/tools/das-extras.js +6 -6
- package/dist/tools/docs.js +55 -41
- package/dist/tools/enhanced-websockets.js +13 -13
- package/dist/tools/fees.js +3 -3
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +2 -80
- package/dist/tools/laserstream.js +20 -23
- package/dist/tools/network.js +10 -4
- package/dist/tools/plans.d.ts +0 -5
- package/dist/tools/plans.js +167 -12
- package/dist/tools/product-catalog.d.ts +1 -0
- package/dist/tools/product-catalog.js +51 -16
- package/dist/tools/recommend.d.ts +0 -1
- package/dist/tools/recommend.js +9 -28
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.js +21 -13
- package/dist/tools/solana-knowledge.js +23 -7
- package/dist/tools/staking.d.ts +2 -0
- package/dist/tools/staking.js +268 -0
- package/dist/tools/transactions.js +167 -3
- package/dist/tools/transfers.js +38 -43
- package/dist/tools/wallet.js +27 -16
- package/dist/tools/webhooks.js +3 -3
- package/dist/tools/zk-compression.d.ts +2 -0
- package/dist/tools/zk-compression.js +781 -0
- package/dist/utils/config.d.ts +2 -2
- package/dist/utils/config.js +68 -6
- package/dist/utils/errors.d.ts +10 -1
- package/dist/utils/errors.js +46 -12
- package/dist/utils/feedback.js +1 -4
- package/dist/utils/helius.js +2 -1
- package/dist/utils/ows.d.ts +74 -0
- package/dist/utils/ows.js +155 -0
- 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 +200 -170
- package/system-prompts/helius/full.md +3212 -2869
- package/system-prompts/helius/openai.developer.md +200 -170
- package/system-prompts/helius-dflow/claude.system.md +324 -290
- package/system-prompts/helius-dflow/full.md +4136 -3648
- package/system-prompts/helius-dflow/openai.developer.md +324 -290
- package/system-prompts/helius-jupiter/claude.system.md +333 -0
- package/system-prompts/helius-jupiter/full.md +5109 -0
- package/system-prompts/helius-jupiter/openai.developer.md +333 -0
- package/system-prompts/helius-okx/claude.system.md +182 -0
- package/system-prompts/helius-okx/full.md +584 -0
- package/system-prompts/helius-okx/openai.developer.md +182 -0
- package/system-prompts/helius-phantom/claude.system.md +345 -333
- package/system-prompts/helius-phantom/full.md +5625 -5473
- package/system-prompts/helius-phantom/openai.developer.md +345 -333
- package/system-prompts/svm/claude.system.md +159 -159
- package/system-prompts/svm/full.md +631 -631
- package/system-prompts/svm/openai.developer.md +159 -159
- package/dist/scripts/test-htmltotext.d.ts +0 -5
- package/dist/scripts/test-htmltotext.js +0 -67
- package/dist/scripts/test-solana-knowledge.d.ts +0 -9
- package/dist/scripts/test-solana-knowledge.js +0 -272
- package/dist/scripts/validate-templates.d.ts +0 -12
- package/dist/scripts/validate-templates.js +0 -94
|
@@ -1,272 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,12 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}
|