File size: 4,259 Bytes
246bf80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""Cursor memory writer — exports to .cursor/rules/*.mdc files.

Cursor's rules system:
- Files in .cursor/rules/ with .mdc extension
- YAML frontmatter with: description, globs (optional), alwaysApply (bool)
- Markdown body with instructions
- Rules with alwaysApply: true are always loaded
- Rules with globs are loaded when matching files are in context
"""

from __future__ import annotations

from collections import defaultdict
from pathlib import Path

from headroom.memory.writers.base import (
    MARKER_END,
    MARKER_PATTERN,
    MARKER_START,
    AgentWriter,
    ExportResult,
    MemoryEntry,
    _estimate_tokens,
)


class CursorMemoryWriter(AgentWriter):
    """Writes memories to Cursor's .cursor/rules/*.mdc format."""

    agent_name = "cursor"
    default_token_budget = 3000

    def format_memories(self, memories: list[MemoryEntry]) -> str:
        """Format as Cursor .mdc content (body only, no frontmatter).

        Frontmatter is handled in export() since it's outside markers.
        """
        lines: list[str] = []

        grouped: dict[str, list[MemoryEntry]] = defaultdict(list)
        for m in memories:
            cat = m.category or "General"
            heading = cat.replace("_", " ").title()
            grouped[heading].append(m)

        for heading, entries in grouped.items():
            lines.append(f"## {heading}")
            lines.append("")
            for entry in entries:
                lines.append(f"- {entry.content}")
            lines.append("")

        return "\n".join(lines)

    def default_path(self) -> Path:
        """Default: .cursor/rules/headroom-memory.mdc in project root."""
        return self._project_path / ".cursor" / "rules" / "headroom-memory.mdc"

    def export(
        self,
        memories: list[MemoryEntry],
        output_path: Path | None = None,
        dry_run: bool = True,
    ) -> ExportResult:
        """Export with Cursor-specific .mdc frontmatter."""
        result = ExportResult(dry_run=dry_run)

        if not memories:
            return result

        # Rank, dedup, budget (reuse base logic)
        ranked = sorted(memories, key=lambda m: m.score, reverse=True)
        seen: set[str] = set()
        unique: list[MemoryEntry] = []
        for m in ranked:
            h = m.content_hash
            if h in seen:
                result.memories_skipped_dedup += 1
                continue
            seen.add(h)
            unique.append(m)

        budgeted: list[MemoryEntry] = []
        tokens_used = 0
        for m in unique:
            entry_tokens = _estimate_tokens(m.content) + 10
            if tokens_used + entry_tokens > self._token_budget:
                result.memories_skipped_budget += 1
                continue
            tokens_used += entry_tokens
            budgeted.append(m)

        if not budgeted:
            return result

        # Build full .mdc file content
        body = self.format_memories(budgeted)

        target = output_path or self.default_path()

        # If file exists and has our markers, only replace marker section
        if target.exists():
            existing = target.read_text()
            if MARKER_START in existing:
                section = f"{MARKER_START}\n{body}\n{MARKER_END}"
                full_content = MARKER_PATTERN.sub(section, existing)
            else:
                # Append our section
                section = f"{MARKER_START}\n{body}\n{MARKER_END}"
                full_content = existing.rstrip() + "\n\n" + section + "\n"
        else:
            # Create new .mdc file with frontmatter
            full_content = (
                "---\n"
                "description: Headroom-learned patterns from proxy traffic\n"
                "alwaysApply: true\n"
                "---\n"
                "\n"
                "# Headroom Learned Context\n"
                "\n"
                f"{MARKER_START}\n{body}\n{MARKER_END}\n"
            )

        result.files_written.append(target)
        result.content_by_file[str(target)] = full_content
        result.memories_exported = len(budgeted)

        if not dry_run:
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(full_content)

        return result