@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.
- package/checker/llm_integration.go +4 -4
- package/checker/matcher.go +2 -2
- package/checker/violation_checker.go +2 -2
- package/commands/run.go +15 -2
- package/commands/sync.go +17 -1
- package/config/config.go +155 -0
- package/extractor/extractor.go +2 -2
- package/extractor/file.go +2 -2
- package/go.mod +20 -1
- package/go.sum +41 -0
- package/llm/client.go +8 -8
- package/main.go +31 -0
- package/package.json +4 -1
- package/tanagram-config.json +72 -0
- package/tui/puzzle.go +694 -0
- package/tui/renderer.go +359 -0
- package/tui/welcome.go +186 -0
|
@@ -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)
|
package/checker/matcher.go
CHANGED
|
@@ -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
|
+
}
|
package/config/config.go
ADDED
|
@@ -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
|
+
}
|
package/extractor/extractor.go
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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.
|
|
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
|
+
]
|