@tanagram/cli 0.4.5 → 0.4.7

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/api/client.go CHANGED
@@ -28,18 +28,18 @@ type Repository struct {
28
28
 
29
29
  // Policy represents a Tanagram policy
30
30
  type Policy struct {
31
- ID string `json:"id"`
32
- Name string `json:"name"`
33
- OrganizationID string `json:"organization_id"`
34
- Substrate string `json:"substrate"` // "llm" or "tql"
35
- DescriptionFromUser string `json:"description_from_user"`
36
- DescriptionRewrittenByLLM string `json:"description_rewritten_by_llm"`
37
- CustomMessage string `json:"custom_message"`
38
- EnabledStatus string `json:"enabled_status"`
39
- CreatedAt time.Time `json:"created_at"`
40
- UpdatedAt time.Time `json:"updated_at"`
41
- PolicyRepositories []Repository `json:"policy_repositories"`
42
- ViolationsCount int `json:"violations_count"`
31
+ ID string `json:"id"`
32
+ Name string `json:"name"`
33
+ OrganizationID string `json:"organization_id"`
34
+ Substrate string `json:"substrate"` // "llm" or "tql"
35
+ DescriptionFromUser string `json:"description_from_user"`
36
+ DescriptionRewrittenByLLM string `json:"description_rewritten_by_llm"`
37
+ CustomMessage string `json:"custom_message"`
38
+ EnabledStatus string `json:"enabled_status"`
39
+ CreatedAt time.Time `json:"created_at"`
40
+ UpdatedAt time.Time `json:"updated_at"`
41
+ PolicyRepositories []Repository `json:"policy_repositories"`
42
+ ViolationsCount int `json:"violations_count"`
43
43
  }
44
44
 
45
45
  // PolicyListResponse is the response from GET /api/policies/
@@ -84,7 +84,7 @@ func ConfigClaude(settingsPath string) error {
84
84
  }
85
85
 
86
86
  if preHookExists && postHookExists {
87
- fmt.Println("✓ Tanagram hooks are already configured in ~/.claude/settings.json")
87
+ fmt.Printf("✓ Tanagram hooks are already configured in %s\n", settingsPath)
88
88
  return nil
89
89
  }
90
90
 
@@ -120,7 +120,7 @@ func ConfigClaude(settingsPath string) error {
120
120
  return fmt.Errorf("failed to save settings: %w", err)
121
121
  }
122
122
 
123
- fmt.Println("✓ Tanagram hooks added to ~/.claude/settings.json")
123
+ fmt.Printf("✓ Tanagram hooks added to %s\n", settingsPath)
124
124
  fmt.Println("\nClaude Code will now:")
125
125
  fmt.Println(" - Snapshot file state before each Edit/Write (PreToolUse)")
126
126
  fmt.Println(" - Check only Claude's changes after Edit/Write (PostToolUse)")
@@ -191,7 +191,7 @@ func ConfigCursor(hooksPath string) error {
191
191
  }
192
192
 
193
193
  if beforeSubmitHookExists && stopHookExists {
194
- fmt.Println("✓ Tanagram hooks are already configured in ~/.cursor/hooks.json")
194
+ fmt.Printf("✓ Tanagram hooks are already configured in %s\n", hooksPath)
195
195
  return nil
196
196
  }
197
197
 
@@ -215,7 +215,7 @@ func ConfigCursor(hooksPath string) error {
215
215
  return fmt.Errorf("failed to save hooks: %w", err)
216
216
  }
217
217
 
218
- fmt.Println("✓ Tanagram hooks added to ~/.cursor/hooks.json")
218
+ fmt.Printf("✓ Tanagram hooks added to %s\n", hooksPath)
219
219
  fmt.Println("\nCursor will now:")
220
220
  fmt.Println(" - Snapshot file state before each prompt (beforeSubmitPrompt)")
221
221
  fmt.Println(" - Check only Cursor's changes after agent completes (stop)")
package/commands/sync.go CHANGED
@@ -8,6 +8,7 @@ import (
8
8
  "sync"
9
9
  "time"
10
10
 
11
+ "github.com/tanagram/cli/api"
11
12
  "github.com/tanagram/cli/config"
12
13
  "github.com/tanagram/cli/extractor"
13
14
  "github.com/tanagram/cli/parser"
@@ -58,9 +59,17 @@ func Sync() error {
58
59
  }
59
60
  }
60
61
 
61
- // If no files changed, nothing to sync
62
+ // If no files changed, skip local sync but still try cloud sync
62
63
  if len(filesToSync) == 0 {
63
64
  fmt.Println("✓ All instruction files are up to date")
65
+
66
+ // Still sync cloud policies if user is authenticated
67
+ if err := syncCloudPolicies(gitRoot); err != nil {
68
+ // Don't fail the whole sync if cloud sync fails - just warn
69
+ fmt.Printf("\nWarning: Could not sync cloud policies: %v\n", err)
70
+ fmt.Println("(Run 'tanagram login' to sync policies from cloud)")
71
+ }
72
+
64
73
  return nil
65
74
  }
66
75
 
@@ -161,6 +170,14 @@ func Sync() error {
161
170
  }
162
171
 
163
172
  fmt.Printf("\n✓ Synced %d policies from %d changed file(s)\n", totalPolicies, len(filesToSync))
173
+
174
+ // Also sync cloud policies if user is authenticated
175
+ if err := syncCloudPolicies(gitRoot); err != nil {
176
+ // Don't fail the whole sync if cloud sync fails - just warn
177
+ fmt.Printf("\nWarning: Could not sync cloud policies: %v\n", err)
178
+ fmt.Println("(Run 'tanagram login' to sync policies from cloud)")
179
+ }
180
+
164
181
  return nil
165
182
  }
