feat: v2.1.0 - improve Claude skill triggering and CLAUDE.md auto-injection

- Update all 7 skill descriptions to be pushy with explicit trigger phrases
- Add DAX expression limitations section to power-bi-dax skill (VAR/RETURN workarounds)
- Add date/calendar table creation section to power-bi-modeling skill
- Add quick troubleshooting section to power-bi-diagnostics skill
- Auto-inject pbi-cli section into ~/.claude/CLAUDE.md on `pbi connect`
- Auto-remove pbi-cli section on `pbi skills uninstall`
- Extract claude_integration.py module to avoid circular imports
- Add 7 unit tests for CLAUDE.md snippet lifecycle
This commit is contained in:
MinaSaad1 2026-03-27 12:00:41 +02:00
parent 3f3fc7bdf1
commit e4fd958e13
13 changed files with 279 additions and 9 deletions

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pbi-cli-tool"
version = "2.0.0"
version = "2.1.0"
description = "CLI for Power BI semantic models - direct .NET connection for token-efficient AI agent usage"
readme = "README.pypi.md"
license = {text = "MIT"}

View file

@ -1,3 +1,3 @@
"""pbi-cli: CLI for Power BI semantic models via direct .NET interop."""
__version__ = "2.0.0"
__version__ = "2.1.0"

View file

