Initial commit
This commit is contained in:
448
skills/nav-update-claude/functions/claude_updater.py
Executable file
448
skills/nav-update-claude/functions/claude_updater.py
Executable file
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Navigator CLAUDE.md Updater
|
||||
Extracts customizations and generates updated CLAUDE.md with v3.1 template
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from urllib import request
|
||||
from urllib.error import URLError, HTTPError
|
||||
|
||||
def get_plugin_version() -> Optional[str]:
|
||||
"""
|
||||
Get installed Navigator plugin version from plugin.json.
|
||||
|
||||
Returns:
|
||||
Version string (e.g., "4.3.0") or None if not found
|
||||
"""
|
||||
possible_paths = [
|
||||
Path.home() / '.claude' / 'plugins' / 'marketplaces' / 'navigator-marketplace' / '.claude-plugin' / 'plugin.json',
|
||||
Path.home() / '.config' / 'claude' / 'plugins' / 'navigator' / '.claude-plugin' / 'plugin.json',
|
||||
Path.home() / '.claude' / 'plugins' / 'navigator' / '.claude-plugin' / 'plugin.json',
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
data = json.load(f)
|
||||
return data.get('version')
|
||||
except (json.JSONDecodeError, FileNotFoundError, PermissionError):
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def fetch_template_from_github(version: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Fetch CLAUDE.md template from GitHub releases.
|
||||
|
||||
Priority:
|
||||
1. Specified version (e.g., 'v4.3.0' or '4.3.0')
|
||||
2. Detected plugin version
|
||||
3. Returns None (caller should use bundled fallback)
|
||||
|
||||
Args:
|
||||
version: Specific version to fetch (optional)
|
||||
|
||||
Returns:
|
||||
Template content as string, or None if fetch fails
|
||||
"""
|
||||
if not version:
|
||||
version = get_plugin_version()
|
||||
|
||||
if not version:
|
||||
return None
|
||||
|
||||
# Ensure version has 'v' prefix for GitHub URL
|
||||
if not version.startswith('v'):
|
||||
version = f'v{version}'
|
||||
|
||||
github_url = f"https://raw.githubusercontent.com/alekspetrov/navigator/{version}/templates/CLAUDE.md"
|
||||
|
||||
try:
|
||||
req = request.Request(github_url)
|
||||
req.add_header('User-Agent', 'Navigator-CLAUDE-Updater')
|
||||
|
||||
with request.urlopen(req, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
content = response.read().decode('utf-8')
|
||||
return content
|
||||
except (URLError, HTTPError, TimeoutError) as e:
|
||||
# Silent fail - caller will use bundled template
|
||||
print(f"⚠️ Could not fetch template from GitHub ({version}): {e}", file=sys.stderr)
|
||||
print(f" Falling back to bundled template", file=sys.stderr)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def get_template_path(bundled_template_dir: str, version: Optional[str] = None) -> tuple[str, bool]:
|
||||
"""
|
||||
Get template path, preferring GitHub source over bundled.
|
||||
|
||||
Args:
|
||||
bundled_template_dir: Path to bundled templates directory
|
||||
version: Optional specific version to fetch
|
||||
|
||||
Returns:
|
||||
Tuple of (template_path_or_content, is_from_github)
|
||||
"""
|
||||
# Try GitHub first
|
||||
github_template = fetch_template_from_github(version)
|
||||
|
||||
if github_template:
|
||||
# Write to temporary file
|
||||
import tempfile
|
||||
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False)
|
||||
temp_file.write(github_template)
|
||||
temp_file.close()
|
||||
|
||||
detected_version = version or get_plugin_version()
|
||||
print(f"✓ Using template from GitHub ({detected_version})", file=sys.stderr)
|
||||
return (temp_file.name, True)
|
||||
|
||||
# Fallback to bundled
|
||||
bundled_path = Path(bundled_template_dir) / 'CLAUDE.md'
|
||||
if bundled_path.exists():
|
||||
bundled_version = get_plugin_version() or "unknown"
|
||||
print(f"✓ Using bundled template (v{bundled_version})", file=sys.stderr)
|
||||
return (str(bundled_path), False)
|
||||
|
||||
raise FileNotFoundError(f"No template found (GitHub failed, bundled not at {bundled_path})")
|
||||
|
||||
def extract_section(content: str, header: str, next_headers: List[str]) -> Optional[str]:
|
||||
"""Extract content between header and next section header"""
|
||||
# Find header (supports ## or # with various markdown formats)
|
||||
header_pattern = r'^#{1,2}\s+' + re.escape(header) + r'.*?$'
|
||||
match = re.search(header_pattern, content, re.MULTILINE | re.IGNORECASE)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
start = match.end()
|
||||
|
||||
# Find next header
|
||||
next_pattern = r'^#{1,2}\s+(' + '|'.join(re.escape(h) for h in next_headers) + r').*?$'
|
||||
next_match = re.search(next_pattern, content[start:], re.MULTILINE | re.IGNORECASE)
|
||||
|
||||
if next_match:
|
||||
end = start + next_match.start()
|
||||
else:
|
||||
end = len(content)
|
||||
|
||||
section = content[start:end].strip()
|
||||
return section if section else None
|
||||
|
||||
def extract_customizations(claude_md_path: str) -> Dict:
|
||||
"""Extract project-specific customizations from CLAUDE.md"""
|
||||
|
||||
with open(claude_md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
customizations = {
|
||||
"project_name": "",
|
||||
"description": "",
|
||||
"tech_stack": [],
|
||||
"code_standards": [],
|
||||
"forbidden_actions": [],
|
||||
"pm_tool": "none",
|
||||
"custom_sections": {}
|
||||
}
|
||||
|
||||
# Extract project name (first # header)
|
||||
title_match = re.search(r'^#\s+(.+?)\s*-\s*Claude Code Configuration', content, re.MULTILINE)
|
||||
if title_match:
|
||||
customizations["project_name"] = title_match.group(1).strip()
|
||||
|
||||
# Extract description from Context section
|
||||
context = extract_section(content, "Context", [
|
||||
"Navigator Quick Start", "Quick Start", "Project-Specific", "Code Standards",
|
||||
"Forbidden Actions", "Documentation", "Project Management"
|
||||
])
|
||||
|
||||
if context:
|
||||
# Extract brief description (text before tech stack, excluding brackets)
|
||||
lines = context.split('\n')
|
||||
desc_lines = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('**Tech Stack') and not line.startswith('['):
|
||||
desc_lines.append(line)
|
||||
if line.startswith('**Tech Stack'):
|
||||
break
|
||||
if desc_lines:
|
||||
customizations["description"] = ' '.join(desc_lines)
|
||||
|
||||
# Extract tech stack
|
||||
tech_match = re.search(r'\*\*Tech Stack\*\*:\s*(.+?)(?:\n|$)', context)
|
||||
if tech_match:
|
||||
tech_text = tech_match.group(1).strip()
|
||||
# Remove brackets and split by comma
|
||||
tech_text = re.sub(r'\[|\]', '', tech_text)
|
||||
customizations["tech_stack"] = [t.strip() for t in tech_text.split(',')]
|
||||
|
||||
# Extract code standards
|
||||
standards_section = extract_section(content, "Project-Specific Code Standards", [
|
||||
"Forbidden", "Documentation", "Project Management", "Configuration",
|
||||
"Commit Guidelines", "Success Metrics"
|
||||
])
|
||||
|
||||
if not standards_section:
|
||||
standards_section = extract_section(content, "Code Standards", [
|
||||
"Forbidden", "Documentation", "Project Management", "Configuration"
|
||||
])
|
||||
|
||||
if standards_section:
|
||||
# Extract custom rules (lines that aren't in default template)
|
||||
default_rules = [
|
||||
"KISS, DRY, SOLID",
|
||||
"TypeScript",
|
||||
"Strict mode",
|
||||
"Line Length",
|
||||
"Max 100",
|
||||
"Testing",
|
||||
"Framework-Specific",
|
||||
"General Standards",
|
||||
"Architecture"
|
||||
]
|
||||
|
||||
lines = standards_section.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
# Skip empty lines, headers, and default rules
|
||||
if not line or line.startswith('#') or line.startswith('**'):
|
||||
continue
|
||||
# Check if it's a custom rule
|
||||
is_default = any(rule in line for rule in default_rules)
|
||||
if not is_default:
|
||||
if line.startswith('-') or line.startswith('*'):
|
||||
customizations["code_standards"].append(line.lstrip('-*').strip())
|
||||
elif ':' in line: # Format like "Custom rule: Always use hooks"
|
||||
customizations["code_standards"].append(line)
|
||||
|
||||
# Extract forbidden actions
|
||||
forbidden_section = extract_section(content, "Forbidden Actions", [
|
||||
"Documentation", "Project Management", "Configuration",
|
||||
"Commit Guidelines", "Success Metrics"
|
||||
])
|
||||
|
||||
if forbidden_section:
|
||||
# Extract custom forbidden actions (not in default template)
|
||||
default_forbidden = [
|
||||
"NEVER wait for explicit commit",
|
||||
"NEVER leave tickets open",
|
||||
"NEVER skip documentation",
|
||||
"NEVER load all `.agent/`",
|
||||
"NEVER load all .agent",
|
||||
"NEVER skip reading DEVELOPMENT-README",
|
||||
"No Claude Code mentions",
|
||||
"No package.json modifications",
|
||||
"Never commit secrets",
|
||||
"Don't delete tests",
|
||||
"NEVER skip tests"
|
||||
]
|
||||
|
||||
lines = forbidden_section.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
# Skip empty lines and headers
|
||||
if not line or line.startswith('#') or line.startswith('**'):
|
||||
continue
|
||||
if line.startswith('❌') or line.startswith('-'):
|
||||
action = line.lstrip('❌- ').strip()
|
||||
# Check if it's truly custom
|
||||
is_default = any(df in action for df in default_forbidden)
|
||||
if action and not is_default:
|
||||
# Remove any leading emoji that might remain
|
||||
action = action.lstrip('❌ ')
|
||||
customizations["forbidden_actions"].append(action)
|
||||
|
||||
# Extract PM tool configuration
|
||||
pm_section = extract_section(content, "Project Management", [
|
||||
"Configuration", "Commit Guidelines", "Success Metrics"
|
||||
])
|
||||
|
||||
if pm_section:
|
||||
# Look for configured tool
|
||||
tool_match = re.search(r'\*\*Configured Tool\*\*:\s*(\w+)', pm_section, re.IGNORECASE)
|
||||
if tool_match:
|
||||
tool = tool_match.group(1).lower()
|
||||
if tool in ['linear', 'github', 'jira', 'gitlab']:
|
||||
customizations["pm_tool"] = tool
|
||||
|
||||
# Extract custom sections (not in standard template)
|
||||
standard_sections = [
|
||||
"Context", "Navigator", "Quick Start", "Code Standards",
|
||||
"Project-Specific Code Standards", "Forbidden Actions",
|
||||
"Documentation Structure", "Project Management",
|
||||
"Configuration", "Commit Guidelines", "Success Metrics"
|
||||
]
|
||||
|
||||
# Find all ## headers
|
||||
headers = re.findall(r'^##\s+(.+?)$', content, re.MULTILINE)
|
||||
for header in headers:
|
||||
if header.strip() not in standard_sections:
|
||||
section_content = extract_section(content, header, standard_sections + headers)
|
||||
if section_content:
|
||||
customizations["custom_sections"][header.strip()] = section_content
|
||||
|
||||
return customizations
|
||||
|
||||
def generate_updated_claude_md(customizations: Dict, template_path: str, output_path: str):
|
||||
"""Generate updated CLAUDE.md using v3.1 template and customizations"""
|
||||
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
template = f.read()
|
||||
|
||||
# Replace project name
|
||||
if customizations["project_name"]:
|
||||
template = template.replace('[Project Name]', customizations["project_name"])
|
||||
|
||||
# Replace description
|
||||
if customizations["description"]:
|
||||
template = template.replace(
|
||||
'[Brief project description - explain what this project does]',
|
||||
customizations["description"]
|
||||
)
|
||||
|
||||
# Replace tech stack
|
||||
if customizations["tech_stack"]:
|
||||
tech_stack = ', '.join(customizations["tech_stack"])
|
||||
template = template.replace(
|
||||
'[List your technologies, e.g., Next.js, TypeScript, PostgreSQL]',
|
||||
tech_stack
|
||||
)
|
||||
|
||||
# Append custom code standards
|
||||
if customizations["code_standards"]:
|
||||
standards_marker = "[Add project-specific violations here]"
|
||||
if standards_marker in template:
|
||||
custom_standards = "\n\n### Additional Project Standards\n\n"
|
||||
for standard in customizations["code_standards"]:
|
||||
custom_standards += f"- {standard}\n"
|
||||
template = template.replace(standards_marker, custom_standards + "\n" + standards_marker)
|
||||
|
||||
# Append custom forbidden actions
|
||||
if customizations["forbidden_actions"]:
|
||||
forbidden_marker = "[Add project-specific violations here]"
|
||||
if forbidden_marker in template:
|
||||
custom_forbidden = "\n### Additional Forbidden Actions\n\n"
|
||||
for action in customizations["forbidden_actions"]:
|
||||
custom_forbidden += f"- ❌ {action}\n"
|
||||
# Find the marker and append after it
|
||||
template = template.replace(forbidden_marker, custom_forbidden)
|
||||
|
||||
# Update PM tool
|
||||
if customizations["pm_tool"] != "none":
|
||||
template = template.replace(
|
||||
'**Configured Tool**: [Linear / GitHub Issues / Jira / GitLab / None]',
|
||||
f'**Configured Tool**: {customizations["pm_tool"].title()}'
|
||||
)
|
||||
# Update config JSON
|
||||
template = template.replace(
|
||||
'"project_management": "none"',
|
||||
f'"project_management": "{customizations["pm_tool"]}"'
|
||||
)
|
||||
|
||||
# Append custom sections at the end
|
||||
if customizations["custom_sections"]:
|
||||
template += "\n\n---\n\n## Custom Project Sections\n\n"
|
||||
for section_name, section_content in customizations["custom_sections"].items():
|
||||
template += f"### {section_name}\n\n{section_content}\n\n"
|
||||
|
||||
# Write updated file
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(template)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage:", file=sys.stderr)
|
||||
print(" Extract: python3 claude_updater.py extract CLAUDE.md > customizations.json", file=sys.stderr)
|
||||
print(" Generate: python3 claude_updater.py generate --customizations file.json --template template.md --output CLAUDE.md", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "extract":
|
||||
claude_md_path = sys.argv[2]
|
||||
|
||||
if not Path(claude_md_path).exists():
|
||||
print(f"Error: File not found: {claude_md_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
customizations = extract_customizations(claude_md_path)
|
||||
print(json.dumps(customizations, indent=2))
|
||||
except Exception as e:
|
||||
print(f"Error extracting customizations: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
elif command == "generate":
|
||||
# Parse arguments
|
||||
args = {
|
||||
'customizations': None,
|
||||
'template': None,
|
||||
'output': None
|
||||
}
|
||||
|
||||
i = 2
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--customizations' and i + 1 < len(sys.argv):
|
||||
args['customizations'] = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == '--template' and i + 1 < len(sys.argv):
|
||||
args['template'] = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == '--output' and i + 1 < len(sys.argv):
|
||||
args['output'] = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not all(args.values()):
|
||||
print("Error: Missing required arguments", file=sys.stderr)
|
||||
print("Required: --customizations, --template, --output", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(args['customizations'], 'r') as f:
|
||||
customizations = json.load(f)
|
||||
|
||||
# Use get_template_path for GitHub fetch with bundled fallback
|
||||
# If --template is a directory, treat it as bundled_template_dir
|
||||
# Otherwise, use it directly as a file path
|
||||
template_arg = args['template']
|
||||
|
||||
if Path(template_arg).is_dir():
|
||||
# Directory provided - use get_template_path for smart fetching
|
||||
template_path, is_github = get_template_path(template_arg)
|
||||
elif Path(template_arg).is_file():
|
||||
# File provided directly - use as-is (backward compatibility)
|
||||
template_path = template_arg
|
||||
is_github = False
|
||||
else:
|
||||
# Try parent directory for get_template_path
|
||||
template_dir = str(Path(template_arg).parent)
|
||||
template_path, is_github = get_template_path(template_dir)
|
||||
|
||||
generate_updated_claude_md(customizations, template_path, args['output'])
|
||||
print(f"✓ Generated {args['output']}", file=sys.stderr)
|
||||
|
||||
# Cleanup temp file if from GitHub
|
||||
if is_github and Path(template_path).exists():
|
||||
Path(template_path).unlink()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating CLAUDE.md: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
else:
|
||||
print(f"Error: Unknown command: {command}", file=sys.stderr)
|
||||
print("Valid commands: extract, generate", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
169
skills/nav-update-claude/functions/version_detector.py
Executable file
169
skills/nav-update-claude/functions/version_detector.py
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Navigator CLAUDE.md Version Detector
|
||||
Detects if CLAUDE.md is outdated, current (v3.1), or unknown
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
VersionStatus = Literal["outdated", "current", "unknown"]
|
||||
|
||||
def detect_version(claude_md_path: str) -> VersionStatus:
|
||||
"""Detect CLAUDE.md version status"""
|
||||
|
||||
if not Path(claude_md_path).exists():
|
||||
print(f"Error: File not found: {claude_md_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(claude_md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for version marker
|
||||
version_match = re.search(r'Navigator Version[:\s]+(\d+\.\d+\.\d+)', content, re.IGNORECASE)
|
||||
|
||||
if version_match:
|
||||
version_str = version_match.group(1)
|
||||
major, minor, patch = map(int, version_str.split('.'))
|
||||
|
||||
# Version 3.1+ is current
|
||||
if major > 3 or (major == 3 and minor >= 1):
|
||||
# Double-check for natural language (should have it in v3+)
|
||||
if has_natural_language_examples(content):
|
||||
return "current"
|
||||
else:
|
||||
# Has v3.1 marker but no natural language - partial migration
|
||||
return "outdated"
|
||||
|
||||
# Version 3.0 - check for natural language
|
||||
elif major == 3 and minor == 0:
|
||||
if has_natural_language_examples(content) and not has_slash_commands(content):
|
||||
return "current"
|
||||
else:
|
||||
return "outdated"
|
||||
|
||||
# Version < 3.0 is definitely outdated
|
||||
else:
|
||||
return "outdated"
|
||||
|
||||
# No version marker - use heuristics
|
||||
return detect_by_heuristics(content)
|
||||
|
||||
def has_slash_commands(content: str) -> bool:
|
||||
"""Check if content has slash command references"""
|
||||
slash_patterns = [
|
||||
r'/nav:start',
|
||||
r'/nav:init',
|
||||
r'/nav:doc',
|
||||
r'/nav:marker',
|
||||
r'/nav:markers',
|
||||
r'/nav:compact',
|
||||
r'/jitd:',
|
||||
]
|
||||
|
||||
for pattern in slash_patterns:
|
||||
if re.search(pattern, content):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_natural_language_examples(content: str) -> bool:
|
||||
"""Check if content has natural language command examples"""
|
||||
natural_language_patterns = [
|
||||
r'"Start my Navigator session"',
|
||||
r'"Initialize Navigator in this project"',
|
||||
r'"Archive TASK-\w+ documentation"',
|
||||
r'"Create an SOP for',
|
||||
r'"Clear context and preserve markers"',
|
||||
r'"Start my session"',
|
||||
r'"Load the navigator"',
|
||||
]
|
||||
|
||||
matches = 0
|
||||
for pattern in natural_language_patterns:
|
||||
if re.search(pattern, content, re.IGNORECASE):
|
||||
matches += 1
|
||||
|
||||
# Need at least 2 natural language examples to be considered current
|
||||
return matches >= 2
|
||||
|
||||
def has_skills_explanation(content: str) -> bool:
|
||||
"""Check if content explains skills architecture"""
|
||||
skills_markers = [
|
||||
r'skills-only architecture',
|
||||
r'skills that auto-invoke',
|
||||
r'How Claude Discovers.*Skills',
|
||||
r'Progressive disclosure.*skills',
|
||||
]
|
||||
|
||||
for pattern in skills_markers:
|
||||
if re.search(pattern, content, re.IGNORECASE | re.DOTALL):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_navigator_markers(content: str) -> bool:
|
||||
"""Check if content has any Navigator-specific markers"""
|
||||
navigator_markers = [
|
||||
r'Navigator',
|
||||
r'\.agent/',
|
||||
r'DEVELOPMENT-README\.md',
|
||||
r'nav-start',
|
||||
r'nav-task',
|
||||
r'nav-compact',
|
||||
r'context markers',
|
||||
r'token optimization',
|
||||
]
|
||||
|
||||
matches = 0
|
||||
for pattern in navigator_markers:
|
||||
if re.search(pattern, content, re.IGNORECASE):
|
||||
matches += 1
|
||||
|
||||
# Need at least 3 Navigator markers to be considered Navigator-related
|
||||
return matches >= 3
|
||||
|
||||
def detect_by_heuristics(content: str) -> VersionStatus:
|
||||
"""Detect version using heuristics when no version marker present"""
|
||||
|
||||
# Check if it's Navigator-related at all
|
||||
if not has_navigator_markers(content):
|
||||
return "unknown"
|
||||
|
||||
# Has slash commands → definitely outdated
|
||||
if has_slash_commands(content):
|
||||
return "outdated"
|
||||
|
||||
# Has natural language + skills explanation → current
|
||||
if has_natural_language_examples(content) and has_skills_explanation(content):
|
||||
return "current"
|
||||
|
||||
# Has natural language but no skills explanation → partial migration
|
||||
if has_natural_language_examples(content):
|
||||
return "outdated"
|
||||
|
||||
# Has Navigator markers but no natural language → old version
|
||||
if has_navigator_markers(content):
|
||||
return "outdated"
|
||||
|
||||
# Can't determine
|
||||
return "unknown"
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 version_detector.py CLAUDE.md", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
claude_md_path = sys.argv[1]
|
||||
|
||||
try:
|
||||
status = detect_version(claude_md_path)
|
||||
print(status)
|
||||
except Exception as e:
|
||||
print(f"Error detecting version: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
362
skills/nav-update-claude/skill.md
Normal file
362
skills/nav-update-claude/skill.md
Normal file
@@ -0,0 +1,362 @@
|
||||
---
|
||||
name: nav-update-claude
|
||||
description: Update project CLAUDE.md to latest Navigator version, preserving customizations. Use when user says "update CLAUDE.md", "migrate to v3", or when detecting outdated Navigator configuration.
|
||||
allowed-tools: Read, Write, Edit, Bash
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Navigator CLAUDE.md Updater Skill
|
||||
|
||||
Update project's CLAUDE.md to latest Navigator version (v3.1) while preserving project-specific customizations.
|
||||
|
||||
## When to Invoke
|
||||
|
||||
Invoke this skill when the user:
|
||||
- Says "update my CLAUDE.md", "migrate CLAUDE.md to v3"
|
||||
- Says "update Navigator configuration", "fix my CLAUDE.md"
|
||||
- Mentions outdated commands like "/nav:start" and wants to upgrade
|
||||
- Complains that Claude doesn't understand Navigator workflow
|
||||
|
||||
**DO NOT invoke** if:
|
||||
- CLAUDE.md already references v3.1 and natural language commands
|
||||
- User is editing CLAUDE.md for project-specific reasons (not Navigator updates)
|
||||
- Working on plugin's root CLAUDE.md (not user projects)
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Detect Current CLAUDE.md Version
|
||||
|
||||
Check if CLAUDE.md exists and detect version:
|
||||
|
||||
```bash
|
||||
if [ ! -f "CLAUDE.md" ]; then
|
||||
echo "❌ No CLAUDE.md found in current directory"
|
||||
echo ""
|
||||
echo "Run 'Initialize Navigator in this project' first."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
Use `version_detector.py` to analyze CLAUDE.md:
|
||||
|
||||
```bash
|
||||
python3 "$SKILL_BASE_DIR/functions/version_detector.py" CLAUDE.md
|
||||
```
|
||||
|
||||
This script checks for:
|
||||
- Version markers (e.g., "Navigator Version: 3.1.0")
|
||||
- Slash command references (`/nav:start`, `/nav:doc`, etc.)
|
||||
- Skills vs commands language
|
||||
- Natural language examples
|
||||
|
||||
**Outputs**:
|
||||
- `outdated` - Has `/nav:` commands or v1/v2 markers
|
||||
- `current` - Already v3.1 with natural language
|
||||
- `unknown` - Can't determine (custom/non-Navigator file)
|
||||
|
||||
**If `current`**:
|
||||
```
|
||||
✅ CLAUDE.md is already up to date (v3.1)
|
||||
|
||||
No migration needed.
|
||||
```
|
||||
Exit successfully.
|
||||
|
||||
**If `unknown`**:
|
||||
```
|
||||
⚠️ CLAUDE.md doesn't appear to be a Navigator file
|
||||
|
||||
This might be a custom configuration. Manual review recommended.
|
||||
Proceed with migration anyway? [y/N]
|
||||
```
|
||||
|
||||
If user declines, exit. If accepts, continue.
|
||||
|
||||
### Step 2: Backup Current CLAUDE.md
|
||||
|
||||
Always create backup before modifying:
|
||||
|
||||
```bash
|
||||
cp CLAUDE.md CLAUDE.md.backup
|
||||
echo "📦 Backup created: CLAUDE.md.backup"
|
||||
```
|
||||
|
||||
### Step 3: Extract Project-Specific Customizations
|
||||
|
||||
Use `claude_updater.py` to parse current CLAUDE.md:
|
||||
|
||||
```bash
|
||||
python3 "$SKILL_BASE_DIR/functions/claude_updater.py" extract CLAUDE.md > /tmp/nav-customizations.json
|
||||
```
|
||||
|
||||
This extracts:
|
||||
- **Project name** (from title)
|
||||
- **Project description** (from Context section)
|
||||
- **Tech stack** (languages, frameworks)
|
||||
- **Code standards** (custom rules beyond Navigator defaults)
|
||||
- **Forbidden actions** (project-specific restrictions)
|
||||
- **PM tool configuration** (Linear, GitHub, Jira, etc.)
|
||||
- **Custom sections** (anything not in Navigator template)
|
||||
|
||||
### Step 4: Generate Updated CLAUDE.md
|
||||
|
||||
Apply latest template with extracted customizations:
|
||||
|
||||
```bash
|
||||
# Template fetching now automatic via get_template_path():
|
||||
# 1. Tries GitHub (version-matched)
|
||||
# 2. Falls back to bundled if offline
|
||||
python3 "$SKILL_BASE_DIR/functions/claude_updater.py" generate \
|
||||
--customizations /tmp/nav-customizations.json \
|
||||
--template "$SKILL_BASE_DIR/../../templates/CLAUDE.md" \
|
||||
--output CLAUDE.md
|
||||
```
|
||||
|
||||
**Template Source Priority**:
|
||||
1. **GitHub** (version-matched): Fetches from `https://raw.githubusercontent.com/alekspetrov/navigator/v{version}/templates/CLAUDE.md`
|
||||
- Matches installed plugin version (e.g., v4.3.0)
|
||||
- Always up-to-date with release
|
||||
- Works with pre-releases
|
||||
2. **Bundled** (fallback): Uses `templates/CLAUDE.md` from installed plugin
|
||||
- Offline fallback
|
||||
- Guaranteed availability
|
||||
|
||||
**What this does**:
|
||||
1. Loads template (GitHub or bundled)
|
||||
2. Replaces placeholders with extracted data
|
||||
3. Preserves custom sections
|
||||
4. Updates Navigator workflow to natural language
|
||||
5. Removes slash command references
|
||||
6. Adds skills explanation
|
||||
|
||||
### Step 5: Show Diff and Confirm
|
||||
|
||||
Display changes for user review:
|
||||
|
||||
```bash
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📝 CHANGES TO CLAUDE.MD"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Show unified diff
|
||||
diff -u CLAUDE.md.backup CLAUDE.md || true
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
```
|
||||
|
||||
### Step 6: Verify and Commit
|
||||
|
||||
Show summary of changes:
|
||||
|
||||
```
|
||||
✅ CLAUDE.md Updated to v3.1
|
||||
|
||||
Key changes:
|
||||
✓ Removed slash command references (e.g., /nav:start)
|
||||
✓ Added natural language examples ("Start my Navigator session")
|
||||
✓ Added skills architecture explanation
|
||||
✓ Updated Navigator workflow section
|
||||
✓ Preserved your project-specific customizations:
|
||||
- Tech stack: [list]
|
||||
- Code standards: [count] custom rules
|
||||
- Forbidden actions: [count] custom rules
|
||||
|
||||
Backup saved: CLAUDE.md.backup
|
||||
|
||||
Next steps:
|
||||
1. Review changes: git diff CLAUDE.md
|
||||
2. Test: "Start my Navigator session" should work
|
||||
3. Commit: git add CLAUDE.md && git commit -m "chore: update CLAUDE.md to Navigator v3.1"
|
||||
4. Remove backup: rm CLAUDE.md.backup
|
||||
|
||||
Rollback if needed: mv CLAUDE.md.backup CLAUDE.md
|
||||
```
|
||||
|
||||
### Step 7: Optional - Update .nav-config.json
|
||||
|
||||
If config exists, check version:
|
||||
|
||||
```bash
|
||||
if [ -f ".agent/.nav-config.json" ]; then
|
||||
version=$(jq -r '.version' .agent/.nav-config.json)
|
||||
if [ "$version" != "4.5.0" ]; then
|
||||
echo ""
|
||||
echo "💡 .nav-config.json is version $version"
|
||||
echo " Update to 4.5.0? [Y/n]"
|
||||
read -r response
|
||||
|
||||
if [[ "$response" =~ ^([yY][eE][sS]|[yY]|)$ ]]; then
|
||||
jq '.version = "4.5.0"' .agent/.nav-config.json > /tmp/nav-config.tmp
|
||||
mv /tmp/nav-config.tmp .agent/.nav-config.json
|
||||
echo " ✓ Updated config to v4.5.0"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
## Predefined Functions
|
||||
|
||||
### functions/version_detector.py
|
||||
|
||||
**Purpose**: Detect CLAUDE.md version (outdated, current, unknown)
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python3 version_detector.py CLAUDE.md
|
||||
```
|
||||
|
||||
**Output**: Prints one of: `outdated`, `current`, `unknown`
|
||||
|
||||
**Exit codes**:
|
||||
- 0: Success (version detected)
|
||||
- 1: File not found
|
||||
- 2: Parse error
|
||||
|
||||
**Detection logic**:
|
||||
1. Check for version marker: `Navigator Version: X.X.X`
|
||||
2. Check for slash commands: `/nav:start`, `/jitd:`, etc.
|
||||
3. Check for natural language examples: `"Start my Navigator session"`
|
||||
4. Check for skills section
|
||||
|
||||
**Heuristics**:
|
||||
- Has `/nav:` → outdated
|
||||
- Version < 3.0 → outdated
|
||||
- Version >= 3.0 + natural language → current
|
||||
- No version + no Navigator markers → unknown
|
||||
|
||||
### functions/claude_updater.py
|
||||
|
||||
**Purpose**: Extract customizations and generate updated CLAUDE.md
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
# Extract customizations
|
||||
python3 claude_updater.py extract CLAUDE.md > customizations.json
|
||||
|
||||
# Generate updated file
|
||||
python3 claude_updater.py generate \
|
||||
--customizations customizations.json \
|
||||
--template ../../templates/CLAUDE.md \
|
||||
--output CLAUDE.md
|
||||
```
|
||||
|
||||
**Extract mode** outputs JSON:
|
||||
```json
|
||||
{
|
||||
"project_name": "MyApp",
|
||||
"description": "Brief project description",
|
||||
"tech_stack": ["Next.js", "TypeScript", "PostgreSQL"],
|
||||
"code_standards": ["Custom rule 1", "Custom rule 2"],
|
||||
"forbidden_actions": ["Custom restriction 1"],
|
||||
"pm_tool": "github",
|
||||
"custom_sections": {
|
||||
"Deployment": "Custom deployment instructions..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Generate mode**:
|
||||
1. Loads template
|
||||
2. Replaces `[Project Name]` with `project_name`
|
||||
3. Replaces `[Brief project description]` with `description`
|
||||
4. Replaces `[List your technologies...]` with `tech_stack`
|
||||
5. Appends custom code standards
|
||||
6. Appends custom forbidden actions
|
||||
7. Inserts custom sections at end
|
||||
|
||||
## Error Handling
|
||||
|
||||
**No CLAUDE.md found**:
|
||||
```
|
||||
❌ No CLAUDE.md found in current directory
|
||||
|
||||
This project doesn't appear to have Navigator initialized.
|
||||
Run "Initialize Navigator in this project" first.
|
||||
```
|
||||
|
||||
**Backup failed**:
|
||||
```
|
||||
❌ Failed to create backup: CLAUDE.md.backup
|
||||
|
||||
Check file permissions and disk space.
|
||||
```
|
||||
|
||||
**Parse error**:
|
||||
```
|
||||
❌ Failed to parse CLAUDE.md
|
||||
|
||||
The file might be corrupted or have unusual formatting.
|
||||
Manual review required.
|
||||
|
||||
Backup saved at: CLAUDE.md.backup
|
||||
```
|
||||
|
||||
**Template not found**:
|
||||
```
|
||||
❌ Navigator template not found
|
||||
|
||||
This might be a plugin installation issue.
|
||||
Try reinstalling Navigator plugin: /plugin update navigator
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Migration is successful when:
|
||||
- [ ] CLAUDE.md backed up successfully
|
||||
- [ ] Version detected correctly
|
||||
- [ ] Customizations extracted
|
||||
- [ ] New file generated with v3.1 template
|
||||
- [ ] Project-specific content preserved
|
||||
- [ ] Diff shown to user for review
|
||||
- [ ] Commit instructions provided
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
If migration fails or user is unhappy:
|
||||
|
||||
```bash
|
||||
# Restore backup
|
||||
mv CLAUDE.md.backup CLAUDE.md
|
||||
|
||||
# Or compare and manually fix
|
||||
diff CLAUDE.md.backup CLAUDE.md
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
This skill:
|
||||
- **Preserves all customizations** (tech stack, standards, restrictions)
|
||||
- **Non-destructive** (always creates backup)
|
||||
- **Idempotent** (running multiple times is safe)
|
||||
- **Transparent** (shows diff before finalizing)
|
||||
|
||||
**What gets updated**:
|
||||
- Navigator version marker
|
||||
- Slash commands → natural language
|
||||
- Workflow examples
|
||||
- Skills vs commands explanation
|
||||
- Token optimization strategy
|
||||
|
||||
**What gets preserved**:
|
||||
- Project name and description
|
||||
- Tech stack
|
||||
- Code standards
|
||||
- Forbidden actions
|
||||
- PM tool configuration
|
||||
- Custom sections
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **nav-init**: Initialize Navigator in new project (creates CLAUDE.md from scratch)
|
||||
- **nav-start**: Start session (uses updated CLAUDE.md)
|
||||
- **nav-task**: Task documentation (benefits from updated workflow)
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Update
|
||||
|
||||
```
|
||||
User: "Update my CLAUDE.md to v3.1"
|
||||
Reference in New Issue
Block a user