166
183
 
@@ -174,20 +191,20 @@ func FindInstructionFiles(gitRoot string) ([]string, error) {
174
191
 
175
192
  // Directories to skip
176
193
  skipDirs := map[string]bool{
177
- ".git": true,
178
- "node_modules": true,
179
- "vendor": true,
180
- ".venv": true,
181
- "venv": true,
182
- "__pycache__": true,
183
- ".pytest_cache": true,
184
- ".mypy_cache": true,
185
- "dist": true,
186
- "build": true,
187
- ".tanagram": true,
188
- ".conductor": true, // Skip conductor directories
189
- "repos": true, // Skip cloned repos
190
- "monorepo_clones": true, // Skip cloned repos
194
+ ".git": true,
195
+ "node_modules": true,
196
+ "vendor": true,
197
+ ".venv": true,
198
+ "venv": true,
199
+ "__pycache__": true,
200
+ ".pytest_cache": true,
201
+ ".mypy_cache": true,
202
+ "dist": true,
203
+ "build": true,
204
+ ".tanagram": true,
205
+ ".conductor": true, // Skip conductor directories
206
+ "repos": true, // Skip cloned repos
207
+ "monorepo_clones": true, // Skip cloned repos
191
208
  }
192
209
 
193
210
  // Search from git root down
@@ -239,3 +256,71 @@ func getAPIKey() (string, error) {
239
256
  }
240
257
  return apiKey, nil
241
258
  }
259
+
260
+ // syncCloudPolicies fetches policies from Tanagram API and saves them locally
261
+ // Returns an error if the user is not authenticated or if the API call fails
262
+ func syncCloudPolicies(gitRoot string) error {
263
+ fmt.Println("\nSyncing cloud policies from Tanagram...")
264
+
265
+ // Create API client (will fail if not authenticated)
266
+ client, err := api.NewAPIClient()
267
+ if err != nil {
268
+ return err
269
+ }
270
+
271
+ // Fetch policies from API
272
+ response, err := client.GetPolicies()
273
+ if err != nil {
274
+ return err
275
+ }
276
+
277
+ if len(response.Policies) == 0 {
278
+ fmt.Println("No cloud policies found for your organization")
279
+ return nil
280
+ }
281
+
282
+ // Group policies by repository
283
+ repoMap := make(map[string][]api.Policy)
284
+ for _, policy := range response.Policies {
285
+ for _, repo := range policy.PolicyRepositories {
286
+ key := repo.Owner + "/" + repo.Name
287
+ repoMap[key] = append(repoMap[key], policy)
288
+ }
289
+ }
290
+
291
+ // Save policies for each repository
292
+ cloudStorage := storage.NewCloudPolicyStorage(gitRoot)
293
+ savedCount := 0
294
+
295
+ for repoKey, policies := range repoMap {
296
+ // Extract owner and repo name
297
+ owner := policies[0].PolicyRepositories[0].Owner
298
+ repo := policies[0].PolicyRepositories[0].Name
299
+
300
+ err := cloudStorage.SavePoliciesForRepo(owner, repo, policies)
301
+ if err != nil {
302
+ fmt.Printf(" Warning: Failed to save policies for %s: %v\n", repoKey, err)
303
+ continue
304
+ }
305
+
306
+ savedCount++
307
+ fmt.Printf(" ✓ Saved %d policies for %s\n", len(policies), repoKey)
308
+ }
309
+
310
+ // Save metadata
311
+ orgID := ""
312
+ orgName := ""
313
+ if len(response.Policies) > 0 {
314
+ orgID = response.Policies[0].OrganizationID
315
+ orgName = orgID
316
+ }
317
+
318
+ err = cloudStorage.SaveMetadata(orgID, orgName, response.Total, len(repoMap))
319
+ if err != nil {
320
+ return fmt.Errorf("failed to save metadata: %w", err)
321
+ }
322
+
323
+ fmt.Printf("\n✓ Synced %d cloud policies across %d repositories\n", response.Total, savedCount)
324
+
325
+ return nil
326
+ }
@@ -107,5 +107,3 @@ Instruction File Content:
107
107
 
