@luquimbo/bi-superpowers 3.1.1 → 4.1.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 (186) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +28 -2
  3. package/.claude-plugin/skill-manifest.json +22 -6
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +52 -36
  6. package/CHANGELOG.md +295 -0
  7. package/README.md +75 -26
  8. package/bin/build-plugin.js +17 -10
  9. package/bin/cli.js +278 -322
  10. package/bin/commands/build-desktop.js +35 -16
  11. package/bin/commands/diff.js +31 -13
  12. package/bin/commands/install.js +93 -72
  13. package/bin/commands/lint.js +40 -26
  14. package/bin/commands/mcp-setup.js +3 -10
  15. package/bin/commands/update-check.js +389 -0
  16. package/bin/lib/agents.js +19 -0
  17. package/bin/lib/generators/claude-plugin.js +144 -6
  18. package/bin/lib/generators/shared.js +29 -33
  19. package/bin/lib/mcp-config.js +191 -16
  20. package/bin/lib/skills.js +115 -27
  21. package/bin/postinstall.js +4 -2
  22. package/bin/utils/mcp-detect.js +2 -2
  23. package/commands/bi-start.md +218 -0
  24. package/commands/pbi-connect.md +43 -65
  25. package/commands/project-kickoff.md +393 -673
  26. package/commands/report-design.md +403 -0
  27. package/desktop-extension/manifest.json +5 -12
  28. package/desktop-extension/server.js +34 -25
  29. package/package.json +6 -10
  30. package/skills/bi-start/SKILL.md +220 -0
  31. package/skills/bi-start/scripts/update-check.js +389 -0
  32. package/skills/pbi-connect/SKILL.md +45 -67
  33. package/skills/pbi-connect/scripts/update-check.js +389 -0
  34. package/skills/project-kickoff/SKILL.md +395 -675
  35. package/skills/project-kickoff/scripts/update-check.js +389 -0
  36. package/skills/report-design/SKILL.md +405 -0
  37. package/skills/report-design/references/cli-commands.md +184 -0
  38. package/skills/report-design/references/cli-setup.md +101 -0
  39. package/skills/report-design/references/close-write-open-pattern.md +80 -0
  40. package/skills/report-design/references/layouts/finance.md +65 -0
  41. package/skills/report-design/references/layouts/generic.md +46 -0
  42. package/skills/report-design/references/layouts/hr.md +48 -0
  43. package/skills/report-design/references/layouts/marketing.md +45 -0
  44. package/skills/report-design/references/layouts/operations.md +44 -0
  45. package/skills/report-design/references/layouts/sales.md +50 -0
  46. package/skills/report-design/references/native-visuals.md +341 -0
  47. package/skills/report-design/references/pbi-desktop-installation.md +87 -0
  48. package/skills/report-design/references/pbir-preview-activation.md +40 -0
  49. package/skills/report-design/references/slicer.md +89 -0
  50. package/skills/report-design/references/textbox.md +101 -0
  51. package/skills/report-design/references/themes/BISuperpowers.json +915 -0
  52. package/skills/report-design/references/troubleshooting.md +135 -0
  53. package/skills/report-design/references/visual-types.md +78 -0
  54. package/skills/report-design/scripts/apply-theme.js +243 -0
  55. package/skills/report-design/scripts/create-visual.js +878 -0
  56. package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  57. package/skills/report-design/scripts/update-check.js +389 -0
  58. package/skills/report-design/scripts/validate-pbir.js +322 -0
  59. package/src/content/base.md +12 -68
  60. package/src/content/mcp-requirements.json +0 -25
  61. package/src/content/routing.md +19 -74
  62. package/src/content/skills/bi-start.md +191 -0
  63. package/src/content/skills/pbi-connect.md +22 -65
  64. package/src/content/skills/project-kickoff.md +372 -673
  65. package/src/content/skills/report-design/SKILL.md +376 -0
  66. package/src/content/skills/report-design/references/cli-commands.md +184 -0
  67. package/src/content/skills/report-design/references/cli-setup.md +101 -0
  68. package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
  69. package/src/content/skills/report-design/references/layouts/finance.md +65 -0
  70. package/src/content/skills/report-design/references/layouts/generic.md +46 -0
  71. package/src/content/skills/report-design/references/layouts/hr.md +48 -0
  72. package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
  73. package/src/content/skills/report-design/references/layouts/operations.md +44 -0
  74. package/src/content/skills/report-design/references/layouts/sales.md +50 -0
  75. package/src/content/skills/report-design/references/native-visuals.md +341 -0
  76. package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
  77. package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
  78. package/src/content/skills/report-design/references/slicer.md +89 -0
  79. package/src/content/skills/report-design/references/textbox.md +101 -0
  80. package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
  81. package/src/content/skills/report-design/references/troubleshooting.md +135 -0
  82. package/src/content/skills/report-design/references/visual-types.md +78 -0
  83. package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
  84. package/src/content/skills/report-design/scripts/create-visual.js +878 -0
  85. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  86. package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
  87. package/bin/commands/add.js +0 -533
  88. package/bin/commands/add.test.js +0 -77
  89. package/bin/commands/changelog.js +0 -443
  90. package/bin/commands/install.test.js +0 -289
  91. package/bin/commands/lint.test.js +0 -103
  92. package/bin/commands/pull.js +0 -287
  93. package/bin/commands/pull.test.js +0 -36
  94. package/bin/commands/push.js +0 -231
  95. package/bin/commands/push.test.js +0 -14
  96. package/bin/commands/search.js +0 -344
  97. package/bin/commands/search.test.js +0 -115
  98. package/bin/commands/setup.js +0 -545
  99. package/bin/commands/setup.test.js +0 -46
  100. package/bin/commands/sync-profile.js +0 -405
  101. package/bin/commands/sync-profile.test.js +0 -14
  102. package/bin/commands/sync-source.js +0 -418
  103. package/bin/commands/sync-source.test.js +0 -14
  104. package/bin/lib/generators/claude-plugin.test.js +0 -111
  105. package/bin/lib/mcp-config.test.js +0 -310
  106. package/bin/lib/microsoft-mcp.test.js +0 -115
  107. package/bin/utils/errors.js +0 -159
  108. package/bin/utils/git.js +0 -298
  109. package/bin/utils/logger.js +0 -142
  110. package/bin/utils/mcp-detect.test.js +0 -81
  111. package/bin/utils/pbix.js +0 -305
  112. package/bin/utils/pbix.test.js +0 -37
  113. package/bin/utils/profiles.js +0 -312
  114. package/bin/utils/projects.js +0 -169
  115. package/bin/utils/readline.js +0 -206
  116. package/bin/utils/readline.test.js +0 -47
  117. package/bin/utils/tui.test.js +0 -127
  118. package/docs/openrouter-free-models.md +0 -92
  119. package/library/examples/README.md +0 -151
  120. package/library/examples/finance-reporting/README.md +0 -351
  121. package/library/examples/finance-reporting/data-model.md +0 -267
  122. package/library/examples/finance-reporting/measures.dax +0 -557
  123. package/library/examples/hr-analytics/README.md +0 -371
  124. package/library/examples/hr-analytics/data-model.md +0 -315
  125. package/library/examples/hr-analytics/measures.dax +0 -460
  126. package/library/examples/marketing-analytics/README.md +0 -37
  127. package/library/examples/marketing-analytics/data-model.md +0 -62
  128. package/library/examples/marketing-analytics/measures.dax +0 -110
  129. package/library/examples/retail-analytics/README.md +0 -439
  130. package/library/examples/retail-analytics/data-model.md +0 -288
  131. package/library/examples/retail-analytics/measures.dax +0 -481
  132. package/library/examples/supply-chain/README.md +0 -37
  133. package/library/examples/supply-chain/data-model.md +0 -69
  134. package/library/examples/supply-chain/measures.dax +0 -77
  135. package/library/examples/udf-library/README.md +0 -228
  136. package/library/examples/udf-library/functions.dax +0 -571
  137. package/library/snippets/dax/README.md +0 -292
  138. package/library/snippets/dax/business-domains.md +0 -576
  139. package/library/snippets/dax/calculate-patterns.md +0 -276
  140. package/library/snippets/dax/calculation-groups.md +0 -489
  141. package/library/snippets/dax/error-handling.md +0 -495
  142. package/library/snippets/dax/iterators-and-aggregations.md +0 -474
  143. package/library/snippets/dax/kpis-and-metrics.md +0 -293
  144. package/library/snippets/dax/rankings-and-topn.md +0 -235
  145. package/library/snippets/dax/security-patterns.md +0 -413
  146. package/library/snippets/dax/text-and-formatting.md +0 -316
  147. package/library/snippets/dax/time-intelligence.md +0 -196
  148. package/library/snippets/dax/user-defined-functions.md +0 -477
  149. package/library/snippets/dax/virtual-tables.md +0 -546
  150. package/library/snippets/excel-formulas/README.md +0 -84
  151. package/library/snippets/excel-formulas/aggregations.md +0 -330
  152. package/library/snippets/excel-formulas/dates-and-times.md +0 -361
  153. package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
  154. package/library/snippets/excel-formulas/lookups.md +0 -169
  155. package/library/snippets/excel-formulas/text-functions.md +0 -363
  156. package/library/snippets/governance/naming-conventions.md +0 -97
  157. package/library/snippets/governance/review-checklists.md +0 -107
  158. package/library/snippets/power-query/README.md +0 -389
  159. package/library/snippets/power-query/api-integration.md +0 -707
  160. package/library/snippets/power-query/connections.md +0 -434
  161. package/library/snippets/power-query/data-cleaning.md +0 -298
  162. package/library/snippets/power-query/error-handling.md +0 -526
  163. package/library/snippets/power-query/parameters.md +0 -350
  164. package/library/snippets/power-query/performance.md +0 -506
  165. package/library/snippets/power-query/transformations.md +0 -330
  166. package/library/snippets/report-design/accessibility.md +0 -78
  167. package/library/snippets/report-design/chart-selection.md +0 -54
  168. package/library/snippets/report-design/layout-patterns.md +0 -87
  169. package/library/templates/data-models/README.md +0 -93
  170. package/library/templates/data-models/finance-model.md +0 -627
  171. package/library/templates/data-models/retail-star-schema.md +0 -473
  172. package/library/templates/excel/README.md +0 -83
  173. package/library/templates/excel/budget-tracker.md +0 -432
  174. package/library/templates/excel/data-entry-form.md +0 -533
  175. package/library/templates/power-bi/README.md +0 -72
  176. package/library/templates/power-bi/finance-report.md +0 -449
  177. package/library/templates/power-bi/kpi-scorecard.md +0 -461
  178. package/library/templates/power-bi/sales-dashboard.md +0 -281
  179. package/library/themes/excel/README.md +0 -436
  180. package/library/themes/power-bi/README.md +0 -271
  181. package/library/themes/power-bi/accessible.json +0 -307
  182. package/library/themes/power-bi/bi-superpowers-default.json +0 -858
  183. package/library/themes/power-bi/corporate-blue.json +0 -291
  184. package/library/themes/power-bi/dark-mode.json +0 -291
  185. package/library/themes/power-bi/minimal.json +0 -292
  186. package/library/themes/power-bi/print-friendly.json +0 -309
