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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -79
  2. package/LICENSE +21 -21
  3. package/README.md +144 -132
  4. package/dist/http.d.ts +1 -1
  5. package/dist/index.js +2 -56
  6. package/dist/results/store.d.ts +8 -0
  7. package/dist/results/store.js +72 -0
  8. package/dist/results/types.d.ts +47 -0
  9. package/dist/results/types.js +1 -0
  10. package/dist/router/action-groups.d.ts +6 -0
  11. package/dist/router/action-groups.js +32 -0
  12. package/dist/router/action-handlers.d.ts +20 -0
  13. package/dist/router/action-handlers.js +125 -0
  14. package/dist/router/actions.d.ts +12 -0
  15. package/dist/router/actions.js +123 -0
  16. package/dist/router/catalog.d.ts +6 -0
  17. package/dist/router/catalog.js +388 -0
  18. package/dist/router/context.d.ts +5 -0
  19. package/dist/router/context.js +10 -0
  20. package/dist/router/dispatch.d.ts +4 -0
  21. package/dist/router/dispatch.js +276 -0
  22. package/dist/router/instructions.d.ts +1 -0
  23. package/dist/router/instructions.js +25 -0
  24. package/dist/router/register.d.ts +2 -0
  25. package/dist/router/register.js +15 -0
  26. package/dist/router/required-params.d.ts +9 -0
  27. package/dist/router/required-params.js +66 -0
  28. package/dist/router/responses.d.ts +29 -0
  29. package/dist/router/responses.js +186 -0
  30. package/dist/router/schemas.d.ts +216 -0
  31. package/dist/router/schemas.js +195 -0
  32. package/dist/router/telemetry.d.ts +27 -0
  33. package/dist/router/telemetry.js +52 -0
  34. package/dist/router/types.d.ts +46 -0
  35. package/dist/router/types.js +1 -0
  36. package/dist/scripts/validate-catalog.d.ts +2 -2
  37. package/dist/scripts/validate-catalog.js +10 -10
  38. package/dist/tools/accounts.js +5 -5
  39. package/dist/tools/assets.js +5 -5
  40. package/dist/tools/auth.js +392 -319
  41. package/dist/tools/config.js +3 -3
  42. package/dist/tools/das-extras.js +6 -6
  43. package/dist/tools/docs.js +55 -41
  44. package/dist/tools/enhanced-websockets.js +13 -13
  45. package/dist/tools/fees.js +3 -3
  46. package/dist/tools/index.d.ts +1 -1
  47. package/dist/tools/index.js +2 -80
  48. package/dist/tools/laserstream.js +20 -23
  49. package/dist/tools/network.js +10 -4
  50. package/dist/tools/plans.d.ts +0 -5
  51. package/dist/tools/plans.js +167 -12
  52. package/dist/tools/product-catalog.d.ts +1 -0
  53. package/dist/tools/product-catalog.js +51 -16
  54. package/dist/tools/recommend.d.ts +0 -1
  55. package/dist/tools/recommend.js +9 -28
  56. package/dist/tools/shared.d.ts +1 -0
  57. package/dist/tools/shared.js +21 -13
  58. package/dist/tools/solana-knowledge.js +23 -7
  59. package/dist/tools/staking.d.ts +2 -0
  60. package/dist/tools/staking.js +268 -0
  61. package/dist/tools/transactions.js +167 -3
  62. package/dist/tools/transfers.js +38 -43
  63. package/dist/tools/wallet.js +27 -16
  64. package/dist/tools/webhooks.js +3 -3
  65. package/dist/tools/zk-compression.d.ts +2 -0
  66. package/dist/tools/zk-compression.js +781 -0
  67. package/dist/utils/config.d.ts +2 -2
  68. package/dist/utils/config.js +68 -6
  69. package/dist/utils/errors.d.ts +10 -1
  70. package/dist/utils/errors.js +46 -12
  71. package/dist/utils/feedback.js +1 -4
  72. package/dist/utils/helius.js +2 -1
  73. package/dist/utils/ows.d.ts +74 -0
  74. package/dist/utils/ows.js +155 -0
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +64 -64
  78. package/system-prompts/helius/claude.system.md +200 -170
  79. package/system-prompts/helius/full.md +3212 -2869
  80. package/system-prompts/helius/openai.developer.md +200 -170
  81. package/system-prompts/helius-dflow/claude.system.md +324 -290
  82. package/system-prompts/helius-dflow/full.md +4136 -3648
  83. package/system-prompts/helius-dflow/openai.developer.md +324 -290
  84. package/system-prompts/helius-jupiter/claude.system.md +333 -0
  85. package/system-prompts/helius-jupiter/full.md +5109 -0
  86. package/system-prompts/helius-jupiter/openai.developer.md +333 -0
  87. package/system-prompts/helius-okx/claude.system.md +182 -0
  88. package/system-prompts/helius-okx/full.md +584 -0
  89. package/system-prompts/helius-okx/openai.developer.md +182 -0
  90. package/system-prompts/helius-phantom/claude.system.md +345 -333
  91. package/system-prompts/helius-phantom/full.md +5625 -5473
  92. package/system-prompts/helius-phantom/openai.developer.md +345 -333
  93. package/system-prompts/svm/claude.system.md +159 -159
  94. package/system-prompts/svm/full.md +631 -631
  95. package/system-prompts/svm/openai.developer.md +159 -159
  96. package/dist/scripts/test-htmltotext.d.ts +0 -5
  97. package/dist/scripts/test-htmltotext.js +0 -67
  98. package/dist/scripts/test-solana-knowledge.d.ts +0 -9
  99. package/dist/scripts/test-solana-knowledge.js +0 -272
  100. package/dist/scripts/validate-templates.d.ts +0 -12
  101. 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
- }