108
108
  Remember: Return ONLY the JSON object, no other text, no markdown formatting.`, content)
109
109
  }
110
-
111
-
package/install.js CHANGED
@@ -97,12 +97,53 @@ function buildBinary(goCommand) {
97
97
  }
98
98
 
99
99
  console.log('✓ Tanagram CLI installed successfully');
100
- return true;
100
+ return binaryPath;
101
101
  } catch (error) {
102
102
  throw new Error(`Build failed: ${error.message}`);
103
103
  }
104
104
  }
105
105
 
106
+ function isCIEnvironment() {
107
+ return (
108
+ process.env.CI === 'true' ||
109
+ process.env.GITHUB_ACTIONS === 'true' ||
110
+ process.env.BUILDKITE === 'true' ||
111
+ process.env.GITLAB_CI === 'true' ||
112
+ process.env.TF_BUILD === 'True'
113
+ );
114
+ }
115
+
116
+ function shouldConfigureHooks() {
117
+ if (process.env.TANAGRAM_SKIP_HOOKS === '1' || process.env.TANAGRAM_SKIP_HOOKS === 'true') {
118
+ return false;
119
+ }
120
+ if (isCIEnvironment()) {
121
+ return false;
122
+ }
123
+ return true;
124
+ }
125
+
126
+ function configureEditorHooks(binaryPath) {
127
+ if (!shouldConfigureHooks()) {
128
+ console.log('Skipping Tanagram editor hook setup (CI or TANAGRAM_SKIP_HOOKS set).');
129
+ return;
130
+ }
131
+
132
+ console.log('Configuring Tanagram editor hooks (Claude Code, Cursor)...');
133
+
134
+ try {
135
+ execSync(`"${binaryPath}" config claude`, { stdio: 'inherit' });
136
+ } catch (err) {
137
+ console.warn('Warning: Failed to configure Claude hooks:', err.message);
138
+ }
139
+
140
+ try {
141
+ execSync(`"${binaryPath}" config cursor`, { stdio: 'inherit' });
142
+ } catch (err) {
143
+ console.warn('Warning: Failed to configure Cursor hooks:', err.message);
144
+ }
145
+ }
146
+
106
147
  // Main installation flow
107
148
  (async () => {
108
149
  try {
@@ -120,7 +161,10 @@ function buildBinary(goCommand) {
120
161
  }
121
162
 
122
163
  // 4. Build the binary
123
- buildBinary(goCommand);
164
+ const binaryPath = buildBinary(goCommand);
165
+
166
+ // 5. Configure hooks
167
+ configureEditorHooks(binaryPath);
124
168
 
125
169
  process.exit(0);
126
170
  } catch (error) {
@@ -143,5 +143,3 @@ func getCwd() string {
143
143
  func getVersion() string {
144
144
  return version
145
145
  }
146
-
147
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
package/parser/agents.go CHANGED
@@ -63,4 +63,3 @@ func parsePolicy(text string) *Policy {
63
63
  OriginalText: text,
64
64
  }
65
65
  }
66
-
@@ -23,8 +23,8 @@ type FileState struct {
23
23
 
24
24
  // Snapshot represents a snapshot of the working directory
25
25
  type Snapshot struct {
26
- Timestamp time.Time `json:"timestamp"`
27
- Files map[string]*FileState `json:"files"`
26
+ Timestamp time.Time `json:"timestamp"`
27
+ Files map[string]*FileState `json:"files"`
28
28
  }
29
29
 
30
30
  // SnapshotPath returns the path to the snapshot file
package/storage/cache.go CHANGED
@@ -28,7 +28,7 @@ type SerializablePolicy struct {
28
28
  type Cache struct {
29
29
  FileMD5s map[string]string // filepath -> MD5 hash
30
30
  Policies map[string][]SerializablePolicy // filepath -> policies
31
- CachePath string // where the cache file is stored
31
+ CachePath string // where the cache file is stored
32
32
  }
33
33
 
34
34
  // NewCache creates a new empty cache
package/tui/puzzle.go CHANGED
@@ -34,7 +34,7 @@ type TangramPiece struct {
34
34
  Type PieceType
35
35
  Color lipgloss.Color
36
36
  Position Position
37
- Rotation int // 0, 90, 180, 270
37
+ Rotation int // 0, 90, 180, 270
38
38
  Flipped bool
39
39
  }
40
40