@tanagram/cli 0.4.11 → 0.4.14
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 +80 -66
- package/api/client.go +12 -2
- package/api/client_test.go +53 -0
- package/commands/config.go +74 -83
- package/commands/config_test.go +185 -44
- package/commands/login.go +2 -2
- package/commands/run.go +21 -23
- package/commands/snapshot_test.go +3 -3
- package/go.mod +2 -1
- package/go.sum +4 -0
- package/main.go +112 -12
- package/package.json +1 -1
- package/utils/process.go +3 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Tanagram
|
|
1
|
+
# Tanagram CLI
|
|
2
2
|
|
|
3
3
|
A lightweight Go CLI that enforces policies from `AGENTS.md` files on your local git changes.
|
|
4
4
|
|
|
@@ -23,8 +23,10 @@ webui/src/Button.tsx:42 - [No hardcoded colors] Don't use hard-coded color value
|
|
|
23
23
|
# 1. Install globally via npm
|
|
24
24
|
npm install -g @tanagram/cli
|
|
25
25
|
|
|
26
|
-
# 2.
|
|
26
|
+
# 2. Automatically add a Claude Code hook
|
|
27
27
|
tanagram config claude
|
|
28
|
+
# and/or if you use Cursor:
|
|
29
|
+
tanagram config cursor
|
|
28
30
|
|
|
29
31
|
# 3. Run tanagram (will prompt for API key on first run)
|
|
30
32
|
tanagram
|
|
@@ -45,28 +47,6 @@ Tanagram uses Claude AI (via Anthropic API) to extract policies from your instru
|
|
|
45
47
|
2. Create an API key in the dashboard
|
|
46
48
|
3. Run `tanagram` and enter your key when prompted
|
|
47
49
|
|
|
48
|
-
### Local Development
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
cd cli
|
|
52
|
-
npm install # Builds the Go binary
|
|
53
|
-
./bin/tanagram
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Install Locally for Testing
|
|
57
|
-
|
|
58
|
-
Install globally from the local directory to test as if it were published:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
cd /Users/molinar/tanagram/cli
|
|
62
|
-
npm install -g .
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Then run from anywhere:
|
|
66
|
-
```bash
|
|
67
|
-
tanagram
|
|
68
|
-
```
|
|
69
|
-
|
|
70
50
|
## Usage
|
|
71
51
|
|
|
72
52
|
```bash
|
|
@@ -113,35 +93,26 @@ tanagram config list
|
|
|
113
93
|
**Manual setup (alternative):**
|
|
114
94
|
If you prefer to manually edit your settings, add this to your `~/.claude/settings.json` (user settings) or `.claude/settings.json` (project settings):
|
|
115
95
|
|
|
116
|
-
```json
|
|
117
|
-
"hooks": {
|
|
118
|
-
"PostToolUse": [
|
|
119
|
-
{
|
|
120
|
-
"matcher": "Edit|Write",
|
|
121
|
-
"hooks": [
|
|
122
|
-
{
|
|
123
|
-
"type": "command",
|
|
124
|
-
"command": "tanagram"
|
|
125
|
-
}
|
|
126
|
-
]
|
|
127
|
-
}
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
For example, your full `settings.json` file might look like this:
|
|
133
|
-
|
|
134
96
|
```json
|
|
135
97
|
{
|
|
136
|
-
"alwaysThinkingEnabled": true,
|
|
137
98
|
"hooks": {
|
|
138
|
-
"
|
|
99
|
+
"SessionStart": [
|
|
139
100
|
{
|
|
140
|
-
"matcher": "Edit|Write",
|
|
141
101
|
"hooks": [
|
|
142
102
|
{
|
|
143
|
-
"
|
|
144
|
-
"
|
|
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"
|
|
145
116
|
}
|
|
146
117
|
]
|
|
147
118
|
}
|
|
@@ -152,6 +123,45 @@ For example, your full `settings.json` file might look like this:
|
|
|
152
123
|
|
|
153
124
|
If you have existing hooks, you can merge this hook into your existing config.
|
|
154
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
|
+
|
|
155
165
|
|
|
156
166
|
## How It Works
|
|
157
167
|
|
|
@@ -171,24 +181,6 @@ Policies are cached in `.tanagram/cache.gob` at your git repository root. Add th
|
|
|
171
181
|
.tanagram/
|
|
172
182
|
```
|
|
173
183
|
|
|
174
|
-
## Fully LLM-Based Architecture
|
|
175
|
-
|
|
176
|
-
Tanagram uses **100% LLM-powered** policy extraction and enforcement:
|
|
177
|
-
|
|
178
|
-
### Extraction Phase
|
|
179
|
-
Claude AI extracts **ALL** policies from instruction files:
|
|
180
|
-
- No classification needed (no MUST_NOT_USE, MUST_USE, etc.)
|
|
181
|
-
- No regex pattern generation
|
|
182
|
-
- Simple: Just extract policy names and descriptions
|
|
183
|
-
- Fast: Simpler prompts = faster responses
|
|
184
|
-
|
|
185
|
-
### Detection Phase
|
|
186
|
-
Claude AI analyzes code changes against all policies:
|
|
187
|
-
- **Semantic understanding** - Not just pattern matching
|
|
188
|
-
- **Context-aware** - Understands code intent and structure
|
|
189
|
-
- **Language-agnostic** - Works with any programming language
|
|
190
|
-
- **Detailed reasoning** - Explains why code violates each policy
|
|
191
|
-
|
|
192
184
|
### What Can Be Enforced
|
|
193
185
|
|
|
194
186
|
**Everything!** Because the LLM reads and understands code like a human:
|
|
@@ -210,7 +202,7 @@ Claude AI analyzes code changes against all policies:
|
|
|
210
202
|
- Won't flag Go code for missing Python type hints
|
|
211
203
|
- Understands JavaScript !== Python !== Go
|
|
212
204
|
|
|
213
|
-
|
|
205
|
+
### Exit Codes
|
|
214
206
|
|
|
215
207
|
- `0` - No violations found
|
|
216
208
|
- `2` - Violations found (triggers Claude Code automatic fix behavior)
|
|
@@ -246,6 +238,28 @@ Then run `tanagram` to enforce them locally!
|
|
|
246
238
|
|
|
247
239
|
**Note:** For `.mdc` files, Tanagram extracts policies from the markdown content only (YAML frontmatter is used by Cursor and ignored during policy extraction).
|
|
248
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
|
+
|
|
249
263
|
---
|
|
250
264
|
|
|
251
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
|
package/api/client.go
CHANGED
|
@@ -5,7 +5,9 @@ import (
|
|
|
5
5
|
"fmt"
|
|
6
6
|
"io"
|
|
7
7
|
"net/http"
|
|
8
|
+
"net/url"
|
|
8
9
|
"os"
|
|
10
|
+
"strings"
|
|
9
11
|
"time"
|
|
10
12
|
|
|
11
13
|
"github.com/tanagram/cli/auth"
|
|
@@ -50,8 +52,16 @@ type PolicyListResponse struct {
|
|
|
50
52
|
|
|
51
53
|
// getAPIBaseURL returns the API base URL
|
|
52
54
|
func getAPIBaseURL() string {
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
+
if urlStr := os.Getenv("TANAGRAM_WEB_HOSTNAME"); urlStr != "" {
|
|
56
|
+
u, err := url.Parse(urlStr)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return urlStr
|
|
59
|
+
}
|
|
60
|
+
if !strings.HasPrefix(u.Host, "api-") {
|
|
61
|
+
u.Host = "api-" + u.Host
|
|
62
|
+
return u.String()
|
|
63
|
+
}
|
|
64
|
+
return urlStr
|
|
55
65
|
}
|
|
56
66
|
return "https://api.tanagram.ai"
|
|
57
67
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestGetAPIBaseURL(t *testing.T) {
|
|
9
|
+
// Save original env var
|
|
10
|
+
orig := os.Getenv("TANAGRAM_WEB_HOSTNAME")
|
|
11
|
+
defer os.Setenv("TANAGRAM_WEB_HOSTNAME", orig)
|
|
12
|
+
|
|
13
|
+
tests := []struct {
|
|
14
|
+
name string
|
|
15
|
+
envValue string
|
|
16
|
+
want string
|
|
17
|
+
}{
|
|
18
|
+
{
|
|
19
|
+
name: "default",
|
|
20
|
+
envValue: "",
|
|
21
|
+
want: "https://api.tanagram.ai",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "with api- prefix",
|
|
25
|
+
envValue: "https://api-runway.tanagram.ai",
|
|
26
|
+
want: "https://api-runway.tanagram.ai",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "without api- prefix",
|
|
30
|
+
envValue: "https://runway.tanagram.ai",
|
|
31
|
+
want: "https://api-runway.tanagram.ai",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "http without api- prefix",
|
|
35
|
+
envValue: "http://localhost:8080",
|
|
36
|
+
want: "http://api-localhost:8080",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for _, tt := range tests {
|
|
41
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
42
|
+
if tt.envValue == "" {
|
|
43
|
+
os.Unsetenv("TANAGRAM_WEB_HOSTNAME")
|
|
44
|
+
} else {
|
|
45
|
+
os.Setenv("TANAGRAM_WEB_HOSTNAME", tt.envValue)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if got := getAPIBaseURL(); got != tt.want {
|
|
49
|
+
t.Errorf("getAPIBaseURL() = %v, want %v", got, tt.want)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
package/commands/config.go
CHANGED
|
@@ -22,36 +22,36 @@ func ConfigClaude(settingsPath string) error {
|
|
|
22
22
|
settings["hooks"] = hooks
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// Check if
|
|
26
|
-
|
|
27
|
-
if !
|
|
28
|
-
|
|
25
|
+
// Check if SessionStart exists
|
|
26
|
+
sessionStart, sessionStartExist := hooks["SessionStart"].([]interface{})
|
|
27
|
+
if !sessionStartExist {
|
|
28
|
+
sessionStart = []interface{}{}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Check if
|
|
32
|
-
|
|
33
|
-
if !
|
|
34
|
-
|
|
31
|
+
// Check if Stop exists
|
|
32
|
+
stopHooks, stopHooksExist := hooks["Stop"].([]interface{})
|
|
33
|
+
if !stopHooksExist {
|
|
34
|
+
stopHooks = []interface{}{}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// Check if tanagram hooks already exist
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
sessionStartHookExists := false
|
|
39
|
+
stopHookExists := false
|
|
40
40
|
|
|
41
|
-
for _, hook := range
|
|
41
|
+
for _, hook := range sessionStart {
|
|
42
42
|
hookMap, ok := hook.(map[string]interface{})
|
|
43
43
|
if !ok {
|
|
44
44
|
continue
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
if matcher, ok := hookMap["matcher"].(string); ok && matcher == "
|
|
47
|
+
if matcher, ok := hookMap["matcher"].(string); ok && matcher == "startup|clear" {
|
|
48
48
|
innerHooks, ok := hookMap["hooks"].([]interface{})
|
|
49
49
|
if ok {
|
|
50
50
|
for _, innerHook := range innerHooks {
|
|
51
51
|
innerHookMap, ok := innerHook.(map[string]interface{})
|
|
52
52
|
if ok {
|
|
53
|
-
if cmd, ok := innerHookMap["command"].(string); ok && cmd
|
|
54
|
-
|
|
53
|
+
if cmd, ok := innerHookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram snapshot") {
|
|
54
|
+
sessionStartHookExists = true
|
|
55
55
|
break
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -60,37 +60,34 @@ func ConfigClaude(settingsPath string) error {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
for _, hook := range
|
|
63
|
+
for _, hook := range stopHooks {
|
|
64
64
|
hookMap, ok := hook.(map[string]interface{})
|
|
65
65
|
if !ok {
|
|
66
66
|
continue
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if ok {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
break
|
|
78
|
-
}
|
|
69
|
+
innerHooks, ok := hookMap["hooks"].([]interface{})
|
|
70
|
+
if ok {
|
|
71
|
+
for _, innerHook := range innerHooks {
|
|
72
|
+
innerHookMap, ok := innerHook.(map[string]interface{})
|
|
73
|
+
if ok {
|
|
74
|
+
if cmd, ok := innerHookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram") {
|
|
75
|
+
stopHookExists = true
|
|
76
|
+
break
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
|
-
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
if
|
|
83
|
+
if sessionStartHookExists && stopHookExists {
|
|
87
84
|
fmt.Printf("✓ Tanagram hooks are already configured in %s\n", settingsPath)
|
|
88
85
|
return nil
|
|
89
86
|
}
|
|
90
87
|
|
|
91
|
-
if !
|
|
92
|
-
|
|
93
|
-
"matcher": "
|
|
88
|
+
if !sessionStartHookExists {
|
|
89
|
+
startHook := map[string]interface{}{
|
|
90
|
+
"matcher": "startup|clear",
|
|
94
91
|
"hooks": []interface{}{
|
|
95
92
|
map[string]interface{}{
|
|
96
93
|
"type": "command",
|
|
@@ -98,13 +95,12 @@ func ConfigClaude(settingsPath string) error {
|
|
|
98
95
|
},
|
|
99
96
|
},
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
hooks["
|
|
98
|
+
sessionStart = append(sessionStart, startHook)
|
|
99
|
+
hooks["SessionStart"] = sessionStart
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
if !
|
|
106
|
-
|
|
107
|
-
"matcher": "Edit|Write",
|
|
102
|
+
if !stopHookExists {
|
|
103
|
+
stopHook := map[string]interface{}{
|
|
108
104
|
"hooks": []interface{}{
|
|
109
105
|
map[string]interface{}{
|
|
110
106
|
"type": "command",
|
|
@@ -112,8 +108,8 @@ func ConfigClaude(settingsPath string) error {
|
|
|
112
108
|
},
|
|
113
109
|
},
|
|
114
110
|
}
|
|
115
|
-
|
|
116
|
-
hooks["
|
|
111
|
+
stopHooks = append(stopHooks, stopHook)
|
|
112
|
+
hooks["Stop"] = stopHooks
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
if err := saveConfig(settingsPath, settings); err != nil {
|
|
@@ -122,8 +118,8 @@ func ConfigClaude(settingsPath string) error {
|
|
|
122
118
|
|
|
123
119
|
fmt.Printf("✓ Tanagram hooks added to %s\n", settingsPath)
|
|
124
120
|
fmt.Println("\nClaude Code will now:")
|
|
125
|
-
fmt.Println(" - Snapshot file state
|
|
126
|
-
fmt.Println(" - Check only Claude's changes after
|
|
121
|
+
fmt.Println(" - Snapshot file state at session start (SessionStart)")
|
|
122
|
+
fmt.Println(" - Check only Claude's changes after agent completes (Stop)")
|
|
127
123
|
fmt.Println(" - Send policy violations to Claude for automatic fixing")
|
|
128
124
|
fmt.Println("\nThis prevents false positives from user-written code!")
|
|
129
125
|
|
|
@@ -172,7 +168,7 @@ func ConfigCursor(hooksPath string) error {
|
|
|
172
168
|
continue
|
|
173
169
|
}
|
|
174
170
|
|
|
175
|
-
if cmd, ok := hookMap["command"].(string); ok && cmd
|
|
171
|
+
if cmd, ok := hookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram snapshot") {
|
|
176
172
|
beforeSubmitHookExists = true
|
|
177
173
|
break
|
|
178
174
|
}
|
|
@@ -184,7 +180,7 @@ func ConfigCursor(hooksPath string) error {
|
|
|
184
180
|
continue
|
|
185
181
|
}
|
|
186
182
|
|
|
187
|
-
if cmd, ok := hookMap["command"].(string); ok && cmd
|
|
183
|
+
if cmd, ok := hookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram") {
|
|
188
184
|
stopHookExists = true
|
|
189
185
|
break
|
|
190
186
|
}
|
|
@@ -264,13 +260,13 @@ func ConfigList() error {
|
|
|
264
260
|
|
|
265
261
|
// ClaudeHookStatus represents the status of a hook configuration
|
|
266
262
|
type ClaudeHookStatus struct {
|
|
267
|
-
FileExists
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
IsUpToDate
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Error
|
|
263
|
+
FileExists bool
|
|
264
|
+
SessionStartHookExists bool
|
|
265
|
+
StopHookExists bool
|
|
266
|
+
IsUpToDate bool
|
|
267
|
+
SessionStartCommand string
|
|
268
|
+
StopCommand string
|
|
269
|
+
Error error
|
|
274
270
|
}
|
|
275
271
|
|
|
276
272
|
// CursorHookStatus represents the status of a Cursor hook configuration
|
|
@@ -287,13 +283,13 @@ type CursorHookStatus struct {
|
|
|
287
283
|
// checkClaudeHookStatus checks the status of Tanagram hook in a settings file
|
|
288
284
|
func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
|
|
289
285
|
status := ClaudeHookStatus{
|
|
290
|
-
FileExists:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
IsUpToDate:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
Error:
|
|
286
|
+
FileExists: false,
|
|
287
|
+
SessionStartHookExists: false,
|
|
288
|
+
StopHookExists: false,
|
|
289
|
+
IsUpToDate: false,
|
|
290
|
+
SessionStartCommand: "",
|
|
291
|
+
StopCommand: "",
|
|
292
|
+
Error: nil,
|
|
297
293
|
}
|
|
298
294
|
|
|
299
295
|
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
|
@@ -313,15 +309,15 @@ func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
|
|
|
313
309
|
return status
|
|
314
310
|
}
|
|
315
311
|
|
|
316
|
-
if
|
|
317
|
-
for _, hook := range
|
|
312
|
+
if sessionStart, ok := hooks["SessionStart"].([]interface{}); ok {
|
|
313
|
+
for _, hook := range sessionStart {
|
|
318
314
|
hookMap, ok := hook.(map[string]interface{})
|
|
319
315
|
if !ok {
|
|
320
316
|
continue
|
|
321
317
|
}
|
|
322
318
|
|
|
323
319
|
matcher, ok := hookMap["matcher"].(string)
|
|
324
|
-
if !ok || matcher != "
|
|
320
|
+
if !ok || matcher != "startup|clear" {
|
|
325
321
|
continue
|
|
326
322
|
}
|
|
327
323
|
|
|
@@ -338,25 +334,20 @@ func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
|
|
|
338
334
|
|
|
339
335
|
cmd, cmdOk := innerHookMap["command"].(string)
|
|
340
336
|
if cmdOk && strings.Contains(cmd, "tanagram snapshot") {
|
|
341
|
-
status.
|
|
342
|
-
status.
|
|
337
|
+
status.SessionStartHookExists = true
|
|
338
|
+
status.SessionStartCommand = cmd
|
|
343
339
|
}
|
|
344
340
|
}
|
|
345
341
|
}
|
|
346
342
|
}
|
|
347
343
|
|
|
348
|
-
if
|
|
349
|
-
for _, hook := range
|
|
344
|
+
if stopHooks, ok := hooks["Stop"].([]interface{}); ok {
|
|
345
|
+
for _, hook := range stopHooks {
|
|
350
346
|
hookMap, ok := hook.(map[string]interface{})
|
|
351
347
|
if !ok {
|
|
352
348
|
continue
|
|
353
349
|
}
|
|
354
350
|
|
|
355
|
-
matcher, ok := hookMap["matcher"].(string)
|
|
356
|
-
if !ok || matcher != "Edit|Write" {
|
|
357
|
-
continue
|
|
358
|
-
}
|
|
359
|
-
|
|
360
351
|
innerHooks, ok := hookMap["hooks"].([]interface{})
|
|
361
352
|
if !ok {
|
|
362
353
|
continue
|
|
@@ -371,15 +362,15 @@ func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
|
|
|
371
362
|
cmd, cmdOk := innerHookMap["command"].(string)
|
|
372
363
|
hookType, typeOk := innerHookMap["type"].(string)
|
|
373
364
|
|
|
374
|
-
if cmdOk && cmd
|
|
375
|
-
status.
|
|
376
|
-
status.
|
|
365
|
+
if cmdOk && strings.HasSuffix(cmd, "tanagram") && typeOk && hookType == "command" {
|
|
366
|
+
status.StopHookExists = true
|
|
367
|
+
status.StopCommand = cmd
|
|
377
368
|
}
|
|
378
369
|
}
|
|
379
370
|
}
|
|
380
371
|
}
|
|
381
372
|
|
|
382
|
-
if status.
|
|
373
|
+
if status.SessionStartHookExists && status.StopHookExists {
|
|
383
374
|
status.IsUpToDate = true
|
|
384
375
|
}
|
|
385
376
|
|
|
@@ -423,7 +414,7 @@ func checkCursorHookStatus(hooksPath string) CursorHookStatus {
|
|
|
423
414
|
}
|
|
424
415
|
|
|
425
416
|
cmd, cmdOk := hookMap["command"].(string)
|
|
426
|
-
if cmdOk && cmd
|
|
417
|
+
if cmdOk && strings.HasSuffix(cmd, "tanagram snapshot") {
|
|
427
418
|
status.BeforeSubmitExists = true
|
|
428
419
|
status.BeforeSubmitCommand = cmd
|
|
429
420
|
}
|
|
@@ -438,7 +429,7 @@ func checkCursorHookStatus(hooksPath string) CursorHookStatus {
|
|
|
438
429
|
}
|
|
439
430
|
|
|
440
431
|
cmd, cmdOk := hookMap["command"].(string)
|
|
441
|
-
if cmdOk && cmd
|
|
432
|
+
if cmdOk && strings.HasSuffix(cmd, "tanagram") {
|
|
442
433
|
status.StopExists = true
|
|
443
434
|
status.StopCommand = cmd
|
|
444
435
|
}
|
|
@@ -465,7 +456,7 @@ func printClaudeHookStatus(status ClaudeHookStatus, path string) {
|
|
|
465
456
|
return
|
|
466
457
|
}
|
|
467
458
|
|
|
468
|
-
if !status.
|
|
459
|
+
if !status.SessionStartHookExists && !status.StopHookExists {
|
|
469
460
|
fmt.Printf(" ○ Not configured\n")
|
|
470
461
|
fmt.Printf(" → Run: tanagram config claude\n")
|
|
471
462
|
return
|
|
@@ -473,20 +464,20 @@ func printClaudeHookStatus(status ClaudeHookStatus, path string) {
|
|
|
473
464
|
|
|
474
465
|
if status.IsUpToDate {
|
|
475
466
|
fmt.Printf(" ✓ Configured and up to date\n")
|
|
476
|
-
fmt.Printf("
|
|
477
|
-
fmt.Printf("
|
|
467
|
+
fmt.Printf(" SessionStart: %s\n", status.SessionStartCommand)
|
|
468
|
+
fmt.Printf(" Stop: %s\n", status.StopCommand)
|
|
478
469
|
fmt.Printf(" Location: %s\n", path)
|
|
479
470
|
} else {
|
|
480
471
|
fmt.Printf(" ⚠ Configured but incomplete\n")
|
|
481
|
-
if status.
|
|
482
|
-
fmt.Printf(" ✓
|
|
472
|
+
if status.SessionStartHookExists {
|
|
473
|
+
fmt.Printf(" ✓ SessionStart: %s\n", status.SessionStartCommand)
|
|
483
474
|
} else {
|
|
484
|
-
fmt.Printf(" ✗
|
|
475
|
+
fmt.Printf(" ✗ SessionStart: missing\n")
|
|
485
476
|
}
|
|
486
|
-
if status.
|
|
487
|
-
fmt.Printf(" ✓
|
|
477
|
+
if status.StopHookExists {
|
|
478
|
+
fmt.Printf(" ✓ Stop: %s\n", status.StopCommand)
|
|
488
479
|
} else {
|
|
489
|
-
fmt.Printf(" ✗
|
|
480
|
+
fmt.Printf(" ✗ Stop: missing\n")
|
|
490
481
|
}
|
|
491
482
|
fmt.Printf(" → Run: tanagram config claude\n")
|
|
492
483
|
}
|
|
@@ -550,7 +541,7 @@ func ensureClaudeConfigured() error {
|
|
|
550
541
|
}
|
|
551
542
|
|
|
552
543
|
// If file doesn't exist or hooks aren't configured, set them up
|
|
553
|
-
if !status.FileExists || !status.
|
|
544
|
+
if !status.FileExists || !status.SessionStartHookExists || !status.StopHookExists {
|
|
554
545
|
fmt.Println("Setting up Claude Code integration...")
|
|
555
546
|
if err := ConfigClaude(settingsPath); err != nil {
|
|
556
547
|
// Don't fail the command if hook setup fails - just warn
|