Skip to content

Commit eb307b1

Browse files
James Zhuclaude
andcommitted
refactor: Remove CAM metadata markers from live prompt files
- Remove embedded <!-- cam-prompt-id: xyz --> markers from live prompt files - Implement content-based sync tracking instead of marker-based tracking - Add get_matching_prompt_id() and _normalize_content_for_comparison() methods - Update status command to use content comparison for installation detection - Clean up live prompt files (CLAUDE.md, copilot-instructions.md) by removing markers - Update tests to expect clean live files without markers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a3a31db commit eb307b1

File tree

5 files changed

+136
-23
lines changed

5 files changed

+136
-23
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,4 @@ This is a test instruction file for GitHub Copilot CLI custom instructions suppo
66

77
- Write clear, maintainable code
88
- Test everything before committing
9-
- Follow the repository's coding style
10-
11-
<!-- cam-prompt-id: 02ee63f9 -->
9+
- Follow the repository's coding style

CLAUDE.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,4 @@ rm -rf dist/*
1313
./install.sh uninstall
1414
./install.sh
1515
cp ~/.config/code-assistant-manager/providers.json.bak ~/.config/code-assistant-manager/providers.json
16-
```
17-
18-
<!-- cam-prompt-id: 71b21925 -->
16+
```

code_assistant_manager/cli/prompts_commands.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,47 @@ def _find_prompt_by_name(manager: PromptManager, name: str) -> Optional[Prompt]:
3030
return None
3131

3232

33+
def _strip_metadata_from_content(content: str) -> str:
34+
"""
35+
Strip metadata headers from content that shouldn't be part of the prompt.
36+
37+
This includes:
38+
- YAML front matter (--- ... ---)
39+
- Existing metadata headers from show command output
40+
- CAM prompt ID markers
41+
"""
42+
import re
43+
44+
# First strip CAM prompt ID markers
45+
from code_assistant_manager.prompts.base import PROMPT_ID_PATTERN
46+
content = PROMPT_ID_PATTERN.sub("", content).strip()
47+
48+
# Strip YAML front matter
49+
yaml_pattern = r'^---\s*\n.*?\n---\s*\n'
50+
content = re.sub(yaml_pattern, '', content, flags=re.DOTALL).strip()
51+
52+
# Strip metadata headers that look like the show command output
53+
# Look for patterns like "Prompt: ..." or "ID: ..." at the beginning
54+
lines = content.splitlines()
55+
56+
# Find where actual content starts (after metadata headers)
57+
content_start_idx = 0
58+
for i, line in enumerate(lines):
59+
# Stop at the first line that doesn't look like metadata
60+
if not re.match(r'^(Prompt|ID|Description|Status|Default|Content|Imported from):\s*', line, re.IGNORECASE):
61+
# Also skip empty lines at the beginning
62+
if line.strip():
63+
content_start_idx = i
64+
break
65+
content_start_idx = i + 1
66+
67+
# If we found metadata headers, return content starting after them
68+
if content_start_idx > 0 and content_start_idx < len(lines):
69+
return '\n'.join(lines[content_start_idx:]).strip()
70+
71+
return content.strip()
72+
73+
3374
def _generate_fancy_name() -> str:
3475
"""Generate a fancy, creative name for a prompt."""
3576
adjectives = [
@@ -133,6 +174,8 @@ def add_prompt(
133174
typer.echo(f"Error: File not found: {file}")
134175
raise typer.Exit(1)
135176
content = file.read_text()
177+
# Strip metadata headers that shouldn't be part of the prompt content
178+
content = _strip_metadata_from_content(content)
136179
elif not sys.stdin.isatty():
137180
# Read from stdin (piped input)
138181
content = sys.stdin.read()
@@ -222,6 +265,9 @@ def update_prompt(
222265
typer.echo("Error: File is empty")
223266
raise typer.Exit(1)
224267

268+
# Strip metadata headers that shouldn't be part of the prompt content
269+
content = _strip_metadata_from_content(content)
270+
225271
# Check if new name conflicts with existing prompt
226272
if new_name and new_name != name:
227273
existing_prompt = _find_prompt_by_name(manager, new_name)
@@ -332,6 +378,9 @@ def import_prompt(
332378
from code_assistant_manager.prompts.base import PROMPT_ID_PATTERN
333379
content = PROMPT_ID_PATTERN.sub("", content).strip()
334380

381+
# Strip metadata headers that shouldn't be part of the prompt content
382+
content = _strip_metadata_from_content(content)
383+
335384
# Generate or validate name
336385
if not name:
337386
# Generate a fancy name with app context and ensure it's unique
@@ -467,27 +516,32 @@ def status(
467516
# Build installation map: prompt_id -> [(app, level, file_path), ...]
468517
install_map = {} # prompt_id -> list of (app, level, file_path) tuples
469518
untracked = [] # list of (app, level, preview) for untracked installs
470-
519+
471520
for app in VALID_APP_TYPES:
472521
handler = manager.get_handler(app)
473-
522+
474523
for level in ["user", "project"]:
475524
proj = project_dir if level == "project" else None
476525
file_path = handler.get_prompt_file_path(level, proj)
477-
526+
478527
if not file_path or not file_path.exists():
479528
continue
480-
529+
481530
content = file_path.read_text().strip()
482531
if not content:
483532
continue
484-
485-
installed_id = handler.get_installed_prompt_id(level, proj)
486-
487-
if installed_id:
488-
if installed_id not in install_map:
489-
install_map[installed_id] = []
490-
install_map[installed_id].append((app, level, file_path))
533+
534+
# Check if any configured prompt matches this content
535+
matched_prompt_id = None
536+
for prompt_id, prompt in prompts.items():
537+
if handler.get_matching_prompt_id(prompt.content, level, proj):
538+
matched_prompt_id = prompt_id
539+
break
540+
541+
if matched_prompt_id:
542+
if matched_prompt_id not in install_map:
543+
install_map[matched_prompt_id] = []
544+
install_map[matched_prompt_id].append((app, level, file_path))
491545
else:
492546
preview = content[:30].replace('\n', ' ')
493547
if len(content) > 30:

code_assistant_manager/prompts/base.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,8 @@ def sync_prompt(
224224
# Normalize header to match this tool's name
225225
content = self._normalize_header(content, filename=file_path.name)
226226

227-
# Embed prompt ID marker at the end if provided
228-
if prompt_id:
229-
content = content.rstrip() + "\n\n" + PROMPT_ID_MARKER.format(prompt_id) + "\n"
227+
# NOTE: We no longer embed prompt ID markers in live files
228+
# Sync status is tracked by content comparison instead
230229

231230
# Ensure parent directory exists
232231
file_path.parent.mkdir(parents=True, exist_ok=True)
@@ -251,6 +250,9 @@ def get_installed_prompt_id(
251250
"""
252251
Extract the prompt ID from an installed prompt file.
253252
253+
NOTE: This method is deprecated. We no longer embed prompt ID markers
254+
in live files. Use get_matching_prompt_id() for content-based matching.
255+
254256
Args:
255257
level: Prompt level ("user" or "project")
256258
project_dir: Optional project directory for project level prompts
@@ -265,6 +267,65 @@ def get_installed_prompt_id(
265267
match = PROMPT_ID_PATTERN.search(content)
266268
return match.group(1) if match else None
267269

270+
def get_matching_prompt_id(
271+
self,
272+
expected_content: str,
273+
level: str = "user",
274+
project_dir: Optional[Path] = None,
275+
) -> Optional[str]:
276+
"""
277+
Check if the live content matches any configured prompt.
278+
279+
This method compares content to determine sync status without
280+
requiring embedded markers in the live files.
281+
282+
Args:
283+
expected_content: The expected prompt content to match against
284+
level: Prompt level ("user" or "project")
285+
project_dir: Optional project directory for project level prompts
286+
287+
Returns:
288+
The prompt ID if content matches a configured prompt, None otherwise
289+
"""
290+
from code_assistant_manager.prompts.manager import PromptManager
291+
292+
live_content = self.get_live_content(level, project_dir)
293+
if not live_content:
294+
return None
295+
296+
# Strip markers and normalize both contents for comparison
297+
normalized_live = self._normalize_content_for_comparison(live_content)
298+
normalized_expected = self._normalize_content_for_comparison(expected_content)
299+
300+
# If contents match, return a synthetic ID for tracking
301+
# We use content hash as a stable identifier
302+
if normalized_live == normalized_expected:
303+
import hashlib
304+
content_hash = hashlib.md5(normalized_expected.encode('utf-8')).hexdigest()[:8]
305+
return f"content-{content_hash}"
306+
307+
return None
308+
309+
def _normalize_content_for_comparison(self, content: str) -> str:
310+
"""
311+
Normalize content for comparison by stripping markers and standardizing format.
312+
313+
Args:
314+
content: Raw content
315+
316+
Returns:
317+
Normalized content for comparison
318+
"""
319+
# Strip CAM markers
320+
content = PROMPT_ID_PATTERN.sub("", content).strip()
321+
322+
# Strip metadata headers that shouldn't be part of comparison
323+
content = self._strip_metadata_header(content)
324+
325+
# Normalize whitespace
326+
lines = [line.rstrip() for line in content.splitlines()]
327+
return "\n".join(lines).strip()
328+
268329
def get_live_content(
269330
self,
270331
level: str = "user",

tests/unit/test_prompts.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,12 @@ def test_sync_to_app(self, temp_config_dir, temp_prompt_dir):
282282

283283
manager.sync_to_app("test", "claude")
284284

285-
# Check prompt was synced to file (with ID marker)
285+
# Check prompt was synced to file (without ID marker - we use content comparison now)
286286
assert prompt_file.exists()
287287
content = prompt_file.read_text()
288288
assert "My test content" in content
289-
assert "<!-- cam-prompt-id: test -->" in content
289+
# No longer expect CAM prompt ID markers in live files
290+
assert "<!-- cam-prompt-id: test -->" not in content
290291

291292
def test_sync_to_app_project_level(self, temp_config_dir, temp_prompt_dir):
292293
"""Project-level sync writes to project CLAUDE.md."""
@@ -303,7 +304,8 @@ def test_sync_to_app_project_level(self, temp_config_dir, temp_prompt_dir):
303304
assert prompt_file.exists()
304305
content = prompt_file.read_text()
305306
assert "Project scoped content" in content
306-
assert "<!-- cam-prompt-id: test -->" in content
307+
# No longer expect CAM prompt ID markers in live files
308+
assert "<!-- cam-prompt-id: test -->" not in content
307309

308310
def test_get_live_content(self, temp_config_dir, temp_prompt_dir):
309311
"""Test getting live prompt content."""

0 commit comments

Comments
 (0)