@tanagram/cli 0.1.25 → 0.1.27

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.
@@ -10,7 +10,7 @@ import (
10
10
  )
11
11
 
12
12
  // CheckChangesWithLLM checks code changes against ALL policies using LLM
13
- func CheckChangesWithLLM(ctx context.Context, changes []git.ChangedLine, policies []parser.Policy) []Violation {
13
+ func CheckChangesWithLLM(ctx context.Context, changes []git.ChangedLine, policies []parser.Policy, apiKey string) []Violation {
14
14
  if len(policies) == 0 {
15
15
  return []Violation{}
16
16
  }
@@ -23,7 +23,7 @@ func CheckChangesWithLLM(ctx context.Context, changes []git.ChangedLine, policie
23
23
 
24
24
  var allViolations []Violation
25
25
  for file, fileChanges := range changesByFile {
26
- violations := checkFileWithLLM(ctx, file, fileChanges, policies, policyMap)
26
+ violations := checkFileWithLLM(ctx, file, fileChanges, policies, policyMap, apiKey)
27
27
  allViolations = append(allViolations, violations...)
28
28
  }
29
29
 
@@ -49,12 +49,12 @@ func groupChangesByFile(changes []git.ChangedLine) map[string][]git.ChangedLine
49
49
  }
50
50
 
51
51
  // checkFileWithLLM checks a single file's changes using the LLM
52
- func checkFileWithLLM(ctx context.Context, file string, changes []git.ChangedLine, policies []parser.Policy, policyMap map[string]parser.Policy) []Violation {
52
+ func checkFileWithLLM(ctx context.Context, file string, changes []git.ChangedLine, policies []parser.Policy, policyMap map[string]parser.Policy, apiKey string) []Violation {
53
53
  // Format changes for LLM
54
54
  codeChanges := formatChangesForLLM(changes)
55
55
 
56
56
  // Call LLM to check violations
57
- checks, err := CheckViolations(ctx, file, codeChanges, policies)
57
+ checks, err := CheckViolations(ctx, file, codeChanges, policies, apiKey)
58
58
  if err != nil {
59
59
  // Log error but don't fail the whole check
60
60
  fmt.Printf("Warning: LLM check failed for %s: %v\n", file, err)
@@ -26,14 +26,14 @@ type CheckResult struct {
26
26
  }
27
27
 
28
28
  // CheckChanges checks all changed lines against policies using LLM-based detection
29
- func CheckChanges(ctx context.Context, changes []git.ChangedLine, policies []parser.Policy) *CheckResult {
29
+ func CheckChanges(ctx context.Context, changes []git.ChangedLine, policies []parser.Policy, apiKey string) *CheckResult {
30
30
  result := &CheckResult{
31
31
  Violations: []Violation{},
32
32
  TotalChecked: len(changes),
33
33
  }
34
34
 
35
35
  // Use LLM-based checking for all policies
36
- llmViolations := CheckChangesWithLLM(ctx, changes, policies)
36
+ llmViolations := CheckChangesWithLLM(ctx, changes, policies, apiKey)
37
37
  result.Violations = append(result.Violations, llmViolations...)
38
38
 
39
39
  return result
@@ -24,12 +24,12 @@ type ViolationCheckResponse struct {
24
24
 
25
25
  // CheckViolations uses LLM to check if code changes violate any policies
26
26
  // Returns a list of violation checks with policy names and reasons
27
- func CheckViolations(ctx context.Context, file string, codeChanges string, policies []parser.Policy) ([]ViolationCheck, error) {
27
+ func CheckViolations(ctx context.Context, file string, codeChanges string, policies []parser.Policy, apiKey string) ([]ViolationCheck, error) {
28
28
  if len(policies) == 0 {
29
29
  return []ViolationCheck{}, nil
30
30
  }
31
31
 
32
- client, err := llm.NewClient()
32
+ client, err := llm.NewClient(apiKey)
33
33
  if err != nil {
34
34
  return nil, err
35
35
  }
package/commands/run.go CHANGED
@@ -9,6 +9,7 @@ import (
9
9
  "time"
10
10
 
11
11
  "github.com/tanagram/cli/checker"
12
+ "github.com/tanagram/cli/config"
12
13
  "github.com/tanagram/cli/extractor"
13
14
  "github.com/tanagram/cli/git"
14
15
  "github.com/tanagram/cli/metrics"
@@ -71,6 +72,12 @@ func Run() error {
71
72
 
72
73
  // Auto-sync only the files that changed
73
74
  if len(filesToSync) > 0 {
75
+ // Get API key once upfront before parallel processing
76
+ apiKey, err := config.GetAPIKey()
77
+ if err != nil {
78
+ return fmt.Errorf("failed to get API key: %w", err)
79
+ }
80
+
74
81
  fmt.Printf("\nSyncing policies with LLM (processing %d changed file(s) in parallel)...\n", len(filesToSync))
75
82
 
76
83
  syncStart := time.Now()
@@ -117,7 +124,7 @@ func Run() error {
117
124
  go func(file string) {
118
125
  defer wg.Done()
119
126
  relPath, _ := filepath.Rel(gitRoot, file)
120
- policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
127
+ policies, err := extractor.ExtractPoliciesFromFile(ctx, file, apiKey)
121
128
  results <- syncResult{file, relPath, policies, err}
122
129
  }(file)
123
130
  }
@@ -204,10 +211,16 @@ func Run() error {
204
211
 
205
212
  fmt.Printf("Scanning %d changed lines...\n\n", len(diffResult.Changes))
206
213
 
214
+ // Get API key once upfront before checking
215
+ apiKey, err := config.GetAPIKey()
216
+ if err != nil {
217
+ return fmt.Errorf("failed to get API key: %w", err)
218
+ }
219
+
207
220
  // Check changes against policies (both regex and LLM-based)
208
221
  ctx := context.Background()
209
222
  checkStart := time.Now()
210
- result := checker.CheckChanges(ctx, diffResult.Changes, policies)
223
+ result := checker.CheckChanges(ctx, diffResult.Changes, policies, apiKey)
211
224
  checkDuration := time.Since(checkStart)
212
225
 
213
226
  // Track policy check results (similar to policy.execute.result in github-app)
package/commands/sync.go CHANGED
@@ -8,6 +8,7 @@ import (
8
8
  "sync"
9
9
  "time"
10
10
 
11
+ "github.com/tanagram/cli/config"
11
12
  "github.com/tanagram/cli/extractor"
12
13
  "github.com/tanagram/cli/parser"
13
14
  "github.com/tanagram/cli/storage"
@@ -15,6 +16,12 @@ import (
15
16
 
16
17
  // Sync manually syncs all instruction files to the cache
17
18
  func Sync() error {
19
+ // Get API key first before doing any work
20
+ apiKey, err := getAPIKey()
21
+ if err != nil {
22
+ return err
23
+ }
24
+
18
25
  // Find git root
19
26
  gitRoot, err := storage.FindGitRoot()
20
27
  if err != nil {
@@ -103,7 +110,7 @@ func Sync() error {
103
110
  go func(file string) {
104
111
  defer wg.Done()
105
112
  relPath, _ := filepath.Rel(gitRoot, file)
106
- policies, err := extractor.ExtractPoliciesFromFile(ctx, file)
113
+ policies, err := extractor.ExtractPoliciesFromFile(ctx, file, apiKey)
107
114
  results <- syncResult{file, relPath, policies, err}
108
115
  }(file)
109
116
  }
@@ -223,3 +230,12 @@ func FindInstructionFiles(gitRoot string) ([]string, error) {
223
230
 
224
231
  return files, nil
225
232
  }
233
+
234
+ // getAPIKey retrieves the API key once upfront before parallel processing
235
+ func getAPIKey() (string, error) {
236
+ apiKey, err := config.GetAPIKey()
237
+ if err != nil {
238
+ return "", fmt.Errorf("failed to get API key: %w", err)
239
+ }
240
+ return apiKey, nil
241
+ }
@@ -0,0 +1,155 @@
1
+ package config
2
+
3
+ import (
4
+ "bufio"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+
12
+ "github.com/anthropics/anthropic-sdk-go"
13
+ "github.com/anthropics/anthropic-sdk-go/option"
14
+ )
15
+
16
+ const (
17
+ configDir = ".tanagram"
18
+ configFile = "config.json"
19
+ )
20
+
21
+ // Config represents the application configuration
22
+ type Config struct {
23
+ AnthropicAPIKey string `json:"anthropic_api_key"`
24
+ }
25
+
26
+ // GetConfigPath returns the full path to the config file
27
+ func GetConfigPath() (string, error) {
28
+ home, err := os.UserHomeDir()
29
+ if err != nil {
30
+ return "", fmt.Errorf("failed to get user home directory: %w", err)
31
+ }
32
+ return filepath.Join(home, configDir, configFile), nil
33
+ }
34
+
35
+ // Load reads the config from disk
36
+ func Load() (*Config, error) {
37
+ configPath, err := GetConfigPath()
38
+ if err != nil {
39
+ return nil, err
40
+ }
41
+
42
+ // If config doesn't exist, return empty config
43
+ if _, err := os.Stat(configPath); os.IsNotExist(err) {
44
+ return &Config{}, nil
45
+ }
46
+
47
+ data, err := os.ReadFile(configPath)
48
+ if err != nil {
49
+ return nil, fmt.Errorf("failed to read config file: %w", err)
50
+ }
51
+
52
+ var cfg Config
53
+ if err := json.Unmarshal(data, &cfg); err != nil {
54
+ return nil, fmt.Errorf("failed to parse config file: %w", err)
55
+ }
56
+
57
+ return &cfg, nil
58
+ }
59
+
60
+ // Save writes the config to disk
61
+ func Save(cfg *Config) error {
62
+ configPath, err := GetConfigPath()
63
+ if err != nil {
64
+ return err
65
+ }
66
+
67
+ // Ensure config directory exists
68
+ configDir := filepath.Dir(configPath)
69
+ if err := os.MkdirAll(configDir, 0755); err != nil {
70
+ return fmt.Errorf("failed to create config directory: %w", err)
71
+ }
72
+
73
+ data, err := json.MarshalIndent(cfg, "", " ")
74
+ if err != nil {
75
+ return fmt.Errorf("failed to marshal config: %w", err)
76
+ }
77
+
78
+ if err := os.WriteFile(configPath, data, 0600); err != nil {
79
+ return fmt.Errorf("failed to write config file: %w", err)
80
+ }
81
+
82
+ return nil
83
+ }
84
+
85
+ // GetAPIKey retrieves the Anthropic API key from config
86
+ // If not found, prompts the user and saves it to config
87
+ func GetAPIKey() (string, error) {
88
+ // Check config file
89
+ cfg, err := Load()
90
+ if err != nil {
91
+ return "", fmt.Errorf("failed to load config: %w", err)
92
+ }
93
+
94
+ if cfg.AnthropicAPIKey != "" {
95
+ return cfg.AnthropicAPIKey, nil
96
+ }
97
+
98
+ // Prompt user for API key
99
+ fmt.Println("ANTHROPIC_API_KEY not found.")
100
+ fmt.Print("Please enter your Anthropic API key: ")
101
+
102
+ reader := bufio.NewReader(os.Stdin)
103
+ apiKey, err := reader.ReadString('\n')
104
+ if err != nil {
105
+ return "", fmt.Errorf("failed to read API key: %w", err)
106
+ }
107
+
108
+ apiKey = strings.TrimSpace(apiKey)
109
+ if apiKey == "" {
110
+ return "", fmt.Errorf("API key cannot be empty")
111
+ }
112
+
113
+ // Validate the API key before saving
114
+ fmt.Print("Validating API key... ")
115
+ if err := ValidateAPIKey(apiKey); err != nil {
116
+ fmt.Println("✗")
117
+ return "", fmt.Errorf("invalid API key: %w", err)
118
+ }
119
+ fmt.Println("✓")
120
+
121
+ // Save to config
122
+ cfg.AnthropicAPIKey = apiKey
123
+ if err := Save(cfg); err != nil {
124
+ return "", fmt.Errorf("failed to save config: %w", err)
125
+ }
126
+
127
+ configPath, _ := GetConfigPath()
128
+ fmt.Printf("API key saved to %s\n", configPath)
129
+
130
+ return apiKey, nil
131
+ }
132
+
133
+ // ValidateAPIKey checks if the API key is valid by making a test API call
134
+ func ValidateAPIKey(apiKey string) error {
135
+ client := anthropic.NewClient(option.WithAPIKey(apiKey))
136
+
137
+ ctx := context.Background()
138
+ _, err := client.Messages.New(ctx, anthropic.MessageNewParams{
139
+ MaxTokens: 10,
140
+ Messages: []anthropic.MessageParam{
141
+ anthropic.NewUserMessage(anthropic.NewTextBlock("test")),
142
+ },
143
+ Model: anthropic.ModelClaudeHaiku4_5_20251001,
144
+ })
145
+
146
+ if err != nil {
147
+ // Check if it's an authentication error
148
+ if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "authentication_error") {
149
+ return fmt.Errorf("authentication failed - please check your API key")
150
+ }
151
+ return err
152
+ }
153
+
154
+ return nil
155
+ }
@@ -23,8 +23,8 @@ type ExtractorResponse struct {
23
23
  }
24
24
 
25
25
  // ExtractPolicies uses LLM to extract policies from instruction file content
26
- func ExtractPolicies(ctx context.Context, content string) ([]parser.Policy, error) {
27
- client, err := llm.NewClient()
26
+ func ExtractPolicies(ctx context.Context, content string, apiKey string) ([]parser.Policy, error) {
27
+ client, err := llm.NewClient(apiKey)
28
28
  if err != nil {
29
29
  return nil, err
30
30
  }
package/extractor/file.go CHANGED
@@ -11,7 +11,7 @@ import (
11
11
  )
12
12
 
13
13
  // ExtractPoliciesFromFile reads a file and extracts policies using LLM
14
- func ExtractPoliciesFromFile(ctx context.Context, filePath string) ([]parser.Policy, error) {
14
+ func ExtractPoliciesFromFile(ctx context.Context, filePath string, apiKey string) ([]parser.Policy, error) {
15
15
  content, err := os.ReadFile(filePath)
16
16
  if err != nil {
17
17
  return nil, fmt.Errorf("failed to read file: %w", err)
@@ -24,5 +24,5 @@ func ExtractPoliciesFromFile(ctx context.Context, filePath string) ([]parser.Pol
24
24
  contentStr = parser.StripMDCFrontmatter(contentStr)
25
25
  }
26
26
 
27
- return ExtractPolicies(ctx, contentStr)
27
+ return ExtractPolicies(ctx, contentStr, apiKey)
28
28
  }
package/go.mod CHANGED
@@ -1,15 +1,34 @@
1
1
  module github.com/tanagram/cli
2
2
 
3
- go 1.23.0
3
+ go 1.24.0
4
4
 
5
5
  require github.com/anthropics/anthropic-sdk-go v1.17.0
6
6
 
7
7
  require (
8
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
9
+ github.com/charmbracelet/bubbletea v1.3.10 // indirect
10
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
11
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
12
+ github.com/charmbracelet/x/ansi v0.10.1 // indirect
13
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
14
+ github.com/charmbracelet/x/term v0.2.1 // indirect
15
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
8
16
  github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
17
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
18
+ github.com/mattn/go-isatty v0.0.20 // indirect
19
+ github.com/mattn/go-localereader v0.0.1 // indirect
20
+ github.com/mattn/go-runewidth v0.0.16 // indirect
21
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
22
+ github.com/muesli/cancelreader v0.2.2 // indirect
23
+ github.com/muesli/termenv v0.16.0 // indirect
9
24
  github.com/posthog/posthog-go v1.6.12 // indirect
25
+ github.com/rivo/uniseg v0.4.7 // indirect
10
26
  github.com/stretchr/testify v1.10.0 // indirect
11
27
  github.com/tidwall/gjson v1.18.0 // indirect
12
28
  github.com/tidwall/match v1.1.1 // indirect
13
29
  github.com/tidwall/pretty v1.2.1 // indirect
14
30
  github.com/tidwall/sjson v1.2.5 // indirect
31
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
32
+ golang.org/x/sys v0.36.0 // indirect
33
+ golang.org/x/text v0.27.0 // indirect
15
34
  )
package/go.sum CHANGED
@@ -1,13 +1,46 @@
1
1
  github.com/anthropics/anthropic-sdk-go v1.17.0 h1:BwK8ApcmaAUkvZTiQE0yi3R9XneEFskDIjLTmOAFZxQ=
2
2
  github.com/anthropics/anthropic-sdk-go v1.17.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
3
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5
+ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
6
+ github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
7
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
8
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
9
+ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
10
+ github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
11
+ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
12
+ github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
13
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
14
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
15
+ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
16
+ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
3
17
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4
18
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
20
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
5
21
  github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
6
22
  github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
23
+ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
24
+ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
25
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
26
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
27
+ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
28
+ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
29
+ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
30
+ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
31
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
32
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
33
+ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
34
+ github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
35
+ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
36
+ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
7
37
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8
38
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9
39
  github.com/posthog/posthog-go v1.6.12 h1:rsOBL/YdMfLJtOVjKJLgdzYmvaL3aIW6IVbAteSe+aI=
10
40
  github.com/posthog/posthog-go v1.6.12/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
41
+ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
42
+ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
43
+ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
11
44
  github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
12
45
  github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
13
46
  github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -22,5 +55,13 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
22
55
  github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
23
56
  github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
24
57
  github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
58
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
59
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
60
+ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
61
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
62
+ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
63
+ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
64
+ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
65
+ golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
25
66
  gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26
67
  gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package/llm/client.go CHANGED
@@ -3,7 +3,7 @@ package llm
3
3
  import (
4
4
  "context"
5
5
  "fmt"
6
- "os"
6
+ "strings"
7
7
 
8
8
  "github.com/anthropics/anthropic-sdk-go"
9
9
  "github.com/anthropics/anthropic-sdk-go/option"
@@ -14,13 +14,8 @@ type Client struct {
14
14
  client *anthropic.Client
15
15
  }
16
16
 
17
- // NewClient creates a new Anthropic API client
18
- func NewClient() (*Client, error) {
19
- apiKey := os.Getenv("ANTHROPIC_API_KEY")
20
- if apiKey == "" {
21
- return nil, fmt.Errorf("ANTHROPIC_API_KEY environment variable not set. Please set it to use LLM-based policy extraction")
22
- }
23
-
17
+ // NewClient creates a new Anthropic API client with the provided API key
18
+ func NewClient(apiKey string) (*Client, error) {
24
19
  client := anthropic.NewClient(
25
20
  option.WithAPIKey(apiKey),
26
21
  )
@@ -40,6 +35,11 @@ func (c *Client) SendMessage(ctx context.Context, prompt string) (string, error)
40
35
  Model: anthropic.ModelClaudeHaiku4_5_20251001,
41
36
  })
42
37
  if err != nil {
38
+ // Check for authentication errors
39
+ errStr := err.Error()
40
+ if strings.Contains(errStr, "401") || strings.Contains(errStr, "authentication_error") {
41
+ return "", fmt.Errorf("authentication failed - your API key is invalid or expired. Please run 'tanagram sync' to update your API key")
42
+ }
43
43
  return "", fmt.Errorf("failed to send message: %w", err)
44
44
  }
45
45
 
package/main.go CHANGED
@@ -6,6 +6,7 @@ import (
6
6
 
7
7
  "github.com/tanagram/cli/commands"
8
8
  "github.com/tanagram/cli/metrics"
9
+ "github.com/tanagram/cli/tui"
9
10
  )
10
11
 
11
12
  func main() {
@@ -36,6 +37,36 @@ func main() {
36
37
  "command": "list",
37
38
  })
38
39
  err = commands.List()
40
+ case "welcome":
41
+ metrics.Track("command.execute", map[string]interface{}{
42
+ "command": "welcome",
43
+ })
44
+ choice, err := tui.RunWelcomeScreen()
45
+ if err != nil {
46
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
47
+ os.Exit(1)
48
+ }
49
+ switch choice {
50
+ case tui.ChoiceImportPolicies:
51
+ fmt.Println("\n✓ Selected: Import policies from Tanagram")
52
+ fmt.Println(" (This feature will be implemented to connect to Tanagram cloud)")
53
+ case tui.ChoiceLocalMode:
54
+ fmt.Println("\n✓ Selected: Continue with local (no sign in)")
55
+ fmt.Println(" Continuing with local policy enforcement...")
56
+ case tui.ChoiceNone:
57
+ fmt.Println("\nNo selection made")
58
+ }
59
+ return
60
+ case "puzzle":
61
+ metrics.Track("command.execute", map[string]interface{}{
62
+ "command": "puzzle",
63
+ })
64
+ err := tui.RunPuzzleEditor()
65
+ if err != nil {
66
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
67
+ os.Exit(1)
68
+ }
69
+ return
39
70
  case "help", "-h", "--help":
40
71
  printHelp()
41
72
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "bin/tanagram.js",
36
36
  "checker/",
37
37
  "commands/",
38
+ "config/",
38
39
  "extractor/",
39
40
  "fixtures/",
40
41
  "git/",
@@ -42,10 +43,12 @@
42
43
  "metrics/",
43
44
  "parser/",
44
45
  "storage/",
46
+ "tui/",
45
47
  "main.go",
46
48
  "go.mod",
47
49
  "go.sum",
48
50
  "install.js",
51
+ "tanagram-config.json",
49
52
  "README.md",
50
53
  "LICENSE"
51
54
  ]
@@ -0,0 +1,72 @@
1
+ [
2
+ {
3
+ "id": 1,
4
+ "type": "small-triangle",
5
+ "position": {
6
+ "X": 22,
7
+ "Y": 8
8
+ },
9
+ "rotation": 180,
10
+ "flipped": true
11
+ },
12
+ {
13
+ "id": 2,
14
+ "type": "small-triangle",
15
+ "position": {
16
+ "X": 52,
17
+ "Y": 16
18
+ },
19
+ "rotation": 0,
20
+ "flipped": true
21
+ },
22
+ {
23
+ "id": 3,
24
+ "type": "medium-triangle",
25
+ "position": {
26
+ "X": 68,
27
+ "Y": 7
28
+ },
29
+ "rotation": 0,
30
+ "flipped": false
31
+ },
32
+ {
33
+ "id": 4,
34
+ "type": "large-triangle",
35
+ "position": {
36
+ "X": 48,
37
+ "Y": 20
38
+ },
39
+ "rotation": 315,
40
+ "flipped": true
41
+ },
42
+ {
43
+ "id": 5,
44
+ "type": "large-triangle",
45
+ "position": {
46
+ "X": 22,
47
+ "Y": 7
48
+ },
49
+ "rotation": 0,
50
+ "flipped": true
51
+ },
52
+ {
53
+ "id": 6,
54
+ "type": "square",
55
+ "position": {
56
+ "X": 50,
57
+ "Y": 7
58
+ },
59
+ "rotation": 0,
60
+ "flipped": false
61
+ },
62
+ {
63
+ "id": 7,
64
+ "type": "parallelogram",
65
+ "position": {
66
+ "X": 50,
67
+ "Y": 16
68
+ },
69
+ "rotation": 135,
70
+ "flipped": true
71
+ }
72
+ ]