@ -194,6 +194,7 @@ def _ensure_ready() -> None:
pbi connect -d localhost:54321
"""
from pbi_cli.commands.skills_cmd import SKILLS_TARGET_DIR, _get_bundled_skills
from pbi_cli.core.claude_integration import ensure_claude_md_snippet
bundled = _get_bundled_skills()
any_missing = any(not (SKILLS_TARGET_DIR / name / "SKILL.md").exists() for name in bundled)
@ -208,3 +209,5 @@ def _ensure_ready() -> None:
target_file = target_dir / "SKILL.md"
target_file.write_text(source_file.read_text(encoding="utf-8"), encoding="utf-8")
print_info("Skills installed.")
ensure_claude_md_snippet()

View file

@ -115,3 +115,9 @@ def skills_uninstall(ctx: object, skill_name: str | None) -> None:
click.echo(f" {name}: removed", err=True)
click.echo(f"\n{removed_count} skill(s) removed.", err=True)
# Remove CLAUDE.md snippet when uninstalling all skills
if skill_name is None:
from pbi_cli.core.claude_integration import remove_claude_md_snippet
remove_claude_md_snippet()

View file

@ -0,0 +1,72 @@
"""CLAUDE.md snippet management for Claude Code integration."""
from __future__ import annotations
from pathlib import Path
import click
CLAUDE_MD_PATH = Path.home() / ".claude" / "CLAUDE.md"
_PBI_CLI_MARKER_START = "<!-- pbi-cli:start -->"
_PBI_CLI_MARKER_END = "<!-- pbi-cli:end -->"
_PBI_CLI_CLAUDE_MD_SNIPPET = (
"\n"
"<!-- pbi-cli:start -->\n"
"# Power BI CLI (pbi-cli)\n"
"\n"
"When working with Power BI, DAX, semantic models, or data modeling,\n"
"invoke the relevant pbi-cli skill before responding:\n"
"\n"
"- **power-bi-dax** -- DAX queries, measures, calculations\n"
"- **power-bi-modeling** -- tables, columns, measures, relationships\n"
"- **power-bi-diagnostics** -- troubleshooting, tracing, setup\n"
"- **power-bi-deployment** -- TMDL export/import, transactions\n"
"- **power-bi-docs** -- model documentation, data dictionary\n"
"- **power-bi-partitions** -- partitions, M expressions, data sources\n"
"- **power-bi-security** -- RLS roles, perspectives, access control\n"
"\n"
"Critical: Multi-line DAX (VAR/RETURN) cannot be passed via `-e`.\n"
"Use `--file` or stdin piping instead. See power-bi-dax skill.\n"
"<!-- pbi-cli:end -->\n"
)
def ensure_claude_md_snippet() -> None:
"""Append pbi-cli section to ~/.claude/CLAUDE.md if not already present."""
if CLAUDE_MD_PATH.exists():
existing = CLAUDE_MD_PATH.read_text(encoding="utf-8")
if _PBI_CLI_MARKER_START in existing:
return # Already present
else:
CLAUDE_MD_PATH.parent.mkdir(parents=True, exist_ok=True)
existing = ""
new_content = existing.rstrip() + _PBI_CLI_CLAUDE_MD_SNIPPET
CLAUDE_MD_PATH.write_text(new_content, encoding="utf-8")
click.echo(" Added pbi-cli section to ~/.claude/CLAUDE.md", err=True)
def remove_claude_md_snippet() -> None:
"""Remove pbi-cli section from ~/.claude/CLAUDE.md if present."""
if not CLAUDE_MD_PATH.exists():
return
content = CLAUDE_MD_PATH.read_text(encoding="utf-8")
if _PBI_CLI_MARKER_START not in content:
return
start_idx = content.index(_PBI_CLI_MARKER_START)
end_idx = content.index(_PBI_CLI_MARKER_END) + len(_PBI_CLI_MARKER_END)
before = content[:start_idx].rstrip()
after = content[end_idx:].lstrip("\n")
cleaned = before
if after:
cleaned = before + "\n\n" + after
cleaned = cleaned.rstrip() + "\n" if cleaned.strip() else ""
CLAUDE_MD_PATH.write_text(cleaned, encoding="utf-8")
click.echo(" Removed pbi-cli section from ~/.claude/CLAUDE.md", err=True)

View file

@ -1,6 +1,6 @@
---
name: Power BI DAX
description: Write, execute, and optimize DAX queries and measures for Power BI semantic models. Use when the user mentions DAX, Power BI calculations, querying data, or wants to analyze data in a semantic model.
description: Write, execute, and optimize DAX queries and measures for Power BI semantic models using pbi-cli. Invoke this skill whenever the user mentions DAX, queries data in Power BI, writes calculations, creates measures, asks about EVALUATE, SUMMARIZECOLUMNS, CALCULATE, time intelligence, or wants to analyze/aggregate data from a semantic model. Also invoke when the user asks to run a query, test a formula, or check row counts. This skill contains critical guidance on passing DAX expressions via CLI arguments -- multi-line DAX (VAR/RETURN) requires special handling.
tools: pbi-cli
---
@ -36,6 +36,47 @@ pbi dax execute "EVALUATE Sales" --timeout 300 # Custom timeout (seconds)
pbi --json dax execute "EVALUATE Sales"
```
## DAX Expression Limitations in CLI
When passing DAX as a `-e` argument, the shell collapses newlines into a single line. Simple expressions like `SUM(Sales[Amount])` work fine, but multi-line DAX using VAR/RETURN breaks because the DAX parser needs line breaks between those keywords.
**Why this matters:** A measure like `VAR x = [Total Sales] VAR y = [Sales PY] RETURN DIVIDE(x - y, y)` will fail with a syntax error because the engine sees it as one continuous line without statement separators.
**Workarounds (pick one):**
```bash
# Option 1: Pipe from stdin (recommended for measures)
echo 'VAR TotalSales = SUM(Sales[Amount])
VAR TotalCost = SUM(Sales[Cost])
RETURN TotalSales - TotalCost' | pbi measure create "Profit" -e - -t Sales
# Option 2: Write to a .dax file and use --file (for queries)
echo 'EVALUATE
ROW("Result",
VAR x = SUM(Sales[Amount])
RETURN x
)' > query.dax
pbi dax execute --file query.dax
```
**Single-line alternatives (preferred when possible):**
For simple ratio/growth measures, use inline patterns instead of VAR/RETURN:
```bash
# Instead of: VAR x = SUM(...) / VAR y = SUM(...) / RETURN DIVIDE(x, y)
# Use inline DIVIDE -- it handles division-by-zero gracefully (returns BLANK):
pbi measure create "Margin %" \
-e "DIVIDE(SUM(Sales[Amount]) - SUM(Sales[Cost]), SUM(Sales[Amount]))" \
-t Sales --format-string "0.0%"
# Instead of: VAR current = [Total Sales] / VAR prev = [Sales PY] / RETURN DIVIDE(...)
# Reference measures directly in DIVIDE:
pbi measure create "YoY %" \
-e "DIVIDE([Total Sales] - [PY Sales], [PY Sales])" \
-t Sales --format-string "0.0%"
```
## Validating Queries
```bash

View file

@ -1,6 +1,6 @@
---
name: Power BI Deployment
description: Import and export TMDL and TMSL formats, manage model lifecycle with transactions, and version-control Power BI semantic models. Use when the user mentions deploying, publishing, migrating, or version-controlling Power BI models.
description: Import and export TMDL/TMSL formats, manage model lifecycle with transactions, and version-control Power BI semantic models using pbi-cli. Invoke this skill whenever the user mentions "deploy", "export", "import", "TMDL", "TMSL", "version control", "git", "backup", "migrate", "transaction", "commit changes", "rollback", or wants to save/restore model state.
tools: pbi-cli
---

View file

@ -1,6 +1,6 @@
---
name: Power BI Diagnostics
description: Troubleshoot Power BI model performance, trace query execution, manage caches, and verify the pbi-cli environment. Use when the user mentions slow queries, performance issues, tracing, profiling, or setup problems.
description: Troubleshoot Power BI model performance, trace query execution, manage caches, and verify the pbi-cli environment using pbi-cli. Invoke this skill whenever the user says "pbi not working", "setup issues", "connection failed", "slow query", "performance", "profiling", "tracing", "health check", "model audit", "pbi setup", or encounters any pbi-cli error. This is the first skill to check when something goes wrong with pbi-cli.
tools: pbi-cli
---
@ -29,6 +29,25 @@ pbi --json setup --info
pbi --version
```
## Quick Troubleshooting
If pbi-cli isn't working, run these checks in order:
```bash
# 1. Is pbi-cli installed correctly?
pbi --version
pbi setup --info
# 2. Is Power BI Desktop running with a model open?
pbi connect
# 3. Is the connection still alive?
pbi connections last
# 4. Can you query the model?
pbi dax execute "EVALUATE ROW(\"test\", 1)"
```
## Model Health Check
```bash

View file

@ -1,6 +1,6 @@
---
name: Power BI Documentation
description: Auto-document Power BI semantic models by extracting metadata, generating comprehensive documentation, and cataloging all model objects. Use when the user wants to document a Power BI model, create a data dictionary, or audit model contents.
description: Auto-document Power BI semantic models by extracting metadata, generating documentation, and cataloging all model objects using pbi-cli. Invoke this skill whenever the user says "document this model", "what's in this model", "list everything", "data dictionary", "model inventory", "audit contents", "catalog", "describe the model", or wants to understand what objects exist in a semantic model.
tools: pbi-cli
---

View file

@ -1,6 +1,6 @@
---
name: Power BI Modeling
description: Create and manage Power BI semantic model objects including tables, columns, measures, relationships, hierarchies, and calculation groups. Use when the user mentions Power BI modeling, semantic models, or wants to create or modify model objects.
description: Create and manage Power BI semantic model structure using pbi-cli -- tables, columns, measures, relationships, hierarchies, calculation groups, and date/calendar tables. Invoke this skill whenever the user says "create table", "add measure", "add column", "create relationship", "date table", "calendar table", "star schema", "mark as date table", "add hierarchy", "calculation group", or any model-building task. Also invoke when creating multiple measures at once -- the skill contains critical guidance on multi-line DAX expression handling.
tools: pbi-cli
---
@ -54,6 +54,16 @@ pbi measure rename "Old" "New" -t Sales # Rename
pbi measure move "Revenue" -t Sales --to-table Finance # Move to another table
```
**Multi-line DAX in measure expressions:** The `-e` flag passes DAX as a shell argument, which collapses newlines. For simple expressions like `SUM(Sales[Amount])` or `DIVIDE([A] - [B], [B])` this works fine. For complex expressions using VAR/RETURN, pipe from stdin instead:
```bash
echo 'VAR TotalSales = SUM(Sales[Amount])
VAR TotalCost = SUM(Sales[Cost])
RETURN TotalSales - TotalCost' | pbi measure create "Profit" -e - -t Sales
```
See the **power-bi-dax** skill for the full explanation and more workarounds.
## Relationships
```bash
@ -89,6 +99,22 @@ pbi calc-group create-item "YTD" \
pbi calc-group delete "Time Intelligence" # Delete group
```
## Creating a Date/Calendar Table
Date tables are essential for time intelligence functions (TOTALYTD, SAMEPERIODLASTYEAR, DATEADD, etc.).
```bash
# Create a calculated date table with DAX (covers full calendar years)
pbi table create Calendar \
--dax-expression "ADDCOLUMNS(CALENDAR(DATE(2023,1,1), DATE(2024,12,31)), \"Year\", YEAR([Date]), \"MonthNumber\", MONTH([Date]), \"MonthName\", FORMAT([Date], \"MMMM\"), \"Quarter\", \"Q\" & FORMAT([Date], \"Q\"))"
# Mark it as a date table (required for time intelligence)
pbi table mark-date Calendar --date-column Date
# Verify it's recognized as a date table
pbi calendar list
```
## Workflow: Create a Star Schema
```bash

View file

@ -1,6 +1,6 @@
---
name: Power BI Partitions & Expressions
description: Manage Power BI table partitions, named expressions (M/Power Query sources), and calendar table configuration. Use when the user mentions partitions, data sources, M expressions, Power Query, incremental refresh, or calendar/date tables.
description: Manage Power BI table partitions, named expressions (M/Power Query data sources), and calendar table configuration using pbi-cli. Invoke this skill whenever the user mentions "partitions", "data sources", "M expressions", "Power Query", "incremental refresh", "named expressions", "connection parameters", or wants to configure how tables load data. For broader modeling tasks (measures, relationships, hierarchies), see power-bi-modeling instead.
tools: pbi-cli
---

View file

@ -1,6 +1,6 @@
---
name: Power BI Security
description: Configure row-level security (RLS) roles, manage object-level security, and set up perspectives for Power BI semantic models. Use when the user mentions Power BI security, RLS, access control, or data restrictions.
description: Configure row-level security (RLS) roles, object-level security, and perspectives for Power BI semantic models using pbi-cli. Invoke this skill whenever the user mentions "security", "RLS", "row-level security", "access control", "data restrictions", "who can see", "filter by user", "perspectives", "limit visibility", or wants to restrict data access by role.
tools: pbi-cli
---

103
tests/test_claude_md.py Normal file
View file

@ -0,0 +1,103 @@
"""Tests for CLAUDE.md snippet injection and removal."""
from __future__ import annotations
from pathlib import Path
import pytest
@pytest.fixture
def tmp_claude_md(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect CLAUDE_MD_PATH to a temp directory."""
claude_md = tmp_path / ".claude" / "CLAUDE.md"
monkeypatch.setattr("pbi_cli.core.claude_integration.CLAUDE_MD_PATH", claude_md)
return claude_md
def _get_funcs():
"""Lazy import to avoid circular import at collection time."""
from pbi_cli.core.claude_integration import (
_PBI_CLI_MARKER_END,
_PBI_CLI_MARKER_START,
ensure_claude_md_snippet,
remove_claude_md_snippet,
)
return (
ensure_claude_md_snippet,
remove_claude_md_snippet,
_PBI_CLI_MARKER_START,
_PBI_CLI_MARKER_END,
)
class TestEnsureClaudeMdSnippet:
def test_creates_file_when_missing(self, tmp_claude_md: Path) -> None:
ensure, _, start, end = _get_funcs()
assert not tmp_claude_md.exists()
ensure()
assert tmp_claude_md.exists()
content = tmp_claude_md.read_text(encoding="utf-8")
assert start in content
assert end in content
assert "power-bi-dax" in content
def test_appends_to_existing(self, tmp_claude_md: Path) -> None:
ensure, _, start, end = _get_funcs()
tmp_claude_md.parent.mkdir(parents=True, exist_ok=True)
tmp_claude_md.write_text("# My Preferences\n\n- No em dashes\n", encoding="utf-8")
ensure()
content = tmp_claude_md.read_text(encoding="utf-8")
assert content.startswith("# My Preferences")
assert "- No em dashes" in content
assert start in content
assert end in content
def test_is_idempotent(self, tmp_claude_md: Path) -> None:
ensure, _, _, _ = _get_funcs()
tmp_claude_md.parent.mkdir(parents=True, exist_ok=True)
tmp_claude_md.write_text("# Existing\n", encoding="utf-8")
ensure()
first_content = tmp_claude_md.read_text(encoding="utf-8")
ensure()
second_content = tmp_claude_md.read_text(encoding="utf-8")
assert first_content == second_content
class TestRemoveClaudeMdSnippet:
def test_removes_snippet(self, tmp_claude_md: Path) -> None:
ensure, remove, start, end = _get_funcs()
tmp_claude_md.parent.mkdir(parents=True, exist_ok=True)
tmp_claude_md.write_text("# Existing\n", encoding="utf-8")
ensure()
assert start in tmp_claude_md.read_text(encoding="utf-8")
remove()
content = tmp_claude_md.read_text(encoding="utf-8")
assert start not in content
assert end not in content
def test_preserves_other_content(self, tmp_claude_md: Path) -> None:
ensure, remove, start, _ = _get_funcs()
tmp_claude_md.parent.mkdir(parents=True, exist_ok=True)
tmp_claude_md.write_text("# My Preferences\n\n- No em dashes\n", encoding="utf-8")
ensure()
remove()
content = tmp_claude_md.read_text(encoding="utf-8")
assert "# My Preferences" in content
assert "- No em dashes" in content
assert start not in content
def test_noop_when_not_present(self, tmp_claude_md: Path) -> None:
_, remove, _, _ = _get_funcs()
tmp_claude_md.parent.mkdir(parents=True, exist_ok=True)
original = "# My Preferences\n\n- No em dashes\n"
tmp_claude_md.write_text(original, encoding="utf-8")
remove()
assert tmp_claude_md.read_text(encoding="utf-8") == original
def test_noop_when_file_missing(self, tmp_claude_md: Path) -> None:
_, remove, _, _ = _get_funcs()
assert not tmp_claude_md.exists()
remove() # Should not raise
assert not tmp_claude_md.exists()