@luquimbo/bi-superpowers 3.2.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +28 -2
- package/.claude-plugin/skill-manifest.json +22 -6
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +53 -36
- package/CHANGELOG.md +310 -0
- package/README.md +77 -26
- package/bin/build-plugin.js +11 -4
- package/bin/cli.js +113 -16
- package/bin/commands/build-desktop.js +35 -16
- package/bin/commands/diff.js +31 -13
- package/bin/commands/install.js +7 -3
- package/bin/commands/lint.js +40 -26
- package/bin/commands/mcp-setup.js +3 -10
- package/bin/commands/update-check.js +403 -0
- package/bin/lib/generators/claude-plugin.js +162 -6
- package/bin/lib/generators/shared.js +29 -33
- package/bin/lib/mcp-config.js +168 -12
- package/bin/lib/skills.js +115 -27
- package/bin/postinstall.js +4 -2
- package/bin/utils/mcp-detect.js +2 -2
- package/commands/bi-start.md +197 -0
- package/commands/pbi-connect.md +43 -65
- package/commands/project-kickoff.md +393 -673
- package/commands/report-design.md +403 -0
- package/desktop-extension/manifest.json +3 -3
- package/package.json +7 -5
- package/skills/bi-start/SKILL.md +199 -0
- package/skills/bi-start/scripts/update-check.js +403 -0
- package/skills/pbi-connect/SKILL.md +45 -67
- package/skills/pbi-connect/scripts/update-check.js +403 -0
- package/skills/project-kickoff/SKILL.md +395 -675
- package/skills/project-kickoff/scripts/update-check.js +403 -0
- package/skills/report-design/SKILL.md +405 -0
- package/skills/report-design/references/cli-commands.md +184 -0
- package/skills/report-design/references/cli-setup.md +101 -0
- package/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/skills/report-design/references/layouts/finance.md +65 -0
- package/skills/report-design/references/layouts/generic.md +46 -0
- package/skills/report-design/references/layouts/hr.md +48 -0
- package/skills/report-design/references/layouts/marketing.md +45 -0
- package/skills/report-design/references/layouts/operations.md +44 -0
- package/skills/report-design/references/layouts/sales.md +50 -0
- package/skills/report-design/references/native-visuals.md +341 -0
- package/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/skills/report-design/references/slicer.md +89 -0
- package/skills/report-design/references/textbox.md +101 -0
- package/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/skills/report-design/references/troubleshooting.md +135 -0
- package/skills/report-design/references/visual-types.md +78 -0
- package/skills/report-design/scripts/apply-theme.js +243 -0
- package/skills/report-design/scripts/create-visual.js +878 -0
- package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/skills/report-design/scripts/update-check.js +403 -0
- package/skills/report-design/scripts/validate-pbir.js +322 -0
- package/src/content/base.md +12 -68
- package/src/content/mcp-requirements.json +0 -25
- package/src/content/routing.md +19 -74
- package/src/content/skills/bi-start.md +191 -0
- package/src/content/skills/pbi-connect.md +22 -65
- package/src/content/skills/project-kickoff.md +372 -673
- package/src/content/skills/report-design/SKILL.md +376 -0
- package/src/content/skills/report-design/references/cli-commands.md +184 -0
- package/src/content/skills/report-design/references/cli-setup.md +101 -0
- package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/src/content/skills/report-design/references/layouts/finance.md +65 -0
- package/src/content/skills/report-design/references/layouts/generic.md +46 -0
- package/src/content/skills/report-design/references/layouts/hr.md +48 -0
- package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
- package/src/content/skills/report-design/references/layouts/operations.md +44 -0
- package/src/content/skills/report-design/references/layouts/sales.md +50 -0
- package/src/content/skills/report-design/references/native-visuals.md +341 -0
- package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/src/content/skills/report-design/references/slicer.md +89 -0
- package/src/content/skills/report-design/references/textbox.md +101 -0
- package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/src/content/skills/report-design/references/troubleshooting.md +135 -0
- package/src/content/skills/report-design/references/visual-types.md +78 -0
- package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
- package/src/content/skills/report-design/scripts/create-visual.js +878 -0
- package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
- package/bin/commands/install.test.js +0 -289
- package/bin/commands/lint.test.js +0 -103
- package/bin/lib/generators/claude-plugin.test.js +0 -111
- package/bin/lib/mcp-config.test.js +0 -310
- package/bin/lib/microsoft-mcp.test.js +0 -115
- package/bin/utils/mcp-detect.test.js +0 -81
- package/bin/utils/tui.test.js +0 -127
|
@@ -20,10 +20,12 @@ const {
|
|
|
20
20
|
} = require('../microsoft-mcp');
|
|
21
21
|
const { parseSkillMetadata, getSkillPurpose } = require('./shared');
|
|
22
22
|
|
|
23
|
-
// Currently the plugin ships
|
|
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,67 @@ 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
|
+
|
|
165
|
+
function shouldIncludeUpdateCheckPreamble(skill, options = {}) {
|
|
166
|
+
if (options.skipPreamble) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
// bi-start owns the interactive update flow itself in PHASE 0. If we also
|
|
170
|
+
// prepend the generic passive preamble, the session-opener gets two
|
|
171
|
+
// competing update contracts in the same skill.
|
|
172
|
+
return skill.name !== 'bi-start';
|
|
173
|
+
}
|
|
174
|
+
|
|
95
175
|
/**
|
|
96
176
|
* Build command markdown with Claude Code frontmatter.
|
|
97
177
|
*
|
|
98
178
|
* @param {Object} skill - Skill definition object
|
|
99
179
|
* @param {string} libraryPrefix - Library prefix used in generated target
|
|
180
|
+
* @param {Object} [options] - Options
|
|
181
|
+
* @param {boolean} [options.skipPreamble] - Omit the update-check preamble (tests)
|
|
100
182
|
* @returns {string} Command markdown
|
|
101
183
|
*/
|
|
102
|
-
function buildCommandMarkdown(skill, libraryPrefix) {
|
|
184
|
+
function buildCommandMarkdown(skill, libraryPrefix, options = {}) {
|
|
103
185
|
const description = getSkillPurpose(skill.name);
|
|
104
186
|
const content = rewriteLibraryReferences(skill.content, libraryPrefix);
|
|
187
|
+
const preamble = shouldIncludeUpdateCheckPreamble(skill, options) ? UPDATE_CHECK_PREAMBLE : '';
|
|
105
188
|
|
|
106
189
|
return `---
|
|
107
190
|
description: ${toFrontmatterValue(description)}
|
|
@@ -109,7 +192,7 @@ description: ${toFrontmatterValue(description)}
|
|
|
109
192
|
|
|
110
193
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/${skill.name}.md instead. -->
|
|
111
194
|
|
|
112
|
-
${content}`;
|
|
195
|
+
${preamble}${content}`;
|
|
113
196
|
}
|
|
114
197
|
|
|
115
198
|
/**
|
|
@@ -118,10 +201,13 @@ ${content}`;
|
|
|
118
201
|
* @param {Object} skill - Skill definition object
|
|
119
202
|
* @param {string} version - Package version
|
|
120
203
|
* @param {string} libraryPrefix - Library prefix used in generated target
|
|
204
|
+
* @param {Object} [options] - Options
|
|
205
|
+
* @param {boolean} [options.skipPreamble] - Omit the update-check preamble (tests)
|
|
121
206
|
* @returns {string} Skill markdown
|
|
122
207
|
*/
|
|
123
|
-
function buildSkillMarkdown(skill, version, libraryPrefix) {
|
|
208
|
+
function buildSkillMarkdown(skill, version, libraryPrefix, options = {}) {
|
|
124
209
|
const content = rewriteLibraryReferences(skill.content, libraryPrefix);
|
|
210
|
+
const preamble = shouldIncludeUpdateCheckPreamble(skill, options) ? UPDATE_CHECK_PREAMBLE : '';
|
|
125
211
|
|
|
126
212
|
return `---
|
|
127
213
|
name: ${toFrontmatterValue(skill.name)}
|
|
@@ -131,7 +217,37 @@ version: ${toFrontmatterValue(version)}
|
|
|
131
217
|
|
|
132
218
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/${skill.name}.md instead. -->
|
|
133
219
|
|
|
134
|
-
${content}`;
|
|
220
|
+
${preamble}${content}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Copy bin/commands/update-check.js into each generated skill's scripts/
|
|
225
|
+
* directory so the preamble's `{skillBundleDir}/scripts/update-check.js`
|
|
226
|
+
* invocation works under both install paths (npm-based `super install` and
|
|
227
|
+
* Claude Code plugin marketplace). The file is bundled with the npm tarball
|
|
228
|
+
* via bin/, so this is purely a generation-time copy into skills/.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} packageDir - npm package root (source of the canonical script)
|
|
231
|
+
* @param {string} targetDir - plugin target root (contains skills/<name>/)
|
|
232
|
+
* @param {Object[]} skills - Skills to wire the script into
|
|
233
|
+
*/
|
|
234
|
+
function copyUpdateCheckScript(packageDir, targetDir, skills, version = null) {
|
|
235
|
+
if (!packageDir) return;
|
|
236
|
+
const src = path.join(packageDir, 'bin', 'commands', 'update-check.js');
|
|
237
|
+
if (!fs.existsSync(src)) return;
|
|
238
|
+
const sourceContent = fs.readFileSync(src, 'utf8');
|
|
239
|
+
let bundledContent = sourceContent;
|
|
240
|
+
if (version) {
|
|
241
|
+
bundledContent = sourceContent.replace(
|
|
242
|
+
'const BUNDLED_INSTALLED_VERSION = null;',
|
|
243
|
+
`const BUNDLED_INSTALLED_VERSION = ${JSON.stringify(version)};`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
for (const skill of skills) {
|
|
247
|
+
const scriptsDir = path.join(targetDir, 'skills', skill.name, 'scripts');
|
|
248
|
+
ensureDirectory(scriptsDir);
|
|
249
|
+
fs.writeFileSync(path.join(scriptsDir, 'update-check.js'), bundledContent);
|
|
250
|
+
}
|
|
135
251
|
}
|
|
136
252
|
|
|
137
253
|
/**
|
|
@@ -178,6 +294,25 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
178
294
|
ensureDirectory(commandsDir);
|
|
179
295
|
ensureDirectory(skillsDir);
|
|
180
296
|
|
|
297
|
+
// Pull discoverable metadata from the installed package.json when available
|
|
298
|
+
// so the generated plugin.json has the same repo/license/keywords/homepage
|
|
299
|
+
// fields as the published npm package. Falls back to bare identity fields
|
|
300
|
+
// if options.packageDir is missing (unit tests, detached generation).
|
|
301
|
+
let pkgMetadata = {};
|
|
302
|
+
if (options.packageDir) {
|
|
303
|
+
try {
|
|
304
|
+
const pkg = require(path.join(options.packageDir, 'package.json'));
|
|
305
|
+
pkgMetadata = {
|
|
306
|
+
repository: pkg.repository,
|
|
307
|
+
homepage: pkg.homepage,
|
|
308
|
+
license: pkg.license,
|
|
309
|
+
keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
|
|
310
|
+
};
|
|
311
|
+
} catch (_) {
|
|
312
|
+
// Package.json unreadable — generate a minimal manifest and move on.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
181
316
|
const pluginManifest = {
|
|
182
317
|
name: 'bi-superpowers',
|
|
183
318
|
description:
|
|
@@ -186,6 +321,10 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
186
321
|
author: {
|
|
187
322
|
name: 'Lucas Sanchez',
|
|
188
323
|
},
|
|
324
|
+
...(pkgMetadata.repository !== undefined ? { repository: pkgMetadata.repository } : {}),
|
|
325
|
+
...(pkgMetadata.homepage !== undefined ? { homepage: pkgMetadata.homepage } : {}),
|
|
326
|
+
...(pkgMetadata.license !== undefined ? { license: pkgMetadata.license } : {}),
|
|
327
|
+
...(pkgMetadata.keywords !== undefined ? { keywords: pkgMetadata.keywords } : {}),
|
|
189
328
|
};
|
|
190
329
|
|
|
191
330
|
fs.writeFileSync(
|
|
@@ -312,6 +451,9 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
312
451
|
}
|
|
313
452
|
|
|
314
453
|
// ALL skills → skills/*/SKILL.md (universal, discoverable by all Claude tools)
|
|
454
|
+
// Folder-based skills additionally get their `references/` and `scripts/`
|
|
455
|
+
// subfolders copied verbatim so the agent can load on-demand material and
|
|
456
|
+
// invoke helper scripts bundled with the skill.
|
|
315
457
|
for (const skill of skills) {
|
|
316
458
|
const skillDir = path.join(skillsDir, skill.name);
|
|
317
459
|
ensureDirectory(skillDir);
|
|
@@ -319,8 +461,18 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
319
461
|
path.join(skillDir, 'SKILL.md'),
|
|
320
462
|
buildSkillMarkdown(skill, version, libraryPrefix)
|
|
321
463
|
);
|
|
464
|
+
|
|
465
|
+
if (skill.bundleDir) {
|
|
466
|
+
copyDirectory(path.join(skill.bundleDir, 'references'), path.join(skillDir, 'references'));
|
|
467
|
+
copyDirectory(path.join(skill.bundleDir, 'scripts'), path.join(skillDir, 'scripts'));
|
|
468
|
+
}
|
|
322
469
|
}
|
|
323
470
|
|
|
471
|
+
// Ship the update-check helper alongside every skill so the SKILL.md
|
|
472
|
+
// preamble can invoke it via `{skillBundleDir}/scripts/update-check.js`
|
|
473
|
+
// regardless of install path.
|
|
474
|
+
copyUpdateCheckScript(options.packageDir, targetDir, skills, version);
|
|
475
|
+
|
|
324
476
|
console.log(' ✓ Created Claude Code plugin manifest');
|
|
325
477
|
console.log(' ✓ Created .mcp.json with official Microsoft MCP servers');
|
|
326
478
|
console.log(` ✓ Created ${commandSkills.length} plugin commands`);
|
|
@@ -331,6 +483,10 @@ module.exports = {
|
|
|
331
483
|
name: 'Claude Code Plugin',
|
|
332
484
|
description: 'Native Claude Code plugin (recommended)',
|
|
333
485
|
generate,
|
|
486
|
+
buildCommandMarkdown,
|
|
487
|
+
buildSkillMarkdown,
|
|
488
|
+
copyUpdateCheckScript,
|
|
489
|
+
UPDATE_CHECK_PREAMBLE,
|
|
334
490
|
COMMAND_SKILLS,
|
|
335
491
|
REFERENCE_SKILLS,
|
|
336
492
|
};
|
|
@@ -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
|
|
36
|
-
if (
|
|
37
|
-
metadata.triggers =
|
|
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
|
-
|
|
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,
|
package/bin/lib/mcp-config.js
CHANGED
|
@@ -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
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -226,7 +377,10 @@ function makeJsonWriter(agentId) {
|
|
|
226
377
|
const agentConfig = JSON_AGENT_CONFIGS[agentId];
|
|
227
378
|
return function jsonWriter(packageDir) {
|
|
228
379
|
const configPath = agentConfig.configPath();
|
|
229
|
-
|
|
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) || {};
|
|
230
384
|
const wrapperKey = agentConfig.wrapperKey;
|
|
231
385
|
|
|
232
386
|
const newServers = buildJsonServers(agentConfig, packageDir);
|
|
@@ -250,7 +404,7 @@ function makeJsonWriter(agentId) {
|
|
|
250
404
|
// appending the fresh ones.
|
|
251
405
|
function writeCodexConfig(packageDir) {
|
|
252
406
|
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
253
|
-
|
|
407
|
+
assertPathSafeForWrite(configPath);
|
|
254
408
|
const launcher = getLauncherAbsolutePath(packageDir);
|
|
255
409
|
|
|
256
410
|
let existing = '';
|
|
@@ -278,11 +432,9 @@ function writeCodexConfig(packageDir) {
|
|
|
278
432
|
|
|
279
433
|
const content = existing.trimEnd() + newSections;
|
|
280
434
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
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);
|
|
286
438
|
return configPath;
|
|
287
439
|
}
|
|
288
440
|
|
|
@@ -322,9 +474,13 @@ module.exports = {
|
|
|
322
474
|
MICROSOFT_LEARN_URL,
|
|
323
475
|
// Exported for testing
|
|
324
476
|
readJsonSafe,
|
|
477
|
+
readJsonStrict,
|
|
325
478
|
writeJson,
|
|
479
|
+
writeFileAtomic,
|
|
326
480
|
tomlEscape,
|
|
327
481
|
escapeRegex,
|
|
328
482
|
assertNotSymlink,
|
|
483
|
+
assertNoSymlinkedAncestor,
|
|
484
|
+
assertPathSafeForWrite,
|
|
329
485
|
getLauncherAbsolutePath,
|
|
330
486
|
};
|