@tanagram/cli 0.4.14 → 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.
@@ -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
package/install.js CHANGED
@@ -4,11 +4,134 @@ const { execSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
+ const https = require('https');
7
8
  const pkg = require('./package.json');
8
9
 
9
10
  const GO_VERSION = '1.21.0';
10
11
  const GO_CACHE_DIR = path.join(os.homedir(), '.tanagram', `go-${GO_VERSION}`);
11
12
 
13
+ // Map Node.js platform/arch to GoReleaser naming
14
+ function getPlatformInfo() {
15
+ const platform = os.platform();
16
+ const arch = os.arch();
17
+
18
+ const platformMap = {
19
+ 'darwin': 'darwin',
20
+ 'linux': 'linux',
21
+ 'win32': 'windows'
22
+ };
23
+
24
+ const archMap = {
25
+ 'x64': 'amd64',
26
+ 'arm64': 'arm64'
27
+ };
28
+
29
+ const nodePlatformDir = {
30
+ 'darwin': arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64',
31
+ 'linux': arch === 'arm64' ? 'linux-arm64' : 'linux-x64',
32
+ 'win32': 'win32-x64'
33
+ };
34
+
35
+ return {
36
+ goos: platformMap[platform] || platform,
37
+ goarch: archMap[arch] || arch,
38
+ binaryName: platform === 'win32' ? 'tanagram.exe' : 'tanagram',
39
+ platformDir: nodePlatformDir[platform]
40
+ };
41
+ }
42
+
43
+ // Check for prebuilt binary bundled with npm package
44
+ function checkPrebuiltBinary() {
45
+ const { platformDir, binaryName } = getPlatformInfo();
46
+
47
+ // Check in dist/npm directory (where CI puts them)
48
+ const distPath = path.join(__dirname, 'dist', 'npm', platformDir, binaryName);
49
+ if (fs.existsSync(distPath)) {
50
+ console.log(`✓ Found prebuilt binary for ${platformDir}`);
51
+ return distPath;
52
+ }
53
+
54
+ // Check in platform directory at package root
55
+ const rootPath = path.join(__dirname, platformDir, binaryName);
56
+ if (fs.existsSync(rootPath)) {
57
+ console.log(`✓ Found prebuilt binary for ${platformDir}`);
58
+ return rootPath;
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ // Download prebuilt binary from GitHub releases
65
+ async function downloadPrebuiltBinary() {
66
+ const { goos, goarch, binaryName } = getPlatformInfo();
67
+ const version = pkg.version;
68
+ const ext = goos === 'windows' ? 'zip' : 'tar.gz';
69
+ const releaseTag = `cli-v${version}`;
70
+ const assetName = `tanagram_${version}_${goos}_${goarch}.${ext}`;
71
+
72
+ const url = `https://github.com/tanagram/monorepo/releases/download/${releaseTag}/${assetName}`;
73
+ const tmpDir = path.join(os.tmpdir(), `tanagram-${Date.now()}`);
74
+ const archivePath = path.join(tmpDir, assetName);
75
+
76
+ console.log(`📦 Downloading prebuilt binary from GitHub releases...`);
77
+ console.log(` ${url}`);
78
+
79
+ return new Promise((resolve, reject) => {
80
+ fs.mkdirSync(tmpDir, { recursive: true });
81
+
82
+ const download = (downloadUrl, redirectCount = 0) => {
83
+ if (redirectCount > 5) {
84
+ reject(new Error('Too many redirects'));
85
+ return;
86
+ }
87
+
88
+ https.get(downloadUrl, (response) => {
89
+ if (response.statusCode === 302 || response.statusCode === 301) {
90
+ download(response.headers.location, redirectCount + 1);
91
+ return;
92
+ }
93
+
94
+ if (response.statusCode !== 200) {
95
+ reject(new Error(`Download failed: HTTP ${response.statusCode}`));
96
+ return;
97
+ }
98
+
99
+ const file = fs.createWriteStream(archivePath);
100
+ response.pipe(file);
101
+
102
+ file.on('finish', () => {
103
+ file.close();
104
+
105
+ try {
106
+ // Extract the archive
107
+ if (ext === 'zip') {
108
+ execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' });
109
+ } else {
110
+ execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
111
+ }
112
+
113
+ const extractedBinary = path.join(tmpDir, binaryName);
114
+ if (fs.existsSync(extractedBinary)) {
115
+ resolve(extractedBinary);
116
+ } else {
117
+ reject(new Error('Binary not found in archive'));
118
+ }
119
+ } catch (err) {
120
+ reject(new Error(`Failed to extract: ${err.message}`));
121
+ }
122
+ });
123
+
124
+ file.on('error', (err) => {
125
+ fs.unlinkSync(archivePath);
126
+ reject(err);
127
+ });
128
+ }).on('error', reject);
129
+ };
130
+
131
+ download(url);
132
+ });
133
+ }
134
+
12
135
  // Check if Go is already installed locally
13
136
  function checkLocalGo() {
14
137
  try {
@@ -64,13 +187,9 @@ async function downloadGo() {
64
187
  }
65
188
  }
66
189
 
67
- // Build the binary
190
+ // Build the binary from source
68
191
  function buildBinary(goCommand) {
69
- const platform = os.platform();
70
- const arch = os.arch();
71
- const goos = platform === 'win32' ? 'windows' : platform;
72
- const goarch = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : arch;
73
- const binaryName = platform === 'win32' ? 'tanagram.exe' : 'tanagram';
192
+ const { goos, goarch, binaryName } = getPlatformInfo();
74
193
  const binaryPath = path.join(__dirname, 'bin', binaryName);
75
194
 
76
195
  const binDir = path.join(__dirname, 'bin');
@@ -78,7 +197,7 @@ function buildBinary(goCommand) {
78
197
  fs.mkdirSync(binDir, { recursive: true });
79
198
  }
80
199
 
81
- console.log('🔧 Building Tanagram CLI...');
200
+ console.log('🔧 Building Tanagram CLI from source...');
82
201
 
83
202
  try {
84
203
  const ldflags = `-X 'main.Version=${pkg.version}'`;
@@ -92,17 +211,37 @@ function buildBinary(goCommand) {
92
211
  }
93
212
  });
94
213
 
95
- if (platform !== 'win32') {
214
+ if (os.platform() !== 'win32') {
96
215
  fs.chmodSync(binaryPath, '755');
97
216
  }
98
217
 
99
- console.log('✓ Tanagram CLI installed successfully');
218
+ console.log('✓ Tanagram CLI built successfully');
100
219
  return binaryPath;
101
220
  } catch (error) {
102
221
  throw new Error(`Build failed: ${error.message}`);
103
222
  }
104
223
  }
105
224
 
225
+ // Copy prebuilt binary to bin directory
226
+ function installPrebuiltBinary(sourcePath) {
227
+ const { binaryName } = getPlatformInfo();
228
+ const binDir = path.join(__dirname, 'bin');
229
+ const targetPath = path.join(binDir, binaryName);
230
+
231
+ if (!fs.existsSync(binDir)) {
232
+ fs.mkdirSync(binDir, { recursive: true });
233
+ }
234
+
235
+ fs.copyFileSync(sourcePath, targetPath);
236
+
237
+ if (os.platform() !== 'win32') {
238
+ fs.chmodSync(targetPath, '755');
239
+ }
240
+
241
+ console.log('✓ Tanagram CLI installed successfully');
242
+ return targetPath;
243
+ }
244
+
106
245
  function isCIEnvironment() {
107
246
  return (
108
247
  process.env.CI === 'true' ||
@@ -147,23 +286,38 @@ function configureEditorHooks(binaryPath) {
147
286
  // Main installation flow
148
287
  (async () => {
149
288
  try {
150
- // 1. Check if Go is already installed locally
151
- let goCommand = checkLocalGo();
289
+ let binaryPath;
152
290
 
153
- // 2. If not, check if we have a cached go-bin download
154
- if (!goCommand) {
155
- goCommand = checkCachedGo();
156
- }
291
+ // 1. Check for prebuilt binary bundled with npm package
292
+ const prebuiltPath = checkPrebuiltBinary();
293
+ if (prebuiltPath) {
294
+ binaryPath = installPrebuiltBinary(prebuiltPath);
295
+ } else {
296
+ // 2. Try to download prebuilt binary from GitHub releases
297
+ try {
298
+ console.log('Looking for prebuilt binary...');
299
+ const downloadedPath = await downloadPrebuiltBinary();
300
+ binaryPath = installPrebuiltBinary(downloadedPath);
301
+ } catch (downloadErr) {
302
+ console.log(`Prebuilt binary not available: ${downloadErr.message}`);
303
+ console.log('Falling back to building from source...');
157
304
 
158
- // 3. If still no Go, download via go-bin and cache it
159
- if (!goCommand) {
160
- goCommand = await downloadGo();
161
- }
305
+ // 3. Fall back to building from source
306
+ let goCommand = checkLocalGo();
307
+
308
+ if (!goCommand) {
309
+ goCommand = checkCachedGo();
310
+ }
162
311
 
163
- // 4. Build the binary
164
- const binaryPath = buildBinary(goCommand);
312
+ if (!goCommand) {
313
+ goCommand = await downloadGo();
314
+ }
315
+
316
+ binaryPath = buildBinary(goCommand);
317
+ }
318
+ }
165
319
 
166
- // 5. Configure hooks
320
+ // Configure hooks
167
321
  configureEditorHooks(binaryPath);
168
322
 
169
323
  process.exit(0);
package/main.go CHANGED
@@ -222,17 +222,6 @@ func main() {
222
222
  "command": "sync-policies",
223
223
  })
224
224
  err = commands.SyncPolicies()
225
- case "puzzle":
226
- metrics.Track("cli.command.execute", map[string]interface{}{
227
- "command": "puzzle",
228
- })
229
- err := tui.RunPuzzleEditor()
230
- if err != nil {
231
- slog.Error("Puzzle editor failed", "error", err)
232
- exitCode = 1
233
- return
234
- }
235
- return
236
225
  case "version", "-v", "--version":
237
226
  fmt.Println(Version)
238
227
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.18",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,8 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "postinstall": "node install.js",
11
- "test": "go test ./...",
12
- "prepublishOnly": "export $(grep -v '^#' .env.publish | xargs) && node install.js"
11
+ "test": "go test ./..."
13
12
  },
14
13
  "keywords": [
15
14
  "tanagram",
@@ -23,7 +22,8 @@
23
22
  "license": "MIT",
24
23
  "repository": {
25
24
  "type": "git",
26
- "url": "https://github.com/tanagram/cli.git"
25
+ "url": "https://github.com/tanagram/monorepo.git",
26
+ "directory": "cli"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=14.0.0"
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "files": [
35
35
  "bin/tanagram.js",
36
+ "dist/npm/",
36
37
  "api/",
37
38
  "auth/",
38
39
  "checker/",
package/tui/welcome.go CHANGED
@@ -70,19 +70,7 @@ func (m WelcomeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
70
70
 
71
71
  // View renders the UI
72
72
  func (m WelcomeModel) View() string {
73
- // Load and render the tangram logo from config
74
- var logo string
75
-
76
- pieces, err := LoadTanagramConfig("tanagram-config.json")
77
- if err == nil && len(pieces) > 0 {
78
- // Render using the shared renderer
79
- renderer := NewTanagramRenderer(90, 45)
80
- canvas := renderer.RenderPiecesToCanvas(pieces)
81
- logo = renderer.CanvasToString(canvas, pieces)
82
- } else {
83
- // Fallback to simple text if config not found
84
- logo = "TANAGRAM"
85
- }
73
+ logo := "TANAGRAM"
86
74
 
87
75
  // Title style
88
76
  titleStyle := lipgloss.NewStyle().