@luquimbo/bi-superpowers 3.1.0 → 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 (110) 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/README.md +2 -2
  6. package/bin/build-plugin.js +6 -6
  7. package/bin/cli.js +169 -310
  8. package/bin/commands/install.js +87 -70
  9. package/bin/commands/install.test.js +2 -2
  10. package/bin/lib/agents.js +21 -2
  11. package/bin/lib/mcp-config.js +27 -5
  12. package/bin/lib/mcp-config.test.js +1 -1
  13. package/desktop-extension/manifest.json +4 -11
  14. package/desktop-extension/server.js +34 -25
  15. package/package.json +3 -9
  16. package/skills/pbi-connect/SKILL.md +1 -1
  17. package/skills/project-kickoff/SKILL.md +1 -1
  18. package/bin/commands/add.js +0 -533
  19. package/bin/commands/add.test.js +0 -77
  20. package/bin/commands/changelog.js +0 -443
  21. package/bin/commands/pull.js +0 -287
  22. package/bin/commands/pull.test.js +0 -36
  23. package/bin/commands/push.js +0 -231
  24. package/bin/commands/push.test.js +0 -14
  25. package/bin/commands/search.js +0 -344
  26. package/bin/commands/search.test.js +0 -115
  27. package/bin/commands/setup.js +0 -545
  28. package/bin/commands/setup.test.js +0 -46
  29. package/bin/commands/sync-profile.js +0 -405
  30. package/bin/commands/sync-profile.test.js +0 -14
  31. package/bin/commands/sync-source.js +0 -418
  32. package/bin/commands/sync-source.test.js +0 -14
  33. package/bin/utils/errors.js +0 -159
  34. package/bin/utils/git.js +0 -298
  35. package/bin/utils/logger.js +0 -142
  36. package/bin/utils/pbix.js +0 -305
  37. package/bin/utils/pbix.test.js +0 -37
  38. package/bin/utils/profiles.js +0 -312
  39. package/bin/utils/projects.js +0 -169
  40. package/bin/utils/readline.js +0 -206
  41. package/bin/utils/readline.test.js +0 -47
  42. package/docs/openrouter-free-models.md +0 -92
  43. package/library/examples/README.md +0 -151
  44. package/library/examples/finance-reporting/README.md +0 -351
  45. package/library/examples/finance-reporting/data-model.md +0 -267
  46. package/library/examples/finance-reporting/measures.dax +0 -557
  47. package/library/examples/hr-analytics/README.md +0 -371
  48. package/library/examples/hr-analytics/data-model.md +0 -315
  49. package/library/examples/hr-analytics/measures.dax +0 -460
  50. package/library/examples/marketing-analytics/README.md +0 -37
  51. package/library/examples/marketing-analytics/data-model.md +0 -62
  52. package/library/examples/marketing-analytics/measures.dax +0 -110
  53. package/library/examples/retail-analytics/README.md +0 -439
  54. package/library/examples/retail-analytics/data-model.md +0 -288
  55. package/library/examples/retail-analytics/measures.dax +0 -481
  56. package/library/examples/supply-chain/README.md +0 -37
  57. package/library/examples/supply-chain/data-model.md +0 -69
  58. package/library/examples/supply-chain/measures.dax +0 -77
  59. package/library/examples/udf-library/README.md +0 -228
  60. package/library/examples/udf-library/functions.dax +0 -571
  61. package/library/snippets/dax/README.md +0 -292
  62. package/library/snippets/dax/business-domains.md +0 -576
  63. package/library/snippets/dax/calculate-patterns.md +0 -276
  64. package/library/snippets/dax/calculation-groups.md +0 -489
  65. package/library/snippets/dax/error-handling.md +0 -495
  66. package/library/snippets/dax/iterators-and-aggregations.md +0 -474
  67. package/library/snippets/dax/kpis-and-metrics.md +0 -293
  68. package/library/snippets/dax/rankings-and-topn.md +0 -235
  69. package/library/snippets/dax/security-patterns.md +0 -413
  70. package/library/snippets/dax/text-and-formatting.md +0 -316
  71. package/library/snippets/dax/time-intelligence.md +0 -196
  72. package/library/snippets/dax/user-defined-functions.md +0 -477
  73. package/library/snippets/dax/virtual-tables.md +0 -546
  74. package/library/snippets/excel-formulas/README.md +0 -84
  75. package/library/snippets/excel-formulas/aggregations.md +0 -330
  76. package/library/snippets/excel-formulas/dates-and-times.md +0 -361
  77. package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
  78. package/library/snippets/excel-formulas/lookups.md +0 -169
  79. package/library/snippets/excel-formulas/text-functions.md +0 -363
  80. package/library/snippets/governance/naming-conventions.md +0 -97
  81. package/library/snippets/governance/review-checklists.md +0 -107
  82. package/library/snippets/power-query/README.md +0 -389
  83. package/library/snippets/power-query/api-integration.md +0 -707
  84. package/library/snippets/power-query/connections.md +0 -434
  85. package/library/snippets/power-query/data-cleaning.md +0 -298
  86. package/library/snippets/power-query/error-handling.md +0 -526
  87. package/library/snippets/power-query/parameters.md +0 -350
  88. package/library/snippets/power-query/performance.md +0 -506
  89. package/library/snippets/power-query/transformations.md +0 -330
  90. package/library/snippets/report-design/accessibility.md +0 -78
  91. package/library/snippets/report-design/chart-selection.md +0 -54
  92. package/library/snippets/report-design/layout-patterns.md +0 -87
  93. package/library/templates/data-models/README.md +0 -93
  94. package/library/templates/data-models/finance-model.md +0 -627
  95. package/library/templates/data-models/retail-star-schema.md +0 -473
  96. package/library/templates/excel/README.md +0 -83
  97. package/library/templates/excel/budget-tracker.md +0 -432
  98. package/library/templates/excel/data-entry-form.md +0 -533
  99. package/library/templates/power-bi/README.md +0 -72
  100. package/library/templates/power-bi/finance-report.md +0 -449
  101. package/library/templates/power-bi/kpi-scorecard.md +0 -461
  102. package/library/templates/power-bi/sales-dashboard.md +0 -281
  103. package/library/themes/excel/README.md +0 -436
  104. package/library/themes/power-bi/README.md +0 -271
  105. package/library/themes/power-bi/accessible.json +0 -307
  106. package/library/themes/power-bi/bi-superpowers-default.json +0 -858
  107. package/library/themes/power-bi/corporate-blue.json +0 -291
  108. package/library/themes/power-bi/dark-mode.json +0 -291
  109. package/library/themes/power-bi/minimal.json +0 -292
  110. 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
- });