@tanagram/cli 0.1.7 → 0.1.8

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
@@ -90,13 +90,37 @@ tanagram help
90
90
 
91
91
  **Smart Caching:** Policies are cached and automatically resynced when instruction files change (detected via MD5 hash).
92
92
 
93
- ## Commands
93
+ ### Commands
94
94
 
95
95
  - **`run`** (default) - Check git changes against policies with auto-sync
96
96
  - **`sync`** - Manually sync all instruction files to cache
97
97
  - **`list`** - View all cached policies (shows enforceable vs unenforceable)
98
98
  - **`help`** - Show usage information
99
99
 
100
+ ### Claude Code Hook
101
+ You can install the CLI as a Claude Code [hook](https://code.claude.com/docs/en/hooks) to have Claude automatically iterate on Tanagram's output. Add the following to your `~/.claude/settings.json` (user settings) or `.claude/settings.json` (project settings):
102
+
103
+ ```json
104
+ {
105
+ "hooks": {
106
+ "PostToolUse": [
107
+ {
108
+ "matcher": "Edit|Write",
109
+ "hooks": [
110
+ {
111
+ "type": "command",
112
+ "command": "tanagram"
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+ }
118
+ }
119
+ ```
120
+
121
+ If you have existing hooks, you can merge this hook into your existing config.
122
+
123
+
100
124
  ## How It Works
101
125
 
102
126
  1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md` in your git repository
package/bin/tanagram CHANGED
Binary file
@@ -21,7 +21,7 @@ type Violation struct {
21
21
 
22
22
  // CheckResult contains all violations found
23
23
  type CheckResult struct {
24
- Violations []Violation
24
+ Violations []Violation
25
25
  TotalChecked int
26
26
  }
27
27
 
@@ -89,3 +89,52 @@ func FormatViolations(result *CheckResult) string {
89
89
 
90
90
  return output.String()
91
91
  }
92
+
93
+ // FormatClaudeInstructions formats violations as actionable instructions for Claude Code
94
+ // to automatically fix via exit code 2 hook behavior
95
+ func FormatClaudeInstructions(result *CheckResult) string {
96
+ if len(result.Violations) == 0 {
97
+ return ""
98
+ }
99
+
100
+ // Group violations by file
101
+ violationsByFile := make(map[string][]Violation)
102
+ for _, v := range result.Violations {
103
+ violationsByFile[v.File] = append(violationsByFile[v.File], v)
104
+ }
105
+
106
+ // Sort files for consistent output
107
+ files := make([]string, 0, len(violationsByFile))
108
+ for file := range violationsByFile {
109
+ files = append(files, file)
110
+ }
111
+ sort.Strings(files)
112
+
113
+ var output strings.Builder
114
+ output.WriteString("POLICY VIOLATIONS DETECTED - PLEASE FIX\n\n")
115
+ output.WriteString(fmt.Sprintf("Found %d policy violation(s) that need to be fixed:\n\n", len(result.Violations)))
116
+
117
+ for _, file := range files {
118
+ violations := violationsByFile[file]
119
+
120
+ // Sort violations by line number
121
+ sort.Slice(violations, func(i, j int) bool {
122
+ return violations[i].LineNumber < violations[j].LineNumber
123
+ })
124
+
125
+ output.WriteString(fmt.Sprintf("File: %s\n", file))
126
+
127
+ for _, v := range violations {
128
+ output.WriteString(fmt.Sprintf("\n Line %d:\n", v.LineNumber))
129
+ output.WriteString(fmt.Sprintf(" Code: %s\n", v.Code))
130
+ output.WriteString(fmt.Sprintf(" Policy: %s\n", v.PolicyName))
131
+ output.WriteString(fmt.Sprintf(" Issue: %s\n", v.Message))
132
+ output.WriteString(fmt.Sprintf(" Action: Review and fix this code to comply with the policy.\n"))
133
+ }
134
+ output.WriteString("\n")
135
+ }
136
+
137
+ output.WriteString("Please fix all violations listed above and ensure the code complies with all policies.\n")
138
+
139
+ return output.String()
140
+ }
@@ -0,0 +1,215 @@
1
+ package checker
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestFormatViolations_NoViolations(t *testing.T) {
9
+ result := &CheckResult{
10
+ Violations: []Violation{},
11
+ TotalChecked: 10,
12
+ }
13
+
14
+ output := FormatViolations(result)
15
+ expected := "✓ No policy violations found"
16
+
17
+ if output != expected {
18
+ t.Errorf("Expected %q, got %q", expected, output)
19
+ }
20
+ }
21
+
22
+ func TestFormatViolations_WithViolations(t *testing.T) {
23
+ result := &CheckResult{
24
+ Violations: []Violation{
25
+ {
26
+ File: "main.go",
27
+ LineNumber: 10,
28
+ PolicyName: "No hardcoded secrets",
29
+ Message: "Potential hardcoded API key detected",
30
+ Code: "apiKey := \"sk-1234567890\"",
31
+ },
32
+ {
33
+ File: "config.go",
34
+ LineNumber: 5,
35
+ PolicyName: "Use environment variables",
36
+ Message: "Configuration should use environment variables",
37
+ Code: "var token = \"abc123\"",
38
+ },
39
+ },
40
+ TotalChecked: 50,
41
+ }
42
+
43
+ output := FormatViolations(result)
44
+
45
+ // Check that output contains expected elements
46
+ if !strings.Contains(output, "✗ Found 2 policy violation(s)") {
47
+ t.Errorf("Expected violation count in output, got: %s", output)
48
+ }
49
+ if !strings.Contains(output, "main.go") {
50
+ t.Errorf("Expected file name 'main.go' in output")
51
+ }
52
+ if !strings.Contains(output, "config.go") {
53
+ t.Errorf("Expected file name 'config.go' in output")
54
+ }
55
+ if !strings.Contains(output, "10:") {
56
+ t.Errorf("Expected line number 10 in output")
57
+ }
58
+ if !strings.Contains(output, "Potential hardcoded API key detected") {
59
+ t.Errorf("Expected violation message in output")
60
+ }
61
+ }
62
+
63
+ func TestFormatClaudeInstructions_NoViolations(t *testing.T) {
64
+ result := &CheckResult{
65
+ Violations: []Violation{},
66
+ TotalChecked: 10,
67
+ }
68
+
69
+ output := FormatClaudeInstructions(result)
70
+
71
+ if output != "" {
72
+ t.Errorf("Expected empty string for no violations, got: %s", output)
73
+ }
74
+ }
75
+
76
+ func TestFormatClaudeInstructions_WithViolations(t *testing.T) {
77
+ result := &CheckResult{
78
+ Violations: []Violation{
79
+ {
80
+ File: "main.go",
81
+ LineNumber: 10,
82
+ PolicyName: "No hardcoded secrets",
83
+ Message: "Potential hardcoded API key detected",
84
+ Code: "apiKey := \"sk-1234567890\"",
85
+ },
86
+ {
87
+ File: "main.go",
88
+ LineNumber: 15,
89
+ PolicyName: "No hardcoded secrets",
90
+ Message: "Potential hardcoded password detected",
91
+ Code: "password := \"secret123\"",
92
+ },
93
+ {
94
+ File: "config.go",
95
+ LineNumber: 5,
96
+ PolicyName: "Use environment variables",
97
+ Message: "Configuration should use environment variables",
98
+ Code: "var token = \"abc123\"",
99
+ },
100
+ },
101
+ TotalChecked: 50,
102
+ }
103
+
104
+ output := FormatClaudeInstructions(result)
105
+
106
+ // Check header
107
+ if !strings.Contains(output, "POLICY VIOLATIONS DETECTED - PLEASE FIX") {
108
+ t.Errorf("Expected header in output")
109
+ }
110
+
111
+ // Check violation count
112
+ if !strings.Contains(output, "Found 3 policy violation(s)") {
113
+ t.Errorf("Expected violation count in output, got: %s", output)
114
+ }
115
+
116
+ // Check file names
117
+ if !strings.Contains(output, "File: main.go") {
118
+ t.Errorf("Expected 'File: main.go' in output")
119
+ }
120
+ if !strings.Contains(output, "File: config.go") {
121
+ t.Errorf("Expected 'File: config.go' in output")
122
+ }
123
+
124
+ // Check line numbers
125
+ if !strings.Contains(output, "Line 10:") {
126
+ t.Errorf("Expected 'Line 10:' in output")
127
+ }
128
+ if !strings.Contains(output, "Line 15:") {
129
+ t.Errorf("Expected 'Line 15:' in output")
130
+ }
131
+ if !strings.Contains(output, "Line 5:") {
132
+ t.Errorf("Expected 'Line 5:' in output")
133
+ }
134
+
135
+ // Check policy names
136
+ if !strings.Contains(output, "Policy: No hardcoded secrets") {
137
+ t.Errorf("Expected policy name in output")
138
+ }
139
+ if !strings.Contains(output, "Policy: Use environment variables") {
140
+ t.Errorf("Expected policy name in output")
141
+ }
142
+
143
+ // Check code snippets
144
+ if !strings.Contains(output, "Code: apiKey := \"sk-1234567890\"") {
145
+ t.Errorf("Expected code snippet in output")
146
+ }
147
+
148
+ // Check issue messages
149
+ if !strings.Contains(output, "Issue: Potential hardcoded API key detected") {
150
+ t.Errorf("Expected issue message in output")
151
+ }
152
+
153
+ // Check action instruction
154
+ if !strings.Contains(output, "Action: Review and fix this code to comply with the policy.") {
155
+ t.Errorf("Expected action instruction in output")
156
+ }
157
+
158
+ // Check footer
159
+ if !strings.Contains(output, "Please fix all violations listed above and ensure the code complies with all policies.") {
160
+ t.Errorf("Expected footer instruction in output")
161
+ }
162
+ }
163
+
164
+ func TestFormatClaudeInstructions_MultipleFilesGroupedAndSorted(t *testing.T) {
165
+ result := &CheckResult{
166
+ Violations: []Violation{
167
+ {
168
+ File: "z_file.go",
169
+ LineNumber: 20,
170
+ PolicyName: "Policy Z",
171
+ Message: "Issue Z",
172
+ Code: "code z",
173
+ },
174
+ {
175
+ File: "a_file.go",
176
+ LineNumber: 30,
177
+ PolicyName: "Policy A2",
178
+ Message: "Issue A2",
179
+ Code: "code a2",
180
+ },
181
+ {
182
+ File: "a_file.go",
183
+ LineNumber: 10,
184
+ PolicyName: "Policy A1",
185
+ Message: "Issue A1",
186
+ Code: "code a1",
187
+ },
188
+ },
189
+ TotalChecked: 50,
190
+ }
191
+
192
+ output := FormatClaudeInstructions(result)
193
+
194
+ // Files should be sorted alphabetically
195
+ aFileIndex := strings.Index(output, "File: a_file.go")
196
+ zFileIndex := strings.Index(output, "File: z_file.go")
197
+
198
+ if aFileIndex == -1 || zFileIndex == -1 {
199
+ t.Errorf("Expected both file names in output")
200
+ }
201
+ if aFileIndex >= zFileIndex {
202
+ t.Errorf("Expected files to be sorted alphabetically (a_file.go before z_file.go)")
203
+ }
204
+
205
+ // Within a file, violations should be sorted by line number
206
+ line10Index := strings.Index(output, "Line 10:")
207
+ line30Index := strings.Index(output, "Line 30:")
208
+
209
+ if line10Index == -1 || line30Index == -1 {
210
+ t.Errorf("Expected both line numbers in output")
211
+ }
212
+ if line10Index >= line30Index {
213
+ t.Errorf("Expected violations to be sorted by line number (10 before 30)")
214
+ }
215
+ }
package/commands/run.go CHANGED
@@ -196,14 +196,21 @@ func Run() error {
196
196
  ctx := context.Background()
197
197
  result := checker.CheckChanges(ctx, diffResult.Changes, policies)
198
198
 
199
- // Output results
200
- output := checker.FormatViolations(result)
201
- fmt.Print(output)
202
-
203
- // Exit with error if violations found
199
+ // Handle results based on whether violations were found
204
200
  if len(result.Violations) > 0 {
205
- os.Exit(1)
201
+ // Format and output violations to stderr
202
+ violationOutput := checker.FormatViolations(result)
203
+ fmt.Fprint(os.Stderr, violationOutput)
204
+
205
+ // Format and output Claude-friendly instructions to stderr
206
+ claudeInstructions := checker.FormatClaudeInstructions(result)
207
+ fmt.Fprint(os.Stderr, claudeInstructions)
208
+
209
+ // Exit with code 2 to trigger Claude Code hook behavior
210
+ os.Exit(2)
206
211
  }
207
212
 
213
+ // No violations found - output success message
214
+ fmt.Println("✓ No policy violations found")
208
215
  return nil
209
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {