From eafcfe5bd12ea5de5b06a9da5e5e52492a37bceb Mon Sep 17 00:00:00 2001 From: lmiranda Date: Mon, 2 Feb 2026 19:33:45 -0500 Subject: [PATCH] fix(scripts): MCP server mapping and CLAUDE.md section markers Issue 1 - MCP Server Mapping: - Add mcp_servers field to plugin.json for plugins using shared MCP servers - projman/pr-review now install gitea MCP server - cmdb-assistant now installs netbox MCP server - Scripts read MCP server names from plugin.json Issue 2 - CLAUDE.md Section Markers: - Install wraps content with HTML comment markers for precise removal - Uninstall uses markers first, falls back to legacy header detection - Fixes code block false positives during uninstall Bug fix: - Change ((servers_added++)) to ((++servers_added)) to avoid exit code 1 with set -e when incrementing from 0 Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 24 +++ .../cmdb-assistant/.claude-plugin/plugin.json | 1 + .../.claude-plugin/plugin.json | 1 + .../data-platform/.claude-plugin/plugin.json | 1 + plugins/pr-review/.claude-plugin/plugin.json | 1 + plugins/projman/.claude-plugin/plugin.json | 1 + .../viz-platform/.claude-plugin/plugin.json | 1 + scripts/install-plugin.sh | 125 +++++++++++----- scripts/list-installed.sh | 95 ++++++++---- scripts/uninstall-plugin.sh | 141 +++++++++++++++--- 10 files changed, 297 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5ee1d..eb0d249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,30 @@ New scripts for installing marketplace plugins into consumer projects: **Documentation:** `docs/CONFIGURATION.md` updated with "Installing Plugins to Consumer Projects" section. +### Fixed + +#### Plugin Installation Scripts — MCP Mapping & Section Markers + +**MCP Server Mapping:** +- Added `mcp_servers` field to plugin.json for plugins that use shared MCP servers +- `projman` and `pr-review` now correctly install `gitea` MCP server +- `cmdb-assistant` now correctly installs `netbox` MCP server +- Scripts read MCP server names from plugin.json instead of assuming plugin name = server name + +**CLAUDE.md Section Markers:** +- Install script now wraps integration content with HTML comment markers: + `` and `` +- Uninstall script uses markers for precise section removal (no more code block false positives) +- Backward compatible: falls back to legacy header detection for pre-marker installations + +**Plugins updated with `mcp_servers` field:** +- `projman` → `["gitea"]` +- `pr-review` → `["gitea"]` +- `cmdb-assistant` → `["netbox"]` +- `data-platform` → `["data-platform"]` +- `viz-platform` → `["viz-platform"]` +- `contract-validator` → `["contract-validator"]` + --- ## [5.8.0] - 2026-02-02 diff --git a/plugins/cmdb-assistant/.claude-plugin/plugin.json b/plugins/cmdb-assistant/.claude-plugin/plugin.json index 801eb77..3cd535b 100644 --- a/plugins/cmdb-assistant/.claude-plugin/plugin.json +++ b/plugins/cmdb-assistant/.claude-plugin/plugin.json @@ -19,5 +19,6 @@ "data-quality", "validation" ], + "mcp_servers": ["netbox"], "commands": ["./commands/"] } diff --git a/plugins/contract-validator/.claude-plugin/plugin.json b/plugins/contract-validator/.claude-plugin/plugin.json index aefb98e..5fac840 100644 --- a/plugins/contract-validator/.claude-plugin/plugin.json +++ b/plugins/contract-validator/.claude-plugin/plugin.json @@ -17,5 +17,6 @@ "interfaces", "cross-plugin" ], + "mcp_servers": ["contract-validator"], "commands": ["./commands/"] } diff --git a/plugins/data-platform/.claude-plugin/plugin.json b/plugins/data-platform/.claude-plugin/plugin.json index 18f596c..94fe4cc 100644 --- a/plugins/data-platform/.claude-plugin/plugin.json +++ b/plugins/data-platform/.claude-plugin/plugin.json @@ -18,5 +18,6 @@ "etl", "dataframe" ], + "mcp_servers": ["data-platform"], "commands": ["./commands/"] } diff --git a/plugins/pr-review/.claude-plugin/plugin.json b/plugins/pr-review/.claude-plugin/plugin.json index 6ee1e11..add585f 100644 --- a/plugins/pr-review/.claude-plugin/plugin.json +++ b/plugins/pr-review/.claude-plugin/plugin.json @@ -16,5 +16,6 @@ "performance", "multi-agent" ], + "mcp_servers": ["gitea"], "commands": ["./commands/"] } diff --git a/plugins/projman/.claude-plugin/plugin.json b/plugins/projman/.claude-plugin/plugin.json index b0d7d92..00a2339 100644 --- a/plugins/projman/.claude-plugin/plugin.json +++ b/plugins/projman/.claude-plugin/plugin.json @@ -16,5 +16,6 @@ "agile", "lessons-learned" ], + "mcp_servers": ["gitea"], "commands": ["./commands/"] } diff --git a/plugins/viz-platform/.claude-plugin/plugin.json b/plugins/viz-platform/.claude-plugin/plugin.json index 5edce0d..3c304f3 100644 --- a/plugins/viz-platform/.claude-plugin/plugin.json +++ b/plugins/viz-platform/.claude-plugin/plugin.json @@ -19,5 +19,6 @@ "visualization", "dmc" ], + "mcp_servers": ["viz-platform"], "commands": ["./commands/"] } diff --git a/scripts/install-plugin.sh b/scripts/install-plugin.sh index 7740d5f..0d19766 100755 --- a/scripts/install-plugin.sh +++ b/scripts/install-plugin.sh @@ -7,7 +7,7 @@ # # This script: # 1. Validates plugin exists in the marketplace -# 2. Updates target project's .mcp.json with MCP server entry (if applicable) +# 2. Updates target project's .mcp.json with MCP server entries (if applicable) # 3. Appends CLAUDE.md integration snippet to target project # 4. Is idempotent (safe to run multiple times) # @@ -39,6 +39,7 @@ log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } # --- Track Changes --- CHANGES_MADE=() SKIPPED=() +MCP_SERVERS_INSTALLED=() # --- Usage --- usage() { @@ -114,15 +115,28 @@ validate_target() { fi } -# --- Check if MCP Server Exists for Plugin --- -has_mcp_server() { +# --- Get MCP Servers for Plugin --- +# Reads the mcp_servers array from plugin.json +# Returns newline-separated list of MCP server names, or empty if none +get_mcp_servers() { local plugin_name="$1" - local mcp_dir="$REPO_ROOT/mcp-servers/$plugin_name" + local plugin_json="$REPO_ROOT/plugins/$plugin_name/.claude-plugin/plugin.json" - if [[ -d "$mcp_dir" && -f "$mcp_dir/run.sh" ]]; then - return 0 + if [[ ! -f "$plugin_json" ]]; then + return fi - return 1 + + # Read mcp_servers array from plugin.json + # Returns empty if field doesn't exist or is empty + jq -r '.mcp_servers // [] | .[]' "$plugin_json" 2>/dev/null || true +} + +# --- Check if plugin has any MCP servers --- +has_mcp_servers() { + local plugin_name="$1" + local servers + servers=$(get_mcp_servers "$plugin_name") + [[ -n "$servers" ]] } # --- Update .mcp.json --- @@ -130,12 +144,14 @@ update_mcp_json() { local plugin_name="$1" local target_path="$2" local mcp_json="$target_path/.mcp.json" - local mcp_server_path="$REPO_ROOT/mcp-servers/$plugin_name/run.sh" - # Check if plugin has MCP server - if ! has_mcp_server "$plugin_name"; then - log_skip "Plugin '$plugin_name' has no MCP server - skipping .mcp.json update" - SKIPPED+=(".mcp.json: No MCP server for $plugin_name") + # Get MCP servers for this plugin + local mcp_servers + mcp_servers=$(get_mcp_servers "$plugin_name") + + if [[ -z "$mcp_servers" ]]; then + log_skip "Plugin '$plugin_name' has no MCP servers - skipping .mcp.json update" + SKIPPED+=(".mcp.json: No MCP servers for $plugin_name") return 0 fi @@ -146,21 +162,37 @@ update_mcp_json() { CHANGES_MADE+=("Created .mcp.json") fi - # Check if entry already exists - if jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then - log_skip "MCP server '$plugin_name' already in .mcp.json" - SKIPPED+=(".mcp.json: $plugin_name already present") - return 0 - fi + # Add each MCP server + local servers_added=0 + while IFS= read -r server_name; do + [[ -z "$server_name" ]] && continue - # Add MCP server entry - log_info "Adding MCP server '$plugin_name' to .mcp.json" - local tmp_file=$(mktemp) - jq ".mcpServers[\"$plugin_name\"] = {\"command\": \"$mcp_server_path\", \"args\": []}" "$mcp_json" > "$tmp_file" - mv "$tmp_file" "$mcp_json" + local mcp_server_path="$REPO_ROOT/mcp-servers/$server_name/run.sh" - CHANGES_MADE+=("Added $plugin_name to .mcp.json") - log_success "Added MCP server entry for '$plugin_name'" + # Verify server exists + if [[ ! -f "$mcp_server_path" ]]; then + log_warning "MCP server '$server_name' not found at $mcp_server_path" + continue + fi + + # Check if entry already exists + if jq -e ".mcpServers[\"$server_name\"]" "$mcp_json" > /dev/null 2>&1; then + log_skip "MCP server '$server_name' already in .mcp.json" + SKIPPED+=(".mcp.json: $server_name already present") + continue + fi + + # Add MCP server entry + log_info "Adding MCP server '$server_name' to .mcp.json" + local tmp_file=$(mktemp) + jq ".mcpServers[\"$server_name\"] = {\"command\": \"$mcp_server_path\", \"args\": []}" "$mcp_json" > "$tmp_file" + mv "$tmp_file" "$mcp_json" + + CHANGES_MADE+=("Added $server_name to .mcp.json") + MCP_SERVERS_INSTALLED+=("$server_name") + log_success "Added MCP server entry for '$server_name'" + ((++servers_added)) + done <<< "$mcp_servers" } # --- Update CLAUDE.md --- @@ -189,22 +221,25 @@ EOF CHANGES_MADE+=("Created CLAUDE.md") fi - # Read integration content - local integration_content - integration_content=$(cat "$integration_file") - - # Extract the first line (# header) to use as marker for detection - local header_marker - header_marker=$(head -1 "$integration_file") - - # Check if already integrated - look for the integration file's header - # Handles both formats: "# {name} Plugin - CLAUDE.md Integration" and "# {name} CLAUDE.md Integration" - if grep -qE "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null; then + # Check if already integrated using HTML comment marker (preferred) + local begin_marker="" + if grep -qF "$begin_marker" "$target_claude_md" 2>/dev/null; then log_skip "Plugin '$plugin_name' integration already in CLAUDE.md" SKIPPED+=("CLAUDE.md: $plugin_name already present") return 0 fi + # Fallback: check for legacy header format (backward compatibility) + if grep -qE "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null; then + log_skip "Plugin '$plugin_name' integration already in CLAUDE.md (legacy format)" + SKIPPED+=("CLAUDE.md: $plugin_name already present") + return 0 + fi + + # Read integration content + local integration_content + integration_content=$(cat "$integration_file") + # Check for or create Marketplace Plugin Integration section local section_header="## Marketplace Plugin Integration" @@ -217,12 +252,18 @@ EOF echo "" >> "$target_claude_md" fi - # Append integration content + # Append integration content with HTML comment markers log_info "Adding '$plugin_name' integration to CLAUDE.md" + local end_marker="" + echo "" >> "$target_claude_md" echo "---" >> "$target_claude_md" echo "" >> "$target_claude_md" + echo "$begin_marker" >> "$target_claude_md" + echo "" >> "$target_claude_md" echo "$integration_content" >> "$target_claude_md" + echo "" >> "$target_claude_md" + echo "$end_marker" >> "$target_claude_md" CHANGES_MADE+=("Added $plugin_name integration to CLAUDE.md") log_success "Added CLAUDE.md integration for '$plugin_name'" @@ -287,8 +328,14 @@ print_summary() { fi echo "" - # MCP tools reminder - if has_mcp_server "$plugin_name"; then + # MCP servers info + if [[ ${#MCP_SERVERS_INSTALLED[@]} -gt 0 ]]; then + echo -e "${CYAN}MCP Servers Installed:${NC}" + for server in "${MCP_SERVERS_INSTALLED[@]}"; do + echo " - $server" + done + echo "" + elif has_mcp_servers "$plugin_name"; then echo -e "${CYAN}MCP Tools:${NC}" echo " This plugin includes MCP server tools. Use ToolSearch to discover them." echo "" diff --git a/scripts/list-installed.sh b/scripts/list-installed.sh index 0233c16..a19802b 100755 --- a/scripts/list-installed.sh +++ b/scripts/list-installed.sh @@ -67,13 +67,25 @@ get_available_plugins() { done } -# --- Get Plugins with MCP Servers --- -get_mcp_plugins() { - for dir in "$REPO_ROOT"/mcp-servers/*/; do - if [[ -d "$dir" && -f "$dir/run.sh" ]]; then - basename "$dir" - fi - done +# --- Get MCP Servers for Plugin --- +# Reads the mcp_servers array from plugin.json +get_mcp_servers() { + local plugin_name="$1" + local plugin_json="$REPO_ROOT/plugins/$plugin_name/.claude-plugin/plugin.json" + + if [[ ! -f "$plugin_json" ]]; then + return + fi + + jq -r '.mcp_servers // [] | .[]' "$plugin_json" 2>/dev/null || true +} + +# --- Check if plugin has any MCP servers defined --- +has_mcp_servers() { + local plugin_name="$1" + local servers + servers=$(get_mcp_servers "$plugin_name") + [[ -n "$servers" ]] } # --- Check MCP Installation --- @@ -86,18 +98,28 @@ check_mcp_installed() { return 1 fi - # Check if this plugin's MCP server is referenced - if jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then + # Get MCP servers for this plugin from plugin.json + local mcp_servers + mcp_servers=$(get_mcp_servers "$plugin_name") + + if [[ -z "$mcp_servers" ]]; then + # Plugin has no MCP servers defined, so MCP check passes return 0 fi - # Also check if any entry points to this marketplace's mcp-servers - if grep -q "leo-claude-mktplace.*mcp-servers/$plugin_name" "$mcp_json" 2>/dev/null || \ - grep -q "claude-plugins-work.*mcp-servers/$plugin_name" "$mcp_json" 2>/dev/null; then - return 0 - fi + # Check if ALL required MCP servers are present + while IFS= read -r server_name; do + [[ -z "$server_name" ]] && continue - return 1 + if ! jq -e ".mcpServers[\"$server_name\"]" "$mcp_json" > /dev/null 2>&1; then + # Also check if any entry points to this marketplace's mcp-servers + if ! grep -q "mcp-servers/$server_name" "$mcp_json" 2>/dev/null; then + return 1 + fi + fi + done <<< "$mcp_servers" + + return 0 } # --- Check CLAUDE.md Integration --- @@ -110,11 +132,13 @@ check_claude_md_installed() { return 1 fi - # Look for plugin-specific headers that install-plugin.sh adds - # Formats vary by plugin: - # "# {plugin-name} Plugin - CLAUDE.md Integration" - # "# {plugin-name} CLAUDE.md Integration" - # Use regex to match both patterns + # Check for HTML comment marker (preferred, new format) + local begin_marker="" + if grep -qF "$begin_marker" "$target_claude_md" 2>/dev/null; then + return 0 + fi + + # Fallback: check for legacy header format if grep -qE "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null; then return 0 fi @@ -185,15 +209,15 @@ NOT_INSTALLED=() for plugin in $(get_available_plugins); do mcp_installed=false claude_installed=false - has_mcp_server=false + needs_mcp=false - # Check if plugin has MCP server - if [[ -d "$REPO_ROOT/mcp-servers/$plugin" ]]; then - has_mcp_server=true + # Check if plugin has MCP servers defined + if has_mcp_servers "$plugin"; then + needs_mcp=true fi - # Check MCP installation (only if plugin has MCP server) - if $has_mcp_server && check_mcp_installed "$plugin" "$TARGET_PATH"; then + # Check MCP installation + if check_mcp_installed "$plugin" "$TARGET_PATH"; then mcp_installed=true INSTALLED_MCP[$plugin]=true fi @@ -205,19 +229,20 @@ for plugin in $(get_available_plugins); do fi # Categorize - if $mcp_installed || $claude_installed; then - if $has_mcp_server; then - if $mcp_installed && $claude_installed; then + if $claude_installed; then + if $needs_mcp; then + if $mcp_installed; then INSTALLED_PLUGINS+=("$plugin") else PARTIAL_PLUGINS+=("$plugin") fi else # Plugins without MCP servers just need CLAUDE.md - if $claude_installed; then - INSTALLED_PLUGINS+=("$plugin") - fi + INSTALLED_PLUGINS+=("$plugin") fi + elif $mcp_installed && $needs_mcp; then + # Has MCP but missing CLAUDE.md + PARTIAL_PLUGINS+=("$plugin") else NOT_INSTALLED+=("$plugin") fi @@ -247,7 +272,11 @@ if [[ ${#PARTIAL_PLUGINS[@]} -gt 0 ]]; then if [[ -v INSTALLED_MCP[$plugin] ]]; then echo " ✓ MCP server configured in .mcp.json" else - echo " ✗ MCP server NOT in .mcp.json" + # Show which MCP servers are missing + mcp_servers=$(get_mcp_servers "$plugin") + if [[ -n "$mcp_servers" ]]; then + echo " ✗ MCP server(s) NOT in .mcp.json: $mcp_servers" + fi fi if [[ -v INSTALLED_CLAUDE_MD[$plugin] ]]; then echo " ✓ Integration in CLAUDE.md" diff --git a/scripts/uninstall-plugin.sh b/scripts/uninstall-plugin.sh index 8949779..e0c62b4 100755 --- a/scripts/uninstall-plugin.sh +++ b/scripts/uninstall-plugin.sh @@ -6,7 +6,7 @@ # Usage: ./scripts/uninstall-plugin.sh # # This script: -# 1. Removes MCP server entry from target project's .mcp.json +# 1. Removes MCP server entries from target project's .mcp.json # 2. Removes CLAUDE.md integration section for the plugin # 3. Is idempotent (safe to run multiple times) # @@ -76,6 +76,22 @@ validate_target() { log_success "Target project found: $target_path" } +# --- Get MCP Servers for Plugin --- +# Reads the mcp_servers array from plugin.json +# Returns newline-separated list of MCP server names, or empty if none +get_mcp_servers() { + local plugin_name="$1" + local plugin_json="$REPO_ROOT/plugins/$plugin_name/.claude-plugin/plugin.json" + + if [[ ! -f "$plugin_json" ]]; then + return + fi + + # Read mcp_servers array from plugin.json + # Returns empty if field doesn't exist or is empty + jq -r '.mcp_servers // [] | .[]' "$plugin_json" 2>/dev/null || true +} + # --- Remove from .mcp.json --- remove_from_mcp_json() { local plugin_name="$1" @@ -89,21 +105,48 @@ remove_from_mcp_json() { return 0 fi - # Check if entry exists - if ! jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then - log_skip "MCP server '$plugin_name' not in .mcp.json" - SKIPPED+=(".mcp.json: $plugin_name not present") + # Get MCP servers for this plugin + local mcp_servers + mcp_servers=$(get_mcp_servers "$plugin_name") + + if [[ -z "$mcp_servers" ]]; then + # Fallback: try to remove entry with plugin name (backward compatibility) + if jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then + log_info "Removing MCP server '$plugin_name' from .mcp.json" + local tmp_file=$(mktemp) + jq "del(.mcpServers[\"$plugin_name\"])" "$mcp_json" > "$tmp_file" + mv "$tmp_file" "$mcp_json" + CHANGES_MADE+=("Removed $plugin_name from .mcp.json") + log_success "Removed MCP server entry for '$plugin_name'" + else + log_skip "Plugin '$plugin_name' has no MCP servers configured" + SKIPPED+=(".mcp.json: No MCP servers for $plugin_name") + fi return 0 fi - # Remove MCP server entry - log_info "Removing MCP server '$plugin_name' from .mcp.json" - local tmp_file=$(mktemp) - jq "del(.mcpServers[\"$plugin_name\"])" "$mcp_json" > "$tmp_file" - mv "$tmp_file" "$mcp_json" + # Remove each MCP server + local servers_removed=0 + while IFS= read -r server_name; do + [[ -z "$server_name" ]] && continue - CHANGES_MADE+=("Removed $plugin_name from .mcp.json") - log_success "Removed MCP server entry for '$plugin_name'" + # Check if entry exists + if ! jq -e ".mcpServers[\"$server_name\"]" "$mcp_json" > /dev/null 2>&1; then + log_skip "MCP server '$server_name' not in .mcp.json" + SKIPPED+=(".mcp.json: $server_name not present") + continue + fi + + # Remove MCP server entry + log_info "Removing MCP server '$server_name' from .mcp.json" + local tmp_file=$(mktemp) + jq "del(.mcpServers[\"$server_name\"])" "$mcp_json" > "$tmp_file" + mv "$tmp_file" "$mcp_json" + + CHANGES_MADE+=("Removed $server_name from .mcp.json") + log_success "Removed MCP server entry for '$server_name'" + ((++servers_removed)) + done <<< "$mcp_servers" } # --- Remove from CLAUDE.md --- @@ -119,8 +162,64 @@ remove_from_claude_md() { return 0 fi - # Look for the plugin section header (handles multiple formats) - # Formats: "# {plugin-name} Plugin - CLAUDE.md Integration" or "# {plugin-name} CLAUDE.md Integration" + # Try HTML comment markers first (preferred method) + local begin_marker="" + local end_marker="" + + if grep -qF "$begin_marker" "$target_claude_md" 2>/dev/null; then + log_info "Removing '$plugin_name' section from CLAUDE.md (using markers)" + + # Remove everything between markers (inclusive) and preceding --- + local tmp_file=$(mktemp) + awk -v begin="$begin_marker" -v end="$end_marker" ' + BEGIN { skip = 0; prev_hr = 0; buffer = "" } + { + is_hr = /^---[[:space:]]*$/ + + if ($0 == begin) { + skip = 1 + # If previous line was ---, dont print it + if (prev_hr) { + buffer = "" + } + next + } + + if (skip) { + if ($0 == end) { + skip = 0 + } + next + } + + # Print buffered content + if (buffer != "") { + print buffer + } + + # Buffer current line (in case its --- before a marker) + buffer = $0 + prev_hr = is_hr + } + END { + # Print final buffered content + if (buffer != "") { + print buffer + } + } + ' "$target_claude_md" > "$tmp_file" + + # Clean up multiple consecutive blank lines + awk 'NF{blank=0} !NF{blank++} blank<=2' "$tmp_file" > "${tmp_file}.clean" + mv "${tmp_file}.clean" "$target_claude_md" + rm -f "$tmp_file" + + CHANGES_MADE+=("Removed $plugin_name section from CLAUDE.md") + log_success "Removed CLAUDE.md section for '$plugin_name'" + return 0 + fi + + # Fallback: try legacy header-based detection local section_header section_header=$(grep -E "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null | head -1) @@ -130,10 +229,9 @@ remove_from_claude_md() { return 0 fi - log_info "Removing '$plugin_name' section from CLAUDE.md" + log_info "Removing '$plugin_name' section from CLAUDE.md (legacy format)" # Create temp file and use awk to remove section - # Remove from header to next "---" divider or next plugin header local tmp_file=$(mktemp) awk -v header="$section_header" ' @@ -155,25 +253,24 @@ remove_from_claude_md() { is_hr = /^---[[:space:]]*$/ && !in_code_block # Check if this is a new plugin section header (only outside code blocks) - # Patterns: "# {name} Plugin - CLAUDE.md Integration" or "# {name} CLAUDE.md Integration" is_new_plugin_section = /^# [a-z-]+( Plugin)? -? ?CLAUDE\.md Integration/ && !in_code_block && $0 != header + # Check for HTML marker (new format) + is_begin_marker = /^