@tanagram/cli 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,7 +123,7 @@ If you have existing hooks, you can merge this hook into your existing config.
123
123
 
124
124
  ## How It Works
125
125
 
126
- 1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md` in your git repository
126
+ 1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md`, and `.cursor/rules/*.mdc` in your git repository
127
127
  2. **Checks cache** - Loads cached policies and MD5 hashes from `.tanagram/`
128
128
  3. **Auto-syncs** - Detects file changes via MD5 and automatically resyncs if needed
129
129
  4. **LLM extraction** - Uses Claude AI to extract ALL policies from instruction files
@@ -181,7 +181,7 @@ Claude AI analyzes code changes against all policies:
181
181
  ## Exit Codes
182
182
 
183
183
  - `0` - No violations found
184
- - `1` - Violations found (fails CI/CD if integrated)
184
+ - `2` - Violations found (triggers Claude Code automatic fix behavior)
185
185
 
186
186
  ## Example
187
187
 
@@ -195,8 +195,25 @@ Create an `AGENTS.md` in your repo with policies:
195
195
  - Always use async/await for database operations
196
196
  ```
197
197
 
198
+ Or use Cursor rules files in `.cursor/rules/`:
199
+
200
+ ```markdown
201
+ ---
202
+ description: TypeScript coding standards
203
+ globs: ["*.ts", "*.tsx"]
204
+ ---
205
+
206
+ # TypeScript Standards
207
+
208
+ - Use strict type checking
209
+ - Avoid using 'any' type
210
+ - Prefer interfaces over type aliases
211
+ ```
212
+
198
213
  Then run `tanagram` to enforce them locally!
199
214
 
215
+ **Note:** For `.mdc` files, Tanagram extracts policies from the markdown content only (YAML frontmatter is used by Cursor and ignored during policy extraction).
216
+
200
217
  ---
201
218
 
202
219
  ## Contributing
package/bin/tanagram CHANGED
Binary file
package/commands/run.go CHANGED
@@ -47,7 +47,7 @@ func Run() error {
47
47
  }
48
48
 
49
49
  if len(instructionFiles) == 0 {
50
- return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md)")
50
+ return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md, .cursor/rules/*.mdc)")
51
51
  }
52
52
 
53
53
  // Load cache
package/commands/sync.go CHANGED
@@ -28,7 +28,7 @@ func Sync() error {
28
28
  }
29
29
 
30
30
  if len(instructionFiles) == 0 {
31
- return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md)")
31
+ return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md, .cursor/rules/*.mdc)")
32
32
  }
33
33
 
34
34
  fmt.Printf("Found %d instruction file(s)\n", len(instructionFiles))
@@ -137,7 +137,7 @@ func Sync() error {
137
137
  }
138
138
 
139
139
  // FindInstructionFiles searches for instruction files in the git repository
140
- // Looks for: AGENTS.md, POLICIES.md, CLAUDE.md
140
+ // Looks for: AGENTS.md, POLICIES.md, CLAUDE.md, and .cursor/rules/*.mdc
141
141
  func FindInstructionFiles(gitRoot string) ([]string, error) {
142
142
  var files []string
143
143
 
@@ -175,12 +175,22 @@ func FindInstructionFiles(gitRoot string) ([]string, error) {
175
175
 
176
176
  // Check if this is one of our instruction files
177
177
  if !info.IsDir() {
178
+ // Check for common instruction files (AGENTS.md, etc.)
178
179
  for _, name := range commonNames {
179
180
  if info.Name() == name {
180
181
  files = append(files, path)
181
182
  break
182
183
  }
183
184
  }
185
+
186
+ // Check for .mdc files in .cursor/rules/ directory
187
+ if filepath.Ext(info.Name()) == ".mdc" {
188
+ // Check if this file is inside .cursor/rules/
189
+ relPath, err := filepath.Rel(gitRoot, path)
190
+ if err == nil && filepath.HasPrefix(relPath, filepath.Join(".cursor", "rules")) {
191
+ files = append(files, path)
192
+ }
193
+ }
184
194
  }
185
195
 
186
196
  return nil
@@ -60,28 +60,33 @@ func ExtractPolicies(ctx context.Context, content string) ([]parser.Policy, erro
60
60
 
61
61
  // buildExtractionPrompt creates the prompt for policy extraction
62
62
  func buildExtractionPrompt(content string) string {
63
- return fmt.Sprintf(`You are a code policy extraction system. Extract ONLY coding policies that apply when developers write and edit code.
64
-
65
- Analyze the following instruction file and extract policies that define:
66
- - Code structure and organization rules
67
- - Coding standards and style guidelines
68
- - Language-specific patterns and practices
69
- - File naming and directory conventions
70
- - Error handling requirements
71
- - Security practices in code
72
- - Testing requirements
73
- - Performance optimization rules
74
- - Import/dependency management
75
-
76
- DO NOT extract:
63
+ return fmt.Sprintf(`You are a code policy extraction system. Extract ONLY policies that define anti-patterns or bad practices that can be detected by reading code.
64
+
65
+ IMPORTANT: Extract policies about what the CODE should or shouldn't look like. DO NOT extract policies that require running commands, executing tools, or performing actions.
66
+
67
+ Extract policies that define CODE ANTI-PATTERNS:
68
+ - Code structure and organization anti-patterns (e.g., "Don't use deeply nested functions")
69
+ - Coding standards violations (e.g., "Don't use var, use let/const")
70
+ - Language-specific anti-patterns (e.g., "Don't use 'any' type in TypeScript")
71
+ - Prohibited code patterns (e.g., "Don't hardcode API keys")
72
+ - Security anti-patterns in code (e.g., "Don't use eval()")
73
+ - Error handling anti-patterns (e.g., "Don't swallow exceptions silently")
74
+ - Import/dependency anti-patterns (e.g., "Don't import entire lodash library")
75
+
76
+ DO NOT extract policies that require ACTIONS or COMMANDS:
77
+ - Running tests (e.g., "Run pytest before committing")
78
+ - Running linters or formatters (e.g., "Run black on all Python files")
79
+ - Building or compiling (e.g., "Build the project to verify")
80
+ - Running scripts or commands (e.g., "Execute database migrations")
81
+ - Using specific tools (e.g., "Use ruff format" - this requires running ruff)
77
82
  - Agent behavior instructions (how AI agents should operate)
78
83
  - Workflow or process instructions
79
- - Tool usage instructions
80
84
  - Communication style guidelines
81
85
  - Project management policies
82
- - Non-code related guidelines
83
86
 
84
- For each code policy, provide:
87
+ CRITICAL: If a policy would require the agent to run a command or tool to fix a violation, DO NOT extract it. Only extract policies about code patterns that can be fixed by editing code directly.
88
+
89
+ For each code anti-pattern policy, provide:
85
90
  1. Name: Short descriptive name (2-5 words)
86
91
  2. Message: The full policy text or a clear summary
87
92
  3. OriginalText: The exact text from the file
package/extractor/file.go CHANGED
@@ -4,16 +4,25 @@ import (
4
4
  "context"
5
5
  "fmt"
6
6
  "os"
7
+ "path/filepath"
8
+ "strings"
7
9
 
8
10
  "github.com/tanagram/cli/parser"
9
11
  )
10
12
 
11
13
  // ExtractPoliciesFromFile reads a file and extracts policies using LLM
12
- func ExtractPoliciesFromFile(ctx context.Context, filepath string) ([]parser.Policy, error) {
13
- content, err := os.ReadFile(filepath)
14
+ func ExtractPoliciesFromFile(ctx context.Context, filePath string) ([]parser.Policy, error) {
15
+ content, err := os.ReadFile(filePath)
14
16
  if err != nil {
15
17
  return nil, fmt.Errorf("failed to read file: %w", err)
16
18
  }
17
19
 
18
- return ExtractPolicies(ctx, string(content))
20
+ contentStr := string(content)
21
+
22
+ // If this is an .mdc file, strip YAML frontmatter
23
+ if strings.HasSuffix(strings.ToLower(filepath.Base(filePath)), ".mdc") {
24
+ contentStr = parser.StripMDCFrontmatter(contentStr)
25
+ }
26
+
27
+ return ExtractPolicies(ctx, contentStr)
19
28
  }
package/main.go CHANGED
@@ -56,9 +56,11 @@ EXAMPLES:
56
56
  tanagram list # View all cached policies
57
57
 
58
58
  INSTRUCTION FILES:
59
- Tanagram looks for instruction files like AGENTS.md, POLICIES.md, or CLAUDE.md
60
- in your git repository. Policies are cached and automatically resynced
61
- when files change.
59
+ Tanagram looks for instruction files in your git repository:
60
+ - AGENTS.md, POLICIES.md, CLAUDE.md
61
+ - Cursor rules: .cursor/rules/*.mdc
62
+
63
+ Policies are cached and automatically resynced when files change.
62
64
  `
63
65
  fmt.Print(help)
64
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
package/parser/mdc.go ADDED
@@ -0,0 +1,57 @@
1
+ package parser
2
+
3
+ import (
4
+ "bufio"
5
+ "strings"
6
+ )
7
+
8
+ // StripMDCFrontmatter removes YAML frontmatter from .mdc files
9
+ // .mdc files have optional YAML frontmatter between --- delimiters
10
+ // We only need the markdown content for policy extraction
11
+ func StripMDCFrontmatter(content string) string {
12
+ lines := strings.Split(content, "\n")
13
+
14
+ // Check if file starts with frontmatter delimiter
15
+ if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
16
+ // No frontmatter, return as-is
17
+ return content
18
+ }
19
+
20
+ // Find the closing frontmatter delimiter
21
+ scanner := bufio.NewScanner(strings.NewReader(content))
22
+ inFrontmatter := false
23
+ frontmatterEnded := false
24
+ lineNum := 0
25
+
26
+ var markdownLines []string
27
+
28
+ for scanner.Scan() {
29
+ line := scanner.Text()
30
+ lineNum++
31
+
32
+ // First line is opening ---
33
+ if lineNum == 1 && strings.TrimSpace(line) == "---" {
34
+ inFrontmatter = true
35
+ continue
36
+ }
37
+
38
+ // Look for closing ---
39
+ if inFrontmatter && strings.TrimSpace(line) == "---" {
40
+ inFrontmatter = false
41
+ frontmatterEnded = true
42
+ continue
43
+ }
44
+
45
+ // Skip frontmatter content
46
+ if inFrontmatter {
47
+ continue
48
+ }
49
+
50
+ // After frontmatter ends, collect markdown lines
51
+ if frontmatterEnded || lineNum > 1 {
52
+ markdownLines = append(markdownLines, line)
53
+ }
54
+ }
55
+
56
+ return strings.Join(markdownLines, "\n")
57
+ }
@@ -0,0 +1,137 @@
1
+ package parser
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestStripMDCFrontmatter_WithFrontmatter(t *testing.T) {
9
+ input := `---
10
+ description: TypeScript coding standards
11
+ globs: ["*.ts", "*.tsx"]
12
+ alwaysApply: false
13
+ ---
14
+
15
+ # TypeScript Standards
16
+
17
+ - Use strict type checking
18
+ - Avoid using 'any' type`
19
+
20
+ expected := `
21
+ # TypeScript Standards
22
+
23
+ - Use strict type checking
24
+ - Avoid using 'any' type`
25
+
26
+ result := StripMDCFrontmatter(input)
27
+
28
+ if result != expected {
29
+ t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result)
30
+ }
31
+ }
32
+
33
+ func TestStripMDCFrontmatter_NoFrontmatter(t *testing.T) {
34
+ input := `# TypeScript Standards
35
+
36
+ - Use strict type checking
37
+ - Avoid using 'any' type`
38
+
39
+ result := StripMDCFrontmatter(input)
40
+
41
+ if result != input {
42
+ t.Errorf("Expected input to be unchanged when no frontmatter present")
43
+ }
44
+ }
45
+
46
+ func TestStripMDCFrontmatter_EmptyFile(t *testing.T) {
47
+ input := ""
48
+ result := StripMDCFrontmatter(input)
49
+
50
+ if result != input {
51
+ t.Errorf("Expected empty string for empty input")
52
+ }
53
+ }
54
+
55
+ func TestStripMDCFrontmatter_OnlyFrontmatter(t *testing.T) {
56
+ input := `---
57
+ description: Test rule
58
+ globs: ["*.ts"]
59
+ ---`
60
+
61
+ result := StripMDCFrontmatter(input)
62
+
63
+ // Should return empty or minimal content after frontmatter
64
+ if strings.TrimSpace(result) != "" {
65
+ t.Errorf("Expected empty content after stripping frontmatter-only file, got: %s", result)
66
+ }
67
+ }
68
+
69
+ func TestStripMDCFrontmatter_ComplexFrontmatter(t *testing.T) {
70
+ input := `---
71
+ description: Multi-line description
72
+ that spans multiple lines
73
+ globs:
74
+ - "src/**/*.ts"
75
+ - "test/**/*.test.ts"
76
+ alwaysApply: false
77
+ priority: 10
78
+ dependencies:
79
+ - other-rule.mdc
80
+ ---
81
+
82
+ # Complex Rule
83
+
84
+ ## Section 1
85
+ - Policy 1
86
+ - Policy 2
87
+
88
+ ## Section 2
89
+ - Policy 3`
90
+
91
+ result := StripMDCFrontmatter(input)
92
+
93
+ // Should contain markdown but not frontmatter
94
+ if !strings.Contains(result, "# Complex Rule") {
95
+ t.Errorf("Expected markdown content to be preserved")
96
+ }
97
+ if strings.Contains(result, "description:") {
98
+ t.Errorf("Expected frontmatter to be removed")
99
+ }
100
+ if strings.Contains(result, "globs:") {
101
+ t.Errorf("Expected frontmatter to be removed")
102
+ }
103
+ }
104
+
105
+ func TestStripMDCFrontmatter_MarkdownWithCodeBlocks(t *testing.T) {
106
+ input := `---
107
+ description: Code examples
108
+ ---
109
+
110
+ # Rules
111
+
112
+ Example with --- in code:
113
+
114
+ ` + "```" + `yaml
115
+ ---
116
+ some: yaml
117
+ ---
118
+ ` + "```" + `
119
+
120
+ - Follow this policy`
121
+
122
+ result := StripMDCFrontmatter(input)
123
+
124
+ // Should preserve --- inside code blocks
125
+ if !strings.Contains(result, "```yaml") {
126
+ t.Errorf("Expected code blocks to be preserved")
127
+ }
128
+ if !strings.Contains(result, "- Follow this policy") {
129
+ t.Errorf("Expected policy content to be preserved")
130
+ }
131
+ // First frontmatter should be removed
132
+ lines := strings.Split(result, "\n")
133
+ if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" {
134
+ // This is tricky - the first --- should be gone, but ones in code blocks should remain
135
+ // For now, we accept this limitation as our parser is simple
136
+ }
137
+ }