@@ -20,10 +20,12 @@ const {
20
20
  } = require('../microsoft-mcp');
21
21
  const { parseSkillMetadata, getSkillPurpose } = require('./shared');
22
22
 
23
- // Currently the plugin ships 2 skills, both interactive wizards.
23
+ // Currently the plugin ships 4 skills (bi-start, project-kickoff,
24
+ // pbi-connect, report-design). bi-start is the session-opener that
25
+ // routes to the other three; the rest are specialists.
24
26
  // The old reference-skills split is kept as an empty set so that any
25
27
  // downstream code that checks `REFERENCE_SKILLS.has(x)` still works.
26
- const COMMAND_SKILLS = new Set(['project-kickoff', 'pbi-connect']);
28
+ const COMMAND_SKILLS = new Set(['bi-start', 'project-kickoff', 'pbi-connect', 'report-design']);
27
29
 
28
30
  const REFERENCE_SKILLS = new Set();
29
31
 
@@ -38,6 +40,36 @@ function ensureDirectory(directory) {
38
40
  }
39
41
  }
40
42
 
43
+ /**
44
+ * Recursively copy a directory tree from `source` to `target`.
45
+ * Follows the Node built-in cp() semantics: overwrites existing files,
46
+ * preserves directory structure. No-op if source does not exist.
47
+ *
48
+ * Used to propagate `references/` and `scripts/` subfolders from
49
+ * folder-based source skills into the generated plugin so the agent can
50
+ * load on-demand material and run bundled helper scripts.
51
+ *
52
+ * @param {string} source - Source directory (must exist to do anything)
53
+ * @param {string} target - Target directory (created if missing)
54
+ */
55
+ function copyDirectory(source, target) {
56
+ if (!fs.existsSync(source)) {
57
+ return;
58
+ }
59
+ ensureDirectory(target);
60
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
61
+ const sourcePath = path.join(source, entry.name);
62
+ const targetPath = path.join(target, entry.name);
63
+ if (entry.isDirectory()) {
64
+ copyDirectory(sourcePath, targetPath);
65
+ } else if (entry.isFile()) {
66
+ fs.copyFileSync(sourcePath, targetPath);
67
+ }
68
+ // Ignore symlinks / other entry types to match the conservative
69
+ // behaviour of readSkillDirectory().
70
+ }
71
+ }
72
+
41
73
  /**
42
74
  * Rewrite library references for generated plugin targets.
43
75
  *
@@ -92,16 +124,57 @@ function toFrontmatterValue(value) {
92
124
  return JSON.stringify(value);
93
125
  }
94
126
 
127
+ // ---------------------------------------------------------------------------
128
+ // Update-check preamble
129
+ // ---------------------------------------------------------------------------
130
+ // Cross-agent "auto-update" mechanism (see ROADMAP item 4 / gstack pattern).
131
+ // The preamble is prepended to every generated SKILL.md and command Markdown
132
+ // so the agent reading the skill content runs the version check at the top
133
+ // of each conversation, surfaces an update notice if available, and then
134
+ // proceeds with the skill. The check caches for 24h on disk and degrades to
135
+ // no-output on network failure — it must never block the skill invocation.
136
+ //
137
+ // The update-check.js script is shipped alongside each skill's `scripts/`
138
+ // folder at generation time (see copyUpdateCheckScript) so `{skillBundleDir}`
139
+ // resolves correctly regardless of the install path (npm-based `super
140
+ // install` OR Claude Code plugin marketplace).
141
+
142
+ const UPDATE_CHECK_PREAMBLE = `<!-- bi-superpowers update-check preamble (auto-generated, do not edit) -->
143
+ ## ⬆ Update check
144
+
145
+ Before handling the user's request below, run this command **once per conversation** and interpret the output:
146
+
147
+ \`\`\`bash
148
+ node "{skillBundleDir}/scripts/update-check.js" --silent-if-uptodate --silent-if-snoozed
149
+ \`\`\`
150
+
151
+ - Empty output or \`UPTODATE\` — proceed with the skill silently. No message.
152
+ - \`UPDATE_AVAILABLE <installed> <latest>\` — tell the user exactly once this conversation, before diving into the skill:
153
+ > "Hay **bi-superpowers v{latest}** disponible (estás en v{installed}). Actualizá con \`super upgrade\` (o \`/plugin update bi-superpowers\` en Claude Code) cuando te venga bien."
154
+
155
+ Then continue with the skill below.
156
+ - \`SNOOZED <iso>\` — proceed silently.
157
+
158
+ If the command fails (missing binary, permissions, offline), ignore the error and proceed with the skill. The update check must never block the user's request.
159
+
160
+ ---
161
+ <!-- /bi-superpowers update-check preamble -->
162
+
163
+ `;
164
+
95
165
  /**
96
166
  * Build command markdown with Claude Code frontmatter.
97
167
  *
98
168
  * @param {Object} skill - Skill definition object
99
169
  * @param {string} libraryPrefix - Library prefix used in generated target
170
+ * @param {Object} [options] - Options
171
+ * @param {boolean} [options.skipPreamble] - Omit the update-check preamble (tests)
100
172
  * @returns {string} Command markdown
101
173
  */
