@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 +19 -2
- package/bin/tanagram +0 -0
- package/commands/run.go +1 -1
- package/commands/sync.go +12 -2
- package/extractor/extractor.go +22 -17
- package/extractor/file.go +12 -3
- package/main.go +5 -3
- package/package.json +1 -1
- package/parser/mdc.go +57 -0
- package/parser/mdc_test.go +137 -0
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
|
-
- `
|
|
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
|
package/extractor/extractor.go
CHANGED
|
@@ -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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
- Import/dependency
|
|
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
|
-
|
|
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,
|
|
13
|
-
content, err := os.ReadFile(
|
|
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
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
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
|
+
}
|