@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 +12 -12
- package/commands/config.go +4 -4
- package/commands/sync.go +100 -15
- package/extractor/extractor.go +0 -2
- package/install.js +46 -2
- package/metrics/metrics.go +0 -2
- package/package.json +1 -1
- package/parser/agents.go +0 -1
- package/snapshot/snapshot.go +2 -2
- package/storage/cache.go +1 -1
- package/tui/puzzle.go +1 -1
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
|
|
32
|
-
Name
|
|
33
|
-
OrganizationID
|
|
34
|
-
Substrate
|
|
35
|
-
DescriptionFromUser
|
|
36
|
-
DescriptionRewrittenByLLM
|
|
37
|
-
CustomMessage
|
|
38
|
-
EnabledStatus
|
|
39
|
-
CreatedAt
|
|
40
|
-
UpdatedAt
|
|
41
|
-
PolicyRepositories
|
|
42
|
-
ViolationsCount
|
|
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/
|
package/commands/config.go
CHANGED
|
@@ -84,7 +84,7 @@ func ConfigClaude(settingsPath string) error {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
if preHookExists && postHookExists {
|
|
87
|
-
fmt.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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":
|
|
178
|
-
"node_modules":
|
|
179
|
-
"vendor":
|
|
180
|
-
".venv":
|
|
181
|
-
"venv":
|
|
182
|
-
"__pycache__":
|
|
183
|
-
".pytest_cache":
|
|
184
|
-
".mypy_cache":
|
|
185
|
-
"dist":
|
|
186
|
-
"build":
|
|
187
|
-
".tanagram":
|
|
188
|
-
".conductor":
|
|
189
|
-
"repos":
|
|
190
|
-
"monorepo_clones":
|
|
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
|
+
}
|
package/extractor/extractor.go
CHANGED
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
|
|
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) {
|
package/metrics/metrics.go
CHANGED
package/package.json
CHANGED
package/parser/agents.go
CHANGED
package/snapshot/snapshot.go
CHANGED
|
@@ -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
|
|
27
|
-
Files map[string]*FileState
|
|
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
|
|
31
|
+
CachePath string // where the cache file is stored
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// NewCache creates a new empty cache
|