102
- function buildCommandMarkdown(skill, libraryPrefix) {
174
+ function buildCommandMarkdown(skill, libraryPrefix, options = {}) {
103
175
  const description = getSkillPurpose(skill.name);
104
176
  const content = rewriteLibraryReferences(skill.content, libraryPrefix);
177
+ const preamble = options.skipPreamble ? '' : UPDATE_CHECK_PREAMBLE;
105
178
 
106
179
  return `---
107
180
  description: ${toFrontmatterValue(description)}
@@ -109,7 +182,7 @@ description: ${toFrontmatterValue(description)}
109
182
 
110
183
  <!-- Generated by BI Agent Superpowers. Edit src/content/skills/${skill.name}.md instead. -->
111
184
 
112
- ${content}`;
185
+ ${preamble}${content}`;
113
186
  }
114
187
 
115
188
  /**
@@ -118,10 +191,13 @@ ${content}`;
118
191
  * @param {Object} skill - Skill definition object
119
192
  * @param {string} version - Package version
120
193
  * @param {string} libraryPrefix - Library prefix used in generated target
194
+ * @param {Object} [options] - Options
195
+ * @param {boolean} [options.skipPreamble] - Omit the update-check preamble (tests)
121
196
  * @returns {string} Skill markdown
122
197
  */
123
- function buildSkillMarkdown(skill, version, libraryPrefix) {
198
+ function buildSkillMarkdown(skill, version, libraryPrefix, options = {}) {
124
199
  const content = rewriteLibraryReferences(skill.content, libraryPrefix);
200
+ const preamble = options.skipPreamble ? '' : UPDATE_CHECK_PREAMBLE;
125
201
 
126
202
  return `---
127
203
  name: ${toFrontmatterValue(skill.name)}
@@ -131,7 +207,29 @@ version: ${toFrontmatterValue(version)}
131
207
 
132
208
  <!-- Generated by BI Agent Superpowers. Edit src/content/skills/${skill.name}.md instead. -->
133
209
 
134
- ${content}`;
210
+ ${preamble}${content}`;
211
+ }
212
+
213
+ /**
214
+ * Copy bin/commands/update-check.js into each generated skill's scripts/
215
+ * directory so the preamble's `{skillBundleDir}/scripts/update-check.js`
216
+ * invocation works under both install paths (npm-based `super install` and
217
+ * Claude Code plugin marketplace). The file is bundled with the npm tarball
218
+ * via bin/, so this is purely a generation-time copy into skills/.
219
+ *
220
+ * @param {string} packageDir - npm package root (source of the canonical script)
221
+ * @param {string} targetDir - plugin target root (contains skills/<name>/)
222
+ * @param {Object[]} skills - Skills to wire the script into
223
+ */
224
+ function copyUpdateCheckScript(packageDir, targetDir, skills) {
225
+ if (!packageDir) return;
226
+ const src = path.join(packageDir, 'bin', 'commands', 'update-check.js');
227
+ if (!fs.existsSync(src)) return;
228
+ for (const skill of skills) {
229
+ const scriptsDir = path.join(targetDir, 'skills', skill.name, 'scripts');
230
+ ensureDirectory(scriptsDir);
231
+ fs.copyFileSync(src, path.join(scriptsDir, 'update-check.js'));
232
+ }
135
233
  }
136
234
 
137
235
  /**
@@ -178,6 +276,25 @@ async function generate(targetDir, skills, options = {}) {
178
276
  ensureDirectory(commandsDir);
179
277
  ensureDirectory(skillsDir);
180
278
 
279
+ // Pull discoverable metadata from the installed package.json when available
280
+ // so the generated plugin.json has the same repo/license/keywords/homepage
281
+ // fields as the published npm package. Falls back to bare identity fields
282
+ // if options.packageDir is missing (unit tests, detached generation).
283
+ let pkgMetadata = {};
284
+ if (options.packageDir) {
285
+ try {
286
+ const pkg = require(path.join(options.packageDir, 'package.json'));
287
+ pkgMetadata = {
288
+ repository: pkg.repository,
289
+ homepage: pkg.homepage,
290
+ license: pkg.license,
291
+ keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
292
+ };
293
+ } catch (_) {
294
+ // Package.json unreadable — generate a minimal manifest and move on.
295
+ }
296
+ }
297
+
181
298
  const pluginManifest = {
182
299
  name: 'bi-superpowers',
183
300
  description:
@@ -186,6 +303,10 @@ async function generate(targetDir, skills, options = {}) {
186
303
  author: {
187
304
  name: 'Lucas Sanchez',
188
305
  },
306
+ ...(pkgMetadata.repository !== undefined ? { repository: pkgMetadata.repository } : {}),
307
+ ...(pkgMetadata.homepage !== undefined ? { homepage: pkgMetadata.homepage } : {}),
308
+ ...(pkgMetadata.license !== undefined ? { license: pkgMetadata.license } : {}),
309
+ ...(pkgMetadata.keywords !== undefined ? { keywords: pkgMetadata.keywords } : {}),
189
310
  };
190
311
 
191
312
  fs.writeFileSync(
@@ -312,6 +433,9 @@ async function generate(targetDir, skills, options = {}) {
312
433
  }
313
434
 
314
435
  // ALL skills → skills/*/SKILL.md (universal, discoverable by all Claude tools)
436
+ // Folder-based skills additionally get their `references/` and `scripts/`
437
+ // subfolders copied verbatim so the agent can load on-demand material and
438
+ // invoke helper scripts bundled with the skill.
315
439
  for (const skill of skills) {
316
440
  const skillDir = path.join(skillsDir, skill.name);
317
441
  ensureDirectory(skillDir);
@@ -319,8 +443,18 @@ async function generate(targetDir, skills, options = {}) {
319
443
  path.join(skillDir, 'SKILL.md'),
320
444
  buildSkillMarkdown(skill, version, libraryPrefix)
321
445
  );
446
+
447
+ if (skill.bundleDir) {
448
+ copyDirectory(path.join(skill.bundleDir, 'references'), path.join(skillDir, 'references'));
449
+ copyDirectory(path.join(skill.bundleDir, 'scripts'), path.join(skillDir, 'scripts'));
450
+ }
322
451
  }
323
452
 
453
+ // Ship the update-check helper alongside every skill so the SKILL.md
454
+ // preamble can invoke it via `{skillBundleDir}/scripts/update-check.js`
455
+ // regardless of install path.
456
+ copyUpdateCheckScript(options.packageDir, targetDir, skills);
457
+
324
458
  console.log(' ✓ Created Claude Code plugin manifest');
325
459
  console.log(' ✓ Created .mcp.json with official Microsoft MCP servers');
326
460
  console.log(` ✓ Created ${commandSkills.length} plugin commands`);
@@ -331,6 +465,10 @@ module.exports = {
331
465
  name: 'Claude Code Plugin',
332
466
  description: 'Native Claude Code plugin (recommended)',
333
467
  generate,
468
+ buildCommandMarkdown,
469
+ buildSkillMarkdown,
470
+ copyUpdateCheckScript,
471
+ UPDATE_CHECK_PREAMBLE,
334
472
  COMMAND_SKILLS,
335
473
  REFERENCE_SKILLS,
336
474
  };
@@ -29,12 +29,19 @@ function parseSkillMetadata(content) {
29
29
  const titleMatch = content.match(/^#\s+(.+)/m);
30
30
  if (titleMatch) metadata.title = titleMatch[1];
31
31
 
32
- // Get triggers
32
+ // Get triggers — collect every double-quoted fragment in the Trigger
33
+ // section. This handles all three shapes our skills use:
34
+ // - Bare-quote bullets: - "connect Power BI", "PBI connection"
35
+ // - Prose bullets: - User mentions: "analizar proyecto", "new project"
36
+ // - Asterisk bullets: * "pattern"
37
+ // The regex intentionally doesn't anchor to bullet syntax — that's
38
+ // what broke extraction for project-kickoff (prose) and under-counted
39
+ // pbi-connect (first quote per bullet only) before this change.
33
40
  const triggerSection = content.match(/##\s+Trigger[\s\S]*?(?=##|$)/i);
34
41
  if (triggerSection) {
35
- const triggers = triggerSection[0].match(/[-*]\s+"([^"]+)"/g);
36
- if (triggers) {
37
- metadata.triggers = triggers.map((t) => t.replace(/[-*]\s+"/, '').replace('"', ''));
42
+ const quotedFragments = triggerSection[0].match(/"([^"]+)"/g);
43
+ if (quotedFragments) {
44
+ metadata.triggers = quotedFragments.map((t) => t.slice(1, -1));
38
45
  }
39
46
  }
40
47
 
@@ -47,41 +54,29 @@ function parseSkillMetadata(content) {
47
54
  return metadata;
48
55
  }
49
56
 
57
+ /**
58
+ * Map of skill name → one-line purpose description used in generated
59
+ * commands/*.md frontmatter and the CLI's `super powers` output.
60
+ *
61
+ * Keep this map in lock-step with `src/content/skills/`. Adding a skill
62
+ * without an entry here makes the generated description fall back to the
63
+ * generic `'Specialized BI assistance'` string and breaks the parity
64
+ * test in shared.test.js.
65
+ */
66
+ const SKILL_PURPOSES = {
67
+ 'bi-start': 'Session opener — environment snapshot, update check, and routing to other skills',
68
+ 'project-kickoff': 'Project analysis and planning',
69
+ 'pbi-connect': 'Power BI Desktop connection',
70
+ 'report-design': '3-page PBIR report generation for Power BI Desktop (Windows)',
71
+ };
72
+
50
73
  /**
51
74
  * Get skill purpose description for table
52
75
  * @param {string} skillName - Name of the skill
53
76
  * @returns {string} Purpose description
54
77
  */
55
78
  function getSkillPurpose(skillName) {
56
- const purposes = {
57
- dax: 'DAX writing and optimization',
58
- 'power-query': 'Power Query / M language patterns',
59
- 'data-modeling': 'Star schema design',
60
- 'data-model-design': 'Interactive model builder',
61
- 'excel-formulas': 'Modern Excel 365 formulas',
62
- 'project-kickoff': 'Project analysis and planning',
63
- 'theme-tweaker': 'Power BI theme customization',
64
- 'rls-design': 'Row-level security design',
65
- 'query-performance': 'Performance optimization',
66
- 'data-quality': 'Data validation and profiling',
67
- 'fabric-scripts': 'Fabric automation scripts',
68
- 'fast-standard': 'FAST spreadsheet standard',
69
- 'testing-validation': 'Testing and validation patterns',
70
- 'pbi-connect': 'Power BI Desktop connection',
71
- contributions: 'Contribution validation',
72
- // New command skills
73
- 'dax-doctor': 'DAX debugging and optimization wizard',
74
- 'model-documenter': 'Semantic model documentation generator',
75
- 'migration-assistant': 'Migration and upgrade assistant',
76
- 'report-layout': 'Report page layout planner',
77
- // New reference skills
78
- governance: 'Naming conventions, standards, and governance',
79
- 'semantic-model': 'Semantic model best practices and patterns',
80
- 'report-design': 'Report design and visualization principles',
81
- deployment: 'CI/CD and deployment patterns for BI',
82
- 'dax-udf': 'DAX user-defined functions (UDFs)',
83
- };
84
- return purposes[skillName] || 'Specialized BI assistance';
79
+ return SKILL_PURPOSES[skillName] || 'Specialized BI assistance';
85
80
  }
86
81
 
87
82
  /**
@@ -243,6 +238,7 @@ ${footer}`;
243
238
  module.exports = {
244
239
  parseSkillMetadata,
245
240
  getSkillPurpose,
241
+ SKILL_PURPOSES,
246
242
  generateSkillsSection,
247
243
  getCodeStandards,
248
244
  getFormatHeader,
@@ -29,7 +29,7 @@ const path = require('path');
29
29
  const os = require('os');
30
30
 
31
31
  const MICROSOFT_LEARN_URL = 'https://learn.microsoft.com/api/mcp';
32
- const MODELING_SERVER_NAME = 'powerbi-modeling';
32
+ const MODELING_SERVER_NAME = 'powerbi-modeling-mcp';
33
33
  const LEARN_SERVER_NAME = 'microsoft-learn';
34
34
 
35
35
  /**
@@ -42,6 +42,11 @@ function getLauncherAbsolutePath(packageDir) {
42
42
 
43
43
  /**
44
44
  * Safely read a JSON file. Returns null if missing or unparseable.
45
+ *
46
+ * NOTE: this swallows parse errors. Do NOT use it before a write — a
47
+ * silent null on a corrupt user config means a spread merge ends up
48
+ * replacing the whole config with our 2 servers (data loss). Use
49
+ * `readJsonStrict` for write paths instead.
45
50
  */
46
51
  function readJsonSafe(filePath) {
47
52
  if (!fs.existsSync(filePath)) return null;
@@ -52,6 +57,36 @@ function readJsonSafe(filePath) {
52
57
  }
53
58
  }
54
59
 
60
+ /**
61
+ * Read a JSON file for write paths. Returns null only if the file is
62
+ * absent or empty. Throws an actionable error if the file exists with
63
+ * non-empty content but is not parseable — silently overwriting a
64
+ * user's corrupt config (especially `~/.claude.json`) destroys data.
65
+ *
66
+ * @throws {Error} if the file exists but is unreadable or unparseable
67
+ */
68
+ function readJsonStrict(filePath) {
69
+ if (!fs.existsSync(filePath)) return null;
70
+ let raw;
71
+ try {
72
+ raw = fs.readFileSync(filePath, 'utf8');
73
+ } catch (err) {
74
+ throw new Error(
75
+ `Could not read ${filePath}: ${err.message}. ` + 'Check file permissions and retry.'
76
+ );
77
+ }
78
+ if (raw.trim() === '') return null;
79
+ try {
80
+ return JSON.parse(raw);
81
+ } catch (err) {
82
+ throw new Error(
83
+ `Refusing to overwrite ${filePath}: file exists but is not valid JSON ` +
84
+ `(${err.message}). Inspect or move it manually before retrying. ` +
85
+ `If a previous-install backup exists at ${filePath}.bak, restore from there.`
86
+ );
87
+ }
88
+ }
89
+
55
90
  /**
56
91
  * Reject symlink attacks before writing to a file. We use lstatSync so
57
92
  * we detect the link itself (not what it points to). If a user home has
@@ -69,16 +104,132 @@ function assertNotSymlink(filePath) {
69
104
  }
70
105
 
71
106
  /**
72
- * Write a JSON file, creating parent dirs if needed.
73
- * Refuses to overwrite symbolic links for safety.
107
+ * Walk the ancestors of `filePath` up to (but not including) the user's
108
+ * home directory and reject if any ancestor is a symbolic link. Needed
109
+ * because `assertNotSymlink` only checks the leaf — a symlink at a
110
+ * parent (e.g. `~/.copilot` → `/tmp/attacker/`) would still redirect
111
+ * every `.tmp` / `.bak` / final rename through the link, bypassing the
112
+ * leaf-only hardening.
113
+ *
114
+ * Trust boundaries:
115
+ * - We stop walking at `os.homedir()` itself. If the user's home is a
116
+ * symlink, that's their machine setup, not something we can police.
117
+ * - We also stop if, after `realpathSync`, the ancestor resolves
118
+ * outside the home subtree — defensive, covers the case where an
119
+ * earlier ancestor already redirected us elsewhere.
120
+ * - We stop at filesystem root (`path.dirname(x) === x`).
121
+ *
122
+ * Known Windows gap: this uses `lstatSync().isSymbolicLink()`. NTFS
123
+ * junctions are NOT classified as symlinks by Node, so a junction at
124
+ * `~/.copilot` would pass this check. Documented as a residual risk
125
+ * in `coordination/TO_CODEX_REVIEW.md`; addressing it needs
126
+ * `fs.readlinkSync` with reparse-point inspection and is out of scope
127
+ * for this commit.
128
+ *
129
+ * @throws {Error} if any ancestor of filePath (up to home) is a symlink
74
130
  */
75
- function writeJson(filePath, data) {
131
+ function assertNoSymlinkedAncestor(filePath) {
132
+ const home = os.homedir();
133
+ const homeResolved = fs.existsSync(home) ? fs.realpathSync(home) : home;
134
+
135
+ let current = path.dirname(filePath);
136
+ const visited = new Set();
137
+
138
+ while (current && !visited.has(current)) {
139
+ visited.add(current);
140
+
141
+ // Stop at the user's home — trust boundary.
142
+ if (current === home || current === homeResolved) return;
143
+
144
+ if (fs.existsSync(current)) {
145
+ if (fs.lstatSync(current).isSymbolicLink()) {
146
+ throw new Error(
147
+ `Refusing to write MCP config: parent directory ${current} is a symbolic link. ` +
148
+ 'Remove the symlink (or redirect super install at a different path) and retry.'
149
+ );
150
+ }
151
+
152
+ // Defensive: if realpath has already pulled us outside the home
153
+ // subtree, stop walking — we're in territory we can't reason about.
154
+ const currentResolved = fs.realpathSync(current);
155
+ if (!currentResolved.startsWith(homeResolved)) return;
156
+ }
157
+
158
+ const parent = path.dirname(current);
159
+ if (parent === current) return; // filesystem root
160
+ current = parent;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Full pre-write safety check: leaf must not be a symlink, and no
166
+ * ancestor up to `os.homedir()` may be a symlink either. Use this
167
+ * instead of `assertNotSymlink` on every write path inside the user
168
+ * home directory.
169
+ */
170
+ function assertPathSafeForWrite(filePath) {
76
171
  assertNotSymlink(filePath);
172
+ assertNoSymlinkedAncestor(filePath);
173
+ }
174
+
175
+ /**
176
+ * Write any text content to a file using a safe pattern:
177
+ *
178
+ * 1. Refuse if the target is a symbolic link (assertNotSymlink).
179
+ * 2. If the target exists, copy it to `<target>.bak` first. This
180
+ * gives the user a one-slot recovery file overwritten on each
181
+ * install — enough to recover from a bad merge without
182
+ * accumulating stale backups across many install runs.
183
+ * 3. Write to `<target>.tmp` then atomically rename to the final
184
+ * path. `fs.renameSync` is atomic on POSIX and functionally
185
+ * atomic for same-volume renames on NTFS, which is the only
186
+ * shape we hit when writing into the user home (~/...).
187
+ * 4. On any error mid-process, best-effort cleanup of the .tmp
188
+ * file so we never leave half-written turds behind.
189
+ *
190
+ * The atomic + backup pattern matters most for `~/.claude.json` —
191
+ * Claude Code's primary user config holds project pointers, OAuth
192
+ * state, conversation history. A crash mid-write or a bad merge
193
+ * would brick the user's Claude install otherwise.
194
+ */
195
+ function writeFileAtomic(filePath, content) {
196
+ assertPathSafeForWrite(filePath);
77
197
  const dir = path.dirname(filePath);
78
198
  if (!fs.existsSync(dir)) {
79
199
  fs.mkdirSync(dir, { recursive: true });
80
200
  }
81
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
201
+
202
+ if (fs.existsSync(filePath)) {
203
+ try {
204
+ fs.copyFileSync(filePath, `${filePath}.bak`);
205
+ } catch (err) {
206
+ throw new Error(
207
+ `Could not back up ${filePath} before writing: ${err.message}. ` +
208
+ 'Aborting to avoid losing the existing file.'
209
+ );
210
+ }
211
+ }
212
+
213
+ const tmpPath = `${filePath}.tmp`;
214
+ try {
215
+ fs.writeFileSync(tmpPath, content);
216
+ fs.renameSync(tmpPath, filePath);
217
+ } catch (err) {
218
+ try {
219
+ fs.unlinkSync(tmpPath);
220
+ } catch (_) {
221
+ // Best-effort cleanup; ignore if there is nothing to remove.
222
+ }
223
+ throw err;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Write a JSON file using the atomic + backup pattern.
229
+ * Refuses to overwrite symbolic links for safety.
230
+ */
231
+ function writeJson(filePath, data) {
232
+ writeFileAtomic(filePath, JSON.stringify(data, null, 2) + '\n');
82
233
  }
83
234
 
84
235
  /**
@@ -124,11 +275,19 @@ function escapeRegex(str) {
124
275
  //
125
276
  // We describe each agent in a single table and build the writer functions
126
277
  // from it so adding a new JSON agent is a one-line change.
278
+ //
279
+ // IMPORTANT: every entry below has a source URL pointing at the official
280
+ // docs where the path/format comes from. If you change any of these,
281
+ // verify the new value against the linked source first. Silent drift is
282
+ // the hardest class of bug in this file because the writes succeed even
283
+ // when the agent never reads the file.
127
284
 
128
285
  const JSON_AGENT_CONFIGS = {
129
286
  'claude-code': {
130
- // Claude Code user-scope MCP config lives in ~/.claude.json under
131
- // mcpServers. stdio entries omit `type`, HTTP entries include it.
287
+ // Source: https://code.claude.com/docs/en/mcp
288
+ // "User-scoped servers are stored in ~/.claude.json"
289
+ // Note: ~/.claude/settings.json does NOT work for MCPs (known docs bug).
290
+ // stdio entries omit `type` here; HTTP entries include it.
132
291
  configPath: () => path.join(os.homedir(), '.claude.json'),
133
292
  wrapperKey: 'mcpServers',
134
293
  stdioIncludesType: false,
@@ -136,7 +295,9 @@ const JSON_AGENT_CONFIGS = {
136
295
  httpField: 'url',
137
296
  },
138
297
  'github-copilot': {
139
- // Copilot CLI uses `servers` (NOT mcpServers) and every server needs
298
+ // Source: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
299
+ // "MCP servers are saved to ~/.copilot/mcp-config.json"
300
+ // Copilot uses `servers` (NOT mcpServers) and every server needs
140
301
  // an explicit `type` field.
141
302
  configPath: () => path.join(os.homedir(), '.copilot', 'mcp-config.json'),
142
303
  wrapperKey: 'servers',
@@ -145,6 +306,8 @@ const JSON_AGENT_CONFIGS = {
145
306
  httpField: 'url',
146
307
  },
147
308
  'gemini-cli': {
309
+ // Source: https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md
310
+ // "MCP config goes in ~/.gemini/settings.json under mcpServers"
148
311
  // Gemini uses `httpUrl` (NOT `url`) for HTTP transports and omits
149
312
  // `type` — it's inferred from which key is present.
150
313
  configPath: () => path.join(os.homedir(), '.gemini', 'settings.json'),
@@ -154,10 +317,12 @@ const JSON_AGENT_CONFIGS = {
154
317
  httpField: 'httpUrl',
155
318
  },
156
319
  kilo: {
320
+ // Source: https://kilo.ai/docs/automate/mcp/using-in-kilo-code
157
321
  // Kilo Code uses ~/.kilo/ as the user config root (consistent with
158
322
  // ~/.kilo/skills/). The global MCP settings live at the natural
159
323
  // neighbor path — mcp_settings.json — which is what the Kilo VS Code
160
- // extension and CLI both read.
324
+ // extension and CLI both read. (NOT ~/.kilocode/ — that path is
325
+ // project-level only.)
161
326
  configPath: () => path.join(os.homedir(), '.kilo', 'mcp_settings.json'),
162
327
  wrapperKey: 'mcpServers',
163
328
  stdioIncludesType: false,
@@ -212,7 +377,10 @@ function makeJsonWriter(agentId) {
212
377
  const agentConfig = JSON_AGENT_CONFIGS[agentId];
213
378
  return function jsonWriter(packageDir) {
214
379
  const configPath = agentConfig.configPath();
215
- const existing = readJsonSafe(configPath) || {};
380
+ // Use strict reader on the write path: a silent parse failure here
381
+ // would discard the user's other config keys (history, OAuth, etc.)
382
+ // when the empty {} fallback is spread into the new write.
383
+ const existing = readJsonStrict(configPath) || {};
216
384
  const wrapperKey = agentConfig.wrapperKey;
217
385
 
218
386
  const newServers = buildJsonServers(agentConfig, packageDir);
@@ -226,12 +394,17 @@ function makeJsonWriter(agentId) {
226
394
  // ============================================
227
395
  // CODEX (OpenAI) — TOML format
228
396
  // ============================================
397
+ // Source: https://developers.openai.com/codex/mcp
398
+ // Also: https://github.com/openai/codex/blob/main/docs/config.md
399
+ // "Codex CLI reads MCP server config from ~/.codex/config.toml, where
400
+ // each server gets its own section with format [mcp_servers.my-server]"
401
+ //
229
402
  // Writes ~/.codex/config.toml, appending [mcp_servers.*] sections.
230
403
  // Preserves existing content by removing only our own sections before
231
404
  // appending the fresh ones.
232
405
  function writeCodexConfig(packageDir) {
233
406
  const configPath = path.join(os.homedir(), '.codex', 'config.toml');
234
- assertNotSymlink(configPath);
407
+ assertPathSafeForWrite(configPath);
235
408
  const launcher = getLauncherAbsolutePath(packageDir);
236
409
 
237
410
  let existing = '';
@@ -259,11 +432,9 @@ function writeCodexConfig(packageDir) {
259
432
 
260
433
  const content = existing.trimEnd() + newSections;
261
434
 
262
- const dir = path.dirname(configPath);
263
- if (!fs.existsSync(dir)) {
264
- fs.mkdirSync(dir, { recursive: true });
265
- }
266
- fs.writeFileSync(configPath, content);
435
+ // Use the same atomic + backup pattern as the JSON writers — a crash
436
+ // mid-write to ~/.codex/config.toml would brick the user's Codex install.
437
+ writeFileAtomic(configPath, content);
267
438
  return configPath;
268
439
  }
269
440
 
@@ -303,9 +474,13 @@ module.exports = {
303
474
  MICROSOFT_LEARN_URL,
304
475
  // Exported for testing
305
476
  readJsonSafe,
477
+ readJsonStrict,
306
478
  writeJson,
479
+ writeFileAtomic,
307
480
  tomlEscape,
308
481
  escapeRegex,
309
482
  assertNotSymlink,
483
+ assertNoSymlinkedAncestor,
484
+ assertPathSafeForWrite,
310
485
  getLauncherAbsolutePath,
311
486
  };