@tanagram/cli 0.4.13 → 0.4.18

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
@@ -263,3 +263,5 @@ tanagram sync
263
263
  ---
264
264
 
265
265
  Built by [@fluttermatt](https://x.com/fluttermatt) and the [Tanagram](https://tanagram.ai/) team. Talk to us [on Twitter](https://x.com/tanagram_) or email: founders AT tanagram.ai
266
+
267
+ #
package/commands/run.go CHANGED
@@ -3,6 +3,7 @@ package commands
3
3
  import (
4
4
  "context"
5
5
  "fmt"
6
+ "log/slog"
6
7
  "os"
7
8
  "path/filepath"
8
9
  "sync"
@@ -26,10 +27,10 @@ func spinner(stop chan bool, message string) {
26
27
  for {
27
28
  select {
28
29
  case <-stop:
29
- fmt.Fprint(os.Stderr, "\r")
30
+ slog.Info("\r")
30
31
  return
31
32
  default:
32
- fmt.Fprintf(os.Stderr, "\r%s %s", chars[i%len(chars)], message)
33
+ slog.Info("spinner", "char", chars[i%len(chars)], "message", message)
33
34
  i++
34
35
  time.Sleep(100 * time.Millisecond)
35
36
  }
@@ -80,7 +81,7 @@ func Run() error {
80
81
  return err
81
82
  }
82
83
 
83
- fmt.Fprintf(os.Stderr, "\nSyncing policies with LLM (processing %d changed file(s) in parallel)...\n", len(filesToSync))
84
+ slog.Info("Syncing policies with LLM", "files_to_process", len(filesToSync))
84
85
 
85
86
  syncStart := time.Now()
86
87
  ctx := context.Background()
@@ -102,19 +103,16 @@ func Run() error {
102
103
  var completed int
103
104
  var mu sync.Mutex
104
105
  go func() {
105
- chars := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
106
- i := 0
107
106
  for {
108
107
  select {
109
108
  case <-stop:
110
- fmt.Fprint(os.Stderr, "\r")
109
+ slog.Info("spinner stopped")
111
110
  return
112
111
  default:
113
112
  mu.Lock()
114
113
  c := completed
115
114
  mu.Unlock()
116
- fmt.Fprintf(os.Stderr, "\r%s Processing files... (%d/%d completed)", chars[i%len(chars)], c, len(filesToSync))
117
- i++
115
+ slog.Info("Processing files", "completed", c, "total", len(filesToSync))
118
116
  time.Sleep(100 * time.Millisecond)
119
117
  }
120
118
  }
@@ -145,7 +143,7 @@ func Run() error {
145
143
  close(stop)
146
144
  time.Sleep(50 * time.Millisecond)
147
145
  mu.Lock()
148
- fmt.Fprintf(os.Stderr, "\r\033[K✗ Failed to process %s\n", result.relPath)
146
+ slog.Error("Failed to process file", "file", result.relPath)
149
147
  mu.Unlock()
150
148
  return fmt.Errorf("failed to extract policies from %s: %w", result.file, result.err)
151
149
  }
@@ -162,7 +160,7 @@ func Run() error {
162
160
  // Atomic update of counter and output (prevents race with spinner)
163
161
  mu.Lock()
164
162
  completed++
165
- fmt.Fprintf(os.Stderr, "\r\033[K✓ %s - %d policies\n", result.relPath, len(result.policies))
163
+ slog.Info("Processed file", "file", result.relPath, "policies", len(result.policies))
166
164
  mu.Unlock()
167
165
  }
168
166
 
@@ -176,7 +174,7 @@ func Run() error {
176
174
  }
177
175
 
178
176
  syncDuration := time.Since(syncStart)
179
- fmt.Fprintf(os.Stderr, "\n✓ Synced %d policies from %d changed file(s)\n", totalPolicies, len(filesToSync))
177
+ slog.Info("Sync complete", "policies_synced", totalPolicies, "files_synced", len(filesToSync))
180
178
 
181
179
  // Track sync metrics
182
180
  metrics.Track("cli.sync.complete", map[string]interface{}{
@@ -202,10 +200,10 @@ func Run() error {
202
200
  cloudPolicies, err = cloudStorage.LoadCloudPoliciesAsParserFormat(repoInfo.Owner, repoInfo.Name)
203
201
  if err != nil {
204
202
  // Cloud policies exist but failed to load - warn but continue
205
- fmt.Fprintf(os.Stderr, "Warning: Failed to load cloud policies: %v\n", err)
203
+ slog.Warn("Failed to load cloud policies", "error", err)
206
204
  cloudPolicies = []parser.Policy{}
207
205
  } else if len(cloudPolicies) > 0 {
208
- fmt.Fprintf(os.Stderr, "Loaded %d cloud policies for %s/%s\n", len(cloudPolicies), repoInfo.Owner, repoInfo.Name)
206
+ slog.Info("Loaded cloud policies", "count", len(cloudPolicies), "owner", repoInfo.Owner, "repo", repoInfo.Name)
209
207
  }
210
208
  }
211
209
  // If repo detection failed, silently continue with local-only policies
@@ -214,7 +212,7 @@ func Run() error {
214
212
  policies := storage.MergePolicies(localPolicies, cloudPolicies)
215
213
 
216
214
  if len(policies) == 0 {
217
- fmt.Fprintln(os.Stderr, "No enforceable policies found")
215
+ slog.Info("No enforceable policies found")
218
216
  return nil
219
217
  }
220
218
 
@@ -223,9 +221,9 @@ func Run() error {
223
221
  totalMerged := len(policies)
224
222
 
225
223
  if totalCloud > 0 {
226
- fmt.Fprintf(os.Stderr, "Total policies: %d (%d local, %d cloud, %d after merge)\n", totalMerged, totalLocal, totalCloud, totalMerged)
224
+ slog.Info("Total policies", "total", totalMerged, "local", totalLocal, "cloud", totalCloud, "after_merge", totalMerged)
227
225
  } else {
228
- fmt.Fprintf(os.Stderr, "Loaded %d local policies\n", totalLocal)
226
+ slog.Info("Loaded local policies", "count", totalLocal)
229
227
  }
230
228
 
231
229
  // Check if a snapshot exists (from PreToolUse hook)
@@ -233,7 +231,7 @@ func Run() error {
233
231
  useSnapshot := false
234
232
 
235
233
  if snapshot.Exists(gitRoot) {
236
- fmt.Fprintln(os.Stderr, "Snapshot detected - checking only Claude's changes...")
234
+ slog.Info("Snapshot detected - checking only Claude's changes")
237
235
 
238
236
  // Load snapshot
239
237
  snap, err := snapshot.Load(gitRoot)
@@ -265,13 +263,13 @@ func Run() error {
265
263
 
266
264
  // Delete snapshot after using it
267
265
  if err := snapshot.Delete(gitRoot); err != nil {
268
- fmt.Fprintf(os.Stderr, "Warning: failed to delete snapshot: %v\n", err)
266
+ slog.Warn("Failed to delete snapshot", "error", err)
269
267
  }
270
268
 
271
269
  useSnapshot = true
272
270
  } else {
273
271
  // No snapshot - fall back to checking all git changes
274
- fmt.Fprintln(os.Stderr, "Checking all changes (unstaged + staged)...")
272
+ slog.Info("Checking all changes (unstaged + staged)")
275
273
  diffResult, err := gitpkg.GetAllChanges()
276
274
  if err != nil {
277
275
  return fmt.Errorf("error getting git diff: %w", err)
@@ -282,14 +280,14 @@ func Run() error {
282
280
 
283
281
  if len(changesToCheck) == 0 {
284
282
  if useSnapshot {
285
- fmt.Fprintln(os.Stderr, "No changes detected since snapshot")
283
+ fmt.Fprint(os.Stdout, "No changes detected since snapshot\n")
286
284
  } else {
287
- fmt.Fprintln(os.Stderr, "No changes to check")
285
+ fmt.Fprint(os.Stdout, "No changes to check\n")
288
286
  }
289
287
  return nil
290
288
  }
291
289
 
292
- fmt.Fprintf(os.Stderr, "Scanning %d changed lines...\n\n", len(changesToCheck))
290
+ slog.Info("Scanning changed lines", "count", len(changesToCheck))
293
291
 
294
292
  // Get API key once upfront before checking
295
293
  apiKey, err := config.GetAPIKey()
@@ -337,6 +335,6 @@ func Run() error {
337
335
  }
338
336
 
339
337
  // No violations found - output success message
340
- fmt.Fprintln(os.Stderr, "No policy violations found")
338
+ fmt.Fprint(os.Stdout, "No policy violations found")
341
339
  return nil
342
340
  }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tanagram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,267 @@
1
+ # Tanagram CLI
2
+
3
+ A lightweight Go CLI that enforces policies from `AGENTS.md` files on your local git changes.
4
+
5
+ ## Quick Start
6
+
7
+ Run `tanagram` before committing to catch policy violations locally:
8
+
9
+ ```bash
10
+ $ tanagram
11
+
12
+ ✗ Found 1 policy violation(s):
13
+
14
+ webui/src/Button.tsx:42 - [No hardcoded colors] Don't use hard-coded color values; use theme colors instead
15
+ > background: "#FF5733"
16
+ ```
17
+
18
+ ## Installation
19
+
20
+ ### Quick Start (3 steps)
21
+
22
+ ```bash
23
+ # 1. Install globally via npm
24
+ npm install -g @tanagram/cli
25
+
26
+ # 2. Automatically add a Claude Code hook
27
+ tanagram config claude
28
+ # and/or if you use Cursor:
29
+ tanagram config cursor
30
+
31
+ # 3. Run tanagram (will prompt for API key on first run)
32
+ tanagram
33
+ ```
34
+
35
+ **Requirements:**
36
+ - Node.js >= 14.0.0
37
+ - **Anthropic API Key** (you'll be prompted to enter it on first run)
38
+
39
+ The CLI is written in Go but distributed via npm for easier installation and version management. During installation, npm automatically downloads the Go compiler and builds the binary for your platform (no manual setup needed!).
40
+
41
+ ### API Key Setup
42
+
43
+ Tanagram uses Claude AI (via Anthropic API) to extract policies from your instruction files. On first run, you'll be prompted to enter your API key, which will be saved to `~/.tanagram/config.json`.
44
+
45
+ **Get an API key:**
46
+ 1. Sign up at [https://console.anthropic.com](https://console.anthropic.com)
47
+ 2. Create an API key in the dashboard
48
+ 3. Run `tanagram` and enter your key when prompted
49
+
50
+ ## Usage
51
+
52
+ ```bash
53
+ # Check all changes (unstaged + staged) - automatically syncs if policies changed
54
+ tanagram
55
+ # or explicitly:
56
+ tanagram run
57
+
58
+ # Manually sync instruction files to cache
59
+ tanagram sync
60
+
61
+ # View all cached policies
62
+ tanagram list
63
+
64
+ # Show help
65
+ tanagram help
66
+ ```
67
+
68
+ **Smart Caching:** Policies are cached and automatically resynced when instruction files change (detected via MD5 hash).
69
+
70
+ ### Commands
71
+
72
+ - **`run`** (default) - Check git changes against policies with auto-sync
73
+ - **`sync`** - Manually sync all instruction files to cache
74
+ - **`list`** - View all cached policies (shows enforceable vs unenforceable)
75
+ - **`help`** - Show usage information
76
+
77
+ ### Claude Code Hook
78
+
79
+ Install the CLI as a Claude Code [hook](https://code.claude.com/docs/en/hooks) to have Claude automatically iterate on Tanagram's output.
80
+
81
+ **Easy setup (recommended):**
82
+ ```bash
83
+ tanagram config claude
84
+ ```
85
+
86
+ This automatically adds the hook to your `~/.claude/settings.json`. It's safe to run multiple times and will preserve any existing settings.
87
+
88
+ **Check hook status:**
89
+ ```bash
90
+ tanagram config list
91
+ ```
92
+
93
+ **Manual setup (alternative):**
94
+ If you prefer to manually edit your settings, add this to your `~/.claude/settings.json` (user settings) or `.claude/settings.json` (project settings):
95
+
96
+ ```json
97
+ {
98
+ "hooks": {
99
+ "SessionStart": [
100
+ {
101
+ "hooks": [
102
+ {
103
+ "command": "tanagram snapshot",
104
+ "type": "command"
105
+ }
106
+ ],
107
+ "matcher": "startup|clear"
108
+ }
109
+ ],
110
+ "Stop": [
111
+ {
112
+ "hooks": [
113
+ {
114
+ "command": "tanagram",
115
+ "type": "command"
116
+ }
117
+ ]
118
+ }
119
+ ]
120
+ }
121
+ }
122
+ ```
123
+
124
+ If you have existing hooks, you can merge this hook into your existing config.
125
+
126
+ ### Cursor Hook
127
+
128
+ Install the CLI as a Cursor Code [hook](https://cursor.com/docs/agent/hooks) to have Cursor automatically iterate on Tanagram's output.
129
+
130
+ **Easy setup (recommended):**
131
+ ```bash
132
+ tanagram config cursor
133
+ ```
134
+
135
+ This automatically adds the hook to your `~/.cursor/hooks.json`. It's safe to run multiple times and will preserve any existing settings.
136
+
137
+ **Check hook status:**
138
+ ```bash
139
+ tanagram config list
140
+ ```
141
+
142
+ **Manual setup (alternative):**
143
+ If you prefer to manually edit your settings, add this to your `~/.cursor/hooks.json` (user settings) or `.cursor/hooks.json` (project settings):
144
+
145
+ ```json
146
+ {
147
+ "hooks": {
148
+ "beforeSubmitPrompt": [
149
+ {
150
+ "command": "tanagram snapshot"
151
+ }
152
+ ],
153
+ "stop": [
154
+ {
155
+ "command": "tanagram"
156
+ }
157
+ ]
158
+ },
159
+ "version": 1
160
+ }
161
+ ```
162
+
163
+ If you have existing hooks, you can merge this hook into your existing config.
164
+
165
+
166
+ ## How It Works
167
+
168
+ 1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md`, `BUGBOT.md`, and `.cursor/rules/*.mdc` in your git repository
169
+ 2. **Checks cache** - Loads cached policies and MD5 hashes from `.tanagram/`
170
+ 3. **Auto-syncs** - Detects file changes via MD5 and automatically resyncs if needed
171
+ 4. **LLM extraction** - Uses Claude AI to extract ALL policies from instruction files
172
+ 5. **Gets git diff** - Analyzes all your changes (unstaged + staged)
173
+ 6. **LLM detection** - Checks violations using intelligent semantic analysis
174
+ 7. **Reports results** - Terminal output with detailed reasoning for each violation
175
+
176
+ ### Cache Location
177
+
178
+ Policies are cached in `.tanagram/cache.gob` at your git repository root. Add this to your `.gitignore`:
179
+
180
+ ```gitignore
181
+ .tanagram/
182
+ ```
183
+
184
+ ### What Can Be Enforced
185
+
186
+ **Everything!** Because the LLM reads and understands code like a human:
187
+
188
+ **Simple patterns:**
189
+ - "Don't use hard-coded colors" → Detects `#FF5733`, `rgb()`, etc.
190
+ - "Use ruff format, not black" → Detects `black` usage
191
+ - "Always use === instead of ==" → Detects `==` operators
192
+
193
+ **Complex guidelines:**
194
+ - "Break down code into modular functions" → Analyzes function length and complexity
195
+ - "Don't deeply layer code" → Detects excessive nesting
196
+ - "Ensure no code smells" → Identifies common anti-patterns
197
+ - "Use structured logging with request IDs" → Checks logging patterns
198
+ - "Prefer async/await for I/O" → Understands async patterns
199
+
200
+ **Language-specific idioms:**
201
+ - Knows Go uses PascalCase for exports (not Python's snake_case)
202
+ - Won't flag Go code for missing Python type hints
203
+ - Understands JavaScript !== Python !== Go
204
+
205
+ ### Exit Codes
206
+
207
+ - `0` - No violations found
208
+ - `2` - Violations found (triggers Claude Code automatic fix behavior)
209
+
210
+ ## Example
211
+
212
+ Create an `AGENTS.md` in your repo with policies:
213
+
214
+ ```markdown
215
+ # Development Policies
216
+
217
+ - Don't use hard-coded color values; use theme colors instead
218
+ - Use ruff format for Python formatting, not black
219
+ - Always use async/await for database operations
220
+ ```
221
+
222
+ Or use Cursor rules files in `.cursor/rules/`:
223
+
224
+ ```markdown
225
+ ---
226
+ description: TypeScript coding standards
227
+ globs: ["*.ts", "*.tsx"]
228
+ ---
229
+
230
+ # TypeScript Standards
231
+
232
+ - Use strict type checking
233
+ - Avoid using 'any' type
234
+ - Prefer interfaces over type aliases
235
+ ```
236
+
237
+ Then run `tanagram` to enforce them locally!
238
+
239
+ **Note:** For `.mdc` files, Tanagram extracts policies from the markdown content only (YAML frontmatter is used by Cursor and ignored during policy extraction).
240
+
241
+ ## Tanagram Web Integration
242
+
243
+ You can also use [Tanagram](https://tanagram.ai) to manage policies across your organization and enforce them on PRs.
244
+ If you have policies defined online, you can enforce them while you develop locally with the CLI as well.
245
+
246
+ ```bash
247
+ # Connect your account
248
+ tanagram login
249
+
250
+ # Download policies from your Tanagram account and cache them locally
251
+ tanagram sync
252
+ ```
253
+
254
+ For customers with an on-prem installation, set the `TANAGRAM_WEB_HOSTNAME` environment variable to the URL of your Tanagram instance — for example:
255
+
256
+ ```bash
257
+ export TANAGRAM_WEB_HOSTNAME=https://yourcompany.tanagram.ai
258
+
259
+ tanagram login
260
+ tanagram sync
261
+ ```
262
+
263
+ ---
264
+
265
+ Built by [@fluttermatt](https://x.com/fluttermatt) and the [Tanagram](https://tanagram.ai/) team. Talk to us [on Twitter](https://x.com/tanagram_) or email: founders AT tanagram.ai
266
+
267
+ #
Binary file
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tanagram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.