@luquimbo/bi-superpowers 3.1.1 → 3.2.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 (107) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/skill-manifest.json +1 -1
  4. package/.plugin/plugin.json +1 -1
  5. package/bin/build-plugin.js +6 -6
  6. package/bin/cli.js +169 -310
  7. package/bin/commands/install.js +87 -70
  8. package/bin/lib/agents.js +19 -0
  9. package/bin/lib/mcp-config.js +23 -4
  10. package/desktop-extension/manifest.json +4 -11
  11. package/desktop-extension/server.js +34 -25
  12. package/package.json +3 -9
  13. package/skills/pbi-connect/SKILL.md +1 -1
  14. package/skills/project-kickoff/SKILL.md +1 -1
  15. package/bin/commands/add.js +0 -533
  16. package/bin/commands/add.test.js +0 -77
  17. package/bin/commands/changelog.js +0 -443
  18. package/bin/commands/pull.js +0 -287
  19. package/bin/commands/pull.test.js +0 -36
  20. package/bin/commands/push.js +0 -231
  21. package/bin/commands/push.test.js +0 -14
  22. package/bin/commands/search.js +0 -344
  23. package/bin/commands/search.test.js +0 -115
  24. package/bin/commands/setup.js +0 -545
  25. package/bin/commands/setup.test.js +0 -46
  26. package/bin/commands/sync-profile.js +0 -405
  27. package/bin/commands/sync-profile.test.js +0 -14
  28. package/bin/commands/sync-source.js +0 -418
  29. package/bin/commands/sync-source.test.js +0 -14
  30. package/bin/utils/errors.js +0 -159
  31. package/bin/utils/git.js +0 -298
  32. package/bin/utils/logger.js +0 -142
  33. package/bin/utils/pbix.js +0 -305
  34. package/bin/utils/pbix.test.js +0 -37
  35. package/bin/utils/profiles.js +0 -312
  36. package/bin/utils/projects.js +0 -169
  37. package/bin/utils/readline.js +0 -206
  38. package/bin/utils/readline.test.js +0 -47
  39. package/docs/openrouter-free-models.md +0 -92
  40. package/library/examples/README.md +0 -151
  41. package/library/examples/finance-reporting/README.md +0 -351
  42. package/library/examples/finance-reporting/data-model.md +0 -267
  43. package/library/examples/finance-reporting/measures.dax +0 -557
  44. package/library/examples/hr-analytics/README.md +0 -371
  45. package/library/examples/hr-analytics/data-model.md +0 -315
  46. package/library/examples/hr-analytics/measures.dax +0 -460
  47. package/library/examples/marketing-analytics/README.md +0 -37
  48. package/library/examples/marketing-analytics/data-model.md +0 -62
  49. package/library/examples/marketing-analytics/measures.dax +0 -110
  50. package/library/examples/retail-analytics/README.md +0 -439
  51. package/library/examples/retail-analytics/data-model.md +0 -288
  52. package/library/examples/retail-analytics/measures.dax +0 -481
  53. package/library/examples/supply-chain/README.md +0 -37
  54. package/library/examples/supply-chain/data-model.md +0 -69
  55. package/library/examples/supply-chain/measures.dax +0 -77
  56. package/library/examples/udf-library/README.md +0 -228
  57. package/library/examples/udf-library/functions.dax +0 -571
  58. package/library/snippets/dax/README.md +0 -292
  59. package/library/snippets/dax/business-domains.md +0 -576
  60. package/library/snippets/dax/calculate-patterns.md +0 -276
  61. package/library/snippets/dax/calculation-groups.md +0 -489
  62. package/library/snippets/dax/error-handling.md +0 -495
  63. package/library/snippets/dax/iterators-and-aggregations.md +0 -474
  64. package/library/snippets/dax/kpis-and-metrics.md +0 -293
  65. package/library/snippets/dax/rankings-and-topn.md +0 -235
  66. package/library/snippets/dax/security-patterns.md +0 -413
  67. package/library/snippets/dax/text-and-formatting.md +0 -316
  68. package/library/snippets/dax/time-intelligence.md +0 -196
  69. package/library/snippets/dax/user-defined-functions.md +0 -477
  70. package/library/snippets/dax/virtual-tables.md +0 -546
  71. package/library/snippets/excel-formulas/README.md +0 -84
  72. package/library/snippets/excel-formulas/aggregations.md +0 -330
  73. package/library/snippets/excel-formulas/dates-and-times.md +0 -361
  74. package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
  75. package/library/snippets/excel-formulas/lookups.md +0 -169
  76. package/library/snippets/excel-formulas/text-functions.md +0 -363
  77. package/library/snippets/governance/naming-conventions.md +0 -97
  78. package/library/snippets/governance/review-checklists.md +0 -107
  79. package/library/snippets/power-query/README.md +0 -389
  80. package/library/snippets/power-query/api-integration.md +0 -707
  81. package/library/snippets/power-query/connections.md +0 -434
  82. package/library/snippets/power-query/data-cleaning.md +0 -298
  83. package/library/snippets/power-query/error-handling.md +0 -526
  84. package/library/snippets/power-query/parameters.md +0 -350
  85. package/library/snippets/power-query/performance.md +0 -506
  86. package/library/snippets/power-query/transformations.md +0 -330
  87. package/library/snippets/report-design/accessibility.md +0 -78
  88. package/library/snippets/report-design/chart-selection.md +0 -54
  89. package/library/snippets/report-design/layout-patterns.md +0 -87
  90. package/library/templates/data-models/README.md +0 -93
  91. package/library/templates/data-models/finance-model.md +0 -627
  92. package/library/templates/data-models/retail-star-schema.md +0 -473
  93. package/library/templates/excel/README.md +0 -83
  94. package/library/templates/excel/budget-tracker.md +0 -432
  95. package/library/templates/excel/data-entry-form.md +0 -533
  96. package/library/templates/power-bi/README.md +0 -72
  97. package/library/templates/power-bi/finance-report.md +0 -449
  98. package/library/templates/power-bi/kpi-scorecard.md +0 -461
  99. package/library/templates/power-bi/sales-dashboard.md +0 -281
  100. package/library/themes/excel/README.md +0 -436
  101. package/library/themes/power-bi/README.md +0 -271
  102. package/library/themes/power-bi/accessible.json +0 -307
  103. package/library/themes/power-bi/bi-superpowers-default.json +0 -858
  104. package/library/themes/power-bi/corporate-blue.json +0 -291
  105. package/library/themes/power-bi/dark-mode.json +0 -291
  106. package/library/themes/power-bi/minimal.json +0 -292
  107. package/library/themes/power-bi/print-friendly.json +0 -309
@@ -1,344 +0,0 @@
1
- /**
2
- * Search Command (xray)
3
- * =====================
4
- * Fuzzy search across snippets, skills, and library content.
5
- *
6
- * Usage:
7
- * super xray "query"
8
- * super xray --category dax "query"
9
- * super xray --tag performance
10
- */
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const Fuse = require('fuse.js');
15
- const tui = require('../utils/tui');
16
-
17
- // Fuse.js configuration for fuzzy search
18
- const FUSE_OPTIONS = {
19
- keys: [
20
- { name: 'title', weight: 0.4 },
21
- { name: 'content', weight: 0.3 },
22
- { name: 'category', weight: 0.2 },
23
- { name: 'tags', weight: 0.1 },
24
- ],
25
- threshold: 0.4,
26
- ignoreLocation: true,
27
- includeScore: true,
28
- includeMatches: true,
29
- };
30
-
31
- /**
32
- * Build search index from library content
33
- * @param {string} libraryDir - Path to library directory
34
- * @returns {Object[]} Array of searchable items
35
- */
36
- function buildSearchIndex(libraryDir) {
37
- const items = [];
38
-
39
- if (!fs.existsSync(libraryDir)) {
40
- return items;
41
- }
42
-
43
- // Index snippets
44
- const snippetsDir = path.join(libraryDir, 'snippets');
45
- if (fs.existsSync(snippetsDir)) {
46
- indexDirectory(snippetsDir, items, 'snippet');
47
- }
48
-
49
- // Index templates
50
- const templatesDir = path.join(libraryDir, 'templates');
51
- if (fs.existsSync(templatesDir)) {
52
- indexDirectory(templatesDir, items, 'template');
53
- }
54
-
55
- // Index examples
56
- const examplesDir = path.join(libraryDir, 'examples');
57
- if (fs.existsSync(examplesDir)) {
58
- indexDirectory(examplesDir, items, 'example');
59
- }
60
-
61
- return items;
62
- }
63
-
64
- /**
65
- * Recursively index a directory
66
- * @param {string} dir - Directory path
67
- * @param {Object[]} items - Items array to populate
68
- * @param {string} type - Content type
69
- */
70
- function indexDirectory(dir, items, type) {
71
- const entries = fs.readdirSync(dir, { withFileTypes: true });
72
-
73
- for (const entry of entries) {
74
- const fullPath = path.join(dir, entry.name);
75
-
76
- if (entry.isDirectory()) {
77
- indexDirectory(fullPath, items, type);
78
- } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') {
79
- const item = parseMarkdownFile(fullPath, type);
80
- if (item) {
81
- items.push(item);
82
- }
83
- }
84
- }
85
- }
86
-
87
- /**
88
- * Parse a markdown file for search indexing
89
- * @param {string} filePath - File path
90
- * @param {string} type - Content type
91
- * @returns {Object|null} Parsed item or null
92
- */
93
- function parseMarkdownFile(filePath, type) {
94
- try {
95
- const content = fs.readFileSync(filePath, 'utf8');
96
- const lines = content.split('\n');
97
-
98
- // Extract title from first H1 or filename
99
- let title = path.basename(filePath, '.md');
100
- const h1Match = content.match(/^#\s+(.+)/m);
101
- if (h1Match) {
102
- title = h1Match[1];
103
- }
104
-
105
- // Extract category from parent directory
106
- const pathParts = filePath.split(path.sep);
107
- const snippetsIndex = pathParts.indexOf('snippets');
108
- const templatesIndex = pathParts.indexOf('templates');
109
- const examplesIndex = pathParts.indexOf('examples');
110
-
111
- let category = '';
112
- const typeIndex = Math.max(snippetsIndex, templatesIndex, examplesIndex);
113
- if (typeIndex !== -1 && typeIndex + 1 < pathParts.length) {
114
- category = pathParts[typeIndex + 1];
115
- }
116
-
117
- // Extract tags from content (look for patterns like `tag`, **tag**, or explicit tags)
118
- const tags = [];
119
- const tagMatches = content.match(/`([^`]+)`/g);
120
- if (tagMatches) {
121
- tagMatches.slice(0, 10).forEach((match) => {
122
- const tag = match.replace(/`/g, '').toLowerCase();
123
- if (tag.length > 2 && tag.length < 30 && !tags.includes(tag)) {
124
- tags.push(tag);
125
- }
126
- });
127
- }
128
-
129
- // Extract first paragraph as preview
130
- let preview = '';
131
- for (let i = 0; i < lines.length && i < 20; i++) {
132
- const line = lines[i].trim();
133
- if (line && !line.startsWith('#') && !line.startsWith('```') && !line.startsWith('|')) {
134
- preview = line;
135
- break;
136
- }
137
- }
138
-
139
- return {
140
- title,
141
- path: filePath,
142
- relativePath: filePath.split('library')[1] || filePath,
143
- content: content.substring(0, 2000), // Index first 2000 chars
144
- category,
145
- type,
146
- tags,
147
- preview,
148
- };
149
- } catch (e) {
150
- return null;
151
- }
152
- }
153
-
154
- /**
155
- * Filter results by category
156
- * @param {Object[]} results - Search results
157
- * @param {string} category - Category filter
158
- * @returns {Object[]} Filtered results
159
- */
160
- function filterByCategory(results, category) {
161
- const lowerCategory = category.toLowerCase();
162
- return results.filter(
163
- (r) =>
164
- r.item.category.toLowerCase().includes(lowerCategory) ||
165
- r.item.type.toLowerCase().includes(lowerCategory)
166
- );
167
- }
168
-
169
- /**
170
- * Filter results by tag
171
- * @param {Object[]} results - Search results
172
- * @param {string} tag - Tag filter
173
- * @returns {Object[]} Filtered results
174
- */
175
- function filterByTag(results, tag) {
176
- const lowerTag = tag.toLowerCase();
177
- return results.filter(
178
- (r) =>
179
- r.item.tags.some((t) => t.toLowerCase().includes(lowerTag)) ||
180
- r.item.content.toLowerCase().includes(lowerTag)
181
- );
182
- }
183
-
184
- /**
185
- * Display search results
186
- * @param {Object[]} results - Search results from Fuse.js
187
- * @param {string} query - Original search query
188
- */
189
- function displayResults(results, query) {
190
- if (results.length === 0) {
191
- tui.warning(`No results found for "${query}"`);
192
- console.log(
193
- tui.colors.muted(
194
- '\nTry a different search term or check available content with: super powers'
195
- )
196
- );
197
- return;
198
- }
199
-
200
- tui.section(`Found ${results.length} result${results.length > 1 ? 's' : ''} for "${query}"`);
201
-
202
- // Group by category
203
- const grouped = {};
204
- results.forEach((result) => {
205
- const cat = result.item.category || result.item.type || 'other';
206
- if (!grouped[cat]) {
207
- grouped[cat] = [];
208
- }
209
- grouped[cat].push(result);
210
- });
211
-
212
- // Display grouped results
213
- Object.entries(grouped).forEach(([category, categoryResults]) => {
214
- console.log(`\n${tui.colors.primary(category.toUpperCase())}`);
215
-
216
- categoryResults.slice(0, 5).forEach((result) => {
217
- const score = (1 - result.score) * 100;
218
- const scoreColor =
219
- score > 70 ? tui.colors.success : score > 40 ? tui.colors.warning : tui.colors.muted;
220
-
221
- console.log(` ${tui.icons.bullet} ${tui.colors.highlight(result.item.title)}`);
222
- console.log(` ${tui.colors.muted('Path:')} ${tui.formatPath(result.item.relativePath)}`);
223
-
224
- if (result.item.preview) {
225
- console.log(` ${tui.colors.muted(tui.truncate(result.item.preview, 80))}`);
226
- }
227
-
228
- console.log(` ${tui.colors.muted('Match:')} ${scoreColor(score.toFixed(0) + '%')}`);
229
- });
230
-
231
- if (categoryResults.length > 5) {
232
- console.log(tui.colors.muted(` ... and ${categoryResults.length - 5} more`));
233
- }
234
- });
235
-
236
- console.log('');
237
- }
238
-
239
- /**
240
- * Parse command line arguments
241
- * @param {string[]} args - CLI arguments
242
- * @returns {Object} Parsed options
243
- */
244
- function parseArgs(args) {
245
- const options = {
246
- query: '',
247
- category: null,
248
- tag: null,
249
- limit: 20,
250
- };
251
-
252
- let i = 0;
253
- while (i < args.length) {
254
- const arg = args[i];
255
-
256
- if (arg === '--category' || arg === '-c') {
257
- options.category = args[++i];
258
- } else if (arg === '--tag' || arg === '-t') {
259
- options.tag = args[++i];
260
- } else if (arg === '--limit' || arg === '-l') {
261
- options.limit = parseInt(args[++i], 10) || 20;
262
- } else if (!arg.startsWith('-')) {
263
- options.query = arg;
264
- }
265
- i++;
266
- }
267
-
268
- return options;
269
- }
270
-
271
- /**
272
- * Main search command handler
273
- * @param {string[]} args - Command arguments
274
- * @param {Object} config - CLI configuration with paths
275
- */
276
- function searchCommand(args, config) {
277
- const options = parseArgs(args);
278
-
279
- if (!options.query && !options.category && !options.tag) {
280
- tui.error('Please provide a search query');
281
- console.log('\nUsage:');
282
- console.log(' super xray "query" Search for a term');
283
- console.log(' super xray --category dax Filter by category');
284
- console.log(' super xray --tag performance Filter by tag');
285
- console.log('\nExamples:');
286
- console.log(' super xray "YTD"');
287
- console.log(' super xray --category dax "time intelligence"');
288
- console.log(' super xray --tag performance');
289
- process.exit(1);
290
- }
291
-
292
- // Try cache directory first, fall back to local library
293
- let libraryDir = config.libraryDir;
294
- const localLibrary = path.join(config.packageDir, 'library');
295
-
296
- if (!fs.existsSync(libraryDir)) {
297
- if (fs.existsSync(localLibrary)) {
298
- libraryDir = localLibrary;
299
- } else {
300
- tui.error('Library not found. Try reinstalling: npm install -g @luquimbo/bi-superpowers');
301
- process.exit(1);
302
- }
303
- }
304
-
305
- tui.header('BI Agent Superpowers', 'Search Library');
306
-
307
- // Build search index
308
- const items = buildSearchIndex(libraryDir);
309
-
310
- if (items.length === 0) {
311
- tui.warning('No content found in library');
312
- return;
313
- }
314
-
315
- tui.info(`Indexed ${items.length} items`);
316
-
317
- // Perform search
318
- const fuse = new Fuse(items, FUSE_OPTIONS);
319
-
320
- let results;
321
- if (options.query) {
322
- results = fuse.search(options.query);
323
- } else {
324
- // If no query, return all items as "results"
325
- results = items.map((item) => ({ item, score: 0 }));
326
- }
327
-
328
- // Apply filters
329
- if (options.category) {
330
- results = filterByCategory(results, options.category);
331
- }
332
-
333
- if (options.tag) {
334
- results = filterByTag(results, options.tag);
335
- }
336
-
337
- // Limit results
338
- results = results.slice(0, options.limit);
339
-
340
- // Display results
341
- displayResults(results, options.query || `category:${options.category}` || `tag:${options.tag}`);
342
- }
343
-
344
- module.exports = searchCommand;
@@ -1,115 +0,0 @@
1
- /**
2
- * Unit tests for the search command (xray)
3
- *
4
- * Run with: npm test
5
- */
6
-
7
- const { test, describe } = require('node:test');
8
- const assert = require('node:assert');
9
-
10
- // Mock content for testing search functionality
11
- const mockContent = `# Time Intelligence Patterns
12
-
13
- ## Overview
14
- DAX time intelligence functions for YTD, MTD, QTD calculations.
15
-
16
- ## YTD Pattern
17
-
18
- \`\`\`dax
19
- Sales YTD = TOTALYTD([Total Sales], 'Date'[Date])
20
- \`\`\`
21
-
22
- ## Rolling Average
23
-
24
- \`\`\`dax
25
- Rolling 12M Average =
26
- AVERAGEX(
27
- DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH),
28
- [Total Sales]
29
- )
30
- \`\`\`
31
- `;
32
-
33
- describe('Search Index Building', () => {
34
- test('should extract title from H1 heading', () => {
35
- const h1Match = mockContent.match(/^#\s+(.+)/m);
36
- assert.ok(h1Match, 'Should find H1 heading');
37
- assert.strictEqual(h1Match[1], 'Time Intelligence Patterns');
38
- });
39
-
40
- test('should extract tags from backtick code', () => {
41
- const tagMatches = mockContent.match(/`([^`]+)`/g);
42
- assert.ok(tagMatches, 'Should find backtick tags');
43
- assert.ok(tagMatches.length > 0, 'Should have at least one tag');
44
- });
45
-
46
- test('should find DAX code blocks', () => {
47
- const daxBlocks = mockContent.match(/```dax[\s\S]*?```/g);
48
- assert.ok(daxBlocks, 'Should find DAX code blocks');
49
- assert.strictEqual(daxBlocks.length, 2, 'Should have 2 DAX code blocks');
50
- });
51
- });
52
-
53
- describe('Search Filtering', () => {
54
- test('category filter should be case-insensitive', () => {
55
- const category = 'dax';
56
- const testCategories = ['DAX', 'dax', 'Dax'];
57
-
58
- testCategories.forEach((testCat) => {
59
- const matches = testCat.toLowerCase().includes(category.toLowerCase());
60
- assert.strictEqual(matches, true, `${testCat} should match ${category}`);
61
- });
62
- });
63
-
64
- test('tag filter should find partial matches', () => {
65
- const tags = ['ytd', 'rolling', 'average', 'time-intelligence'];
66
- const searchTag = 'roll';
67
-
68
- const found = tags.some((t) => t.toLowerCase().includes(searchTag.toLowerCase()));
69
- assert.strictEqual(found, true, 'Should find partial tag match');
70
- });
71
- });
72
-
73
- describe('Search Result Scoring', () => {
74
- test('perfect match should score higher than partial', () => {
75
- // Simulating Fuse.js scoring (lower is better, 0 is perfect)
76
- const perfectScore = 0;
77
- const partialScore = 0.3;
78
-
79
- assert.ok(perfectScore < partialScore, 'Perfect match should have lower score');
80
- });
81
-
82
- test('title match should have higher weight', () => {
83
- const titleWeight = 0.4;
84
- const contentWeight = 0.3;
85
- const categoryWeight = 0.2;
86
- const tagsWeight = 0.1;
87
-
88
- const totalWeight = titleWeight + contentWeight + categoryWeight + tagsWeight;
89
- // Use tolerance for floating point comparison
90
- assert.ok(Math.abs(totalWeight - 1.0) < 0.0001, 'Weights should sum to 1.0');
91
- assert.ok(titleWeight > contentWeight, 'Title should have highest weight');
92
- });
93
- });
94
-
95
- describe('Argument Parsing', () => {
96
- test('should parse query argument', () => {
97
- const args = ['YTD'];
98
- const query = args.find((arg) => !arg.startsWith('-'));
99
- assert.strictEqual(query, 'YTD');
100
- });
101
-
102
- test('should parse category flag', () => {
103
- const args = ['--category', 'dax', 'YTD'];
104
- const categoryIndex = args.indexOf('--category');
105
- const category = categoryIndex !== -1 ? args[categoryIndex + 1] : null;
106
- assert.strictEqual(category, 'dax');
107
- });
108
-
109
- test('should parse short category flag', () => {
110
- const args = ['-c', 'power-query', 'transform'];
111
- const categoryIndex = args.indexOf('-c');
112
- const category = categoryIndex !== -1 ? args[categoryIndex + 1] : null;
113
- assert.strictEqual(category, 'power-query');
114
- });
115
- });