@tanagram/cli 0.5.62 → 0.5.63

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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/install.js CHANGED
@@ -6,6 +6,7 @@ const os = require('os');
6
6
  const https = require('https');
7
7
  const crypto = require('crypto');
8
8
  const pkg = require('./package.json');
9
+ const { SKILLS } = require('./skills.config');
9
10
 
10
11
  const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
11
12
  const POSTHOG_HOST = 'phe.tanagram.ai';
@@ -124,21 +125,10 @@ function isCIEnvironment() {
124
125
  );
125
126
  }
126
127
 
127
- function installClaudeSkill() {
128
- if (process.env.TANAGRAM_SKIP_SKILL === '1' || process.env.TANAGRAM_SKIP_SKILL === 'true') {
129
- console.error('Skipping Tanagram skill installation (TANAGRAM_SKIP_SKILL set).');
130
- return;
131
- }
132
- if (isCIEnvironment()) {
133
- console.error('Skipping Tanagram skill installation (CI environment).');
134
- return;
135
- }
136
-
137
- console.error('Installing Tanagram skill...');
138
-
128
+ function installSkill(skillName) {
139
129
  try {
140
- const skillsSourceDir = path.join(__dirname, 'skills', 'tanagram');
141
- const skillsTargetDir = path.join(os.homedir(), '.claude', 'skills', 'tanagram');
130
+ const skillsSourceDir = path.join(__dirname, 'skills', skillName);
131
+ const skillsTargetDir = path.join(os.homedir(), '.claude', 'skills', skillName);
142
132
 
143
133
  const isFirstTime = !fs.existsSync(path.join(skillsTargetDir, 'SKILL.md'));
144
134
 
@@ -162,11 +152,29 @@ function installClaudeSkill() {
162
152
  }
163
153
  }
164
154
 
165
- console.error(`✓ Tanagram skill installed to ${skillsTargetDir}`);
166
- track('cli.skill.install.success', { first_time: isFirstTime });
155
+ console.error(`✓ ${skillName} skill installed to ${skillsTargetDir}`);
156
+ track('cli.skill.install.success', { skill: skillName, first_time: isFirstTime });
167
157
  } catch (err) {
168
- console.error('Warning: Failed to install Claude skill:', err.message);
169
- track('cli.skill.install.failure', { error: err.message });
158
+ console.error(`Warning: Failed to install ${skillName} skill:`, err.message);
159
+ track('cli.skill.install.failure', { skill: skillName, error: err.message });
160
+ throw err;
161
+ }
162
+ }
163
+
164
+ function installClaudeSkills() {
165
+ if (process.env.TANAGRAM_SKIP_SKILL === '1' || process.env.TANAGRAM_SKIP_SKILL === 'true') {
166
+ console.error('Skipping Tanagram skill installation (TANAGRAM_SKIP_SKILL set).');
167
+ return;
168
+ }
169
+ if (isCIEnvironment()) {
170
+ console.error('Skipping Tanagram skill installation (CI environment).');
171
+ return;
172
+ }
173
+
174
+ console.error('Installing Tanagram skills...');
175
+
176
+ for (const skill of SKILLS) {
177
+ installSkill(skill);
170
178
  }
171
179
  }
172
180
 
@@ -269,8 +277,8 @@ function ensureOpenCode() {
269
277
  const prebuiltPath = findPrebuiltBinary();
270
278
  installPrebuiltBinary(prebuiltPath);
271
279
 
272
- // Install Claude skill
273
- installClaudeSkill();
280
+ // Install Claude skills
281
+ installClaudeSkills();
274
282
 
275
283
  // Install Stop hook for Claude Code
276
284
  installStopHook();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.5.62",
3
+ "version": "0.5.63",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: tanagram-mine
3
+ description: Mine PR comments from any GitHub repo to discover anti-patterns and code review feedback, then create Tanagram rules from the findings. Use when the user says "mine rules", "mine PRs", "extract rules from PRs", "learn from code reviews", or wants to turn a repo's review history into enforceable rules.
4
+ argument-hint: <owner/repo> [--limit N] [--repos repo-slug]
5
+ allowed-tools: Bash, Read, Glob, Grep, Agent
6
+ ---
7
+
8
+ # Tanagram Mine — PR Comment Rule Mining
9
+
10
+ Mines human code review comments from any GitHub repository's PR history, identifies recurring anti-patterns and code conventions, and creates Tanagram rules so the team never has to give the same feedback twice.
11
+
12
+ ## Prerequisites
13
+
14
+ - `gh` CLI authenticated and able to access the target repo
15
+ - `tanagram` CLI installed and authenticated (`tanagram login`)
16
+
17
+ ## Arguments
18
+
19
+ - **First argument**: `owner/repo` (e.g., `tanagram/monorepo`, `facebook/react`). If omitted, detect from the current git repo's remote.
20
+ - **`--limit N`**: Number of PRs to scan (default: 50)
21
+ - **`--repos slug`**: Tanagram repo slug to scope rules to (default: same as `owner/repo`)
22
+
23
+ ## Procedure
24
+
25
+ ### Step 0: Verify tooling
26
+
27
+ Before doing anything, confirm both CLIs are available and authenticated:
28
+
29
+ ```bash
30
+ # Check gh is authenticated and can reach GitHub
31
+ gh auth status
32
+
33
+ # Check tanagram is installed
34
+ which tanagram
35
+
36
+ # Check tanagram is authenticated (will show rules or auth error)
37
+ tanagram rules list --json
38
+ ```
39
+
40
+ If `tanagram` is not installed, tell the user to run `npm install -g @tanagram/cli && tanagram login`.
41
+ If `tanagram rules list` returns an auth error, tell the user to run `tanagram login`.
42
+
43
+ ### Step 1: Resolve the target repo
44
+
45
+ ```bash
46
+ # If argument provided, use it directly
47
+ REPO="owner/repo"
48
+
49
+ # If no argument, detect from current git repo
50
+ REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
51
+ ```
52
+
53
+ Verify access:
54
+ ```bash
55
+ gh repo view "$REPO" --json nameWithOwner --jq '.nameWithOwner'
56
+ ```
57
+
58
+ ### Step 2: Fetch PR list
59
+
60
+ ```bash
61
+ gh pr list --repo "$REPO" --state all --limit 50 --json number,title,state,author
62
+ ```
63
+
64
+ ### Step 3: Mine human review comments
65
+
66
+ For each PR, fetch three types of comments and **filter out bots**:
67
+
68
+ #### 3a. Inline review comments (code-level feedback)
69
+ ```bash
70
+ gh api "repos/$REPO/pulls/$PR_NUMBER/comments" \
71
+ --jq '.[] | select(.user.type != "Bot") | select(.body | test("BUGBOT|bugbot|TANAGRAM-COMMENTS|<!-- ") | not) | "\(.user.login): \(.body[0:500])"'
72
+ ```
73
+
74
+ #### 3b. General PR comments
75
+ ```bash
76
+ gh pr view $PR_NUMBER --repo "$REPO" --json comments \
77
+ --jq '.comments[] | select(.author.login | test("bot$|\\[bot\\]") | not) | select(.body | test("BUGBOT|bugbot|TANAGRAM-COMMENTS|<!-- |linear-linkback") | not) | "\(.author.login): \(.body[0:500])"'
78
+ ```
79
+
80
+ #### 3c. Review bodies
81
+ ```bash
82
+ gh pr view $PR_NUMBER --repo "$REPO" --json reviews \
83
+ --jq '.reviews[] | select(.body != "" and (.body | test("BUGBOT|bugbot|<!-- ") | not) and (.author.login | test("bot$|\\[bot\\]") | not)) | "\(.author.login): \(.body[0:500])"'
84
+ ```
85
+
86
+ **Important filters:**
87
+ - Exclude bot users (`.user.type == "Bot"`, login ending in `bot` or `[bot]`)
88
+ - Exclude automated comments (Bugbot, Tanagram, Linear linkbacks, HTML comments)
89
+ - Only keep comments with actionable code review feedback
90
+
91
+ ### Step 4: Identify patterns
92
+
93
+ Analyze all collected human comments and look for:
94
+
95
+ 1. **Repeated corrections** — The same reviewer giving similar feedback across multiple PRs
96
+ 2. **Anti-patterns called out** — "Don't do X", "use Y instead", "this pattern is wrong"
97
+ 3. **Convention enforcement** — "We always...", "prefer...", "this should be in..."
98
+ 4. **Architecture guidance** — "This belongs in...", "separate this into..."
99
+ 5. **Bug patterns** — "This will cause...", "race condition", "memory leak"
100
+ 6. **Style/readability** — "Extract this", "rename to", "use named types"
101
+
102
+ **Discard comments that are:**
103
+ - Simple approvals ("LGTM", "+1", "looks good")
104
+ - One-off contextual discussions not generalizable to rules
105
+ - Questions without clear guidance ("why did you...?")
106
+ - References to specific PRs/commits without a general principle
107
+
108
+ ### Step 5: Check for duplicates
109
+
110
+ Before creating rules, fetch existing rules and compare:
111
+ ```bash
112
+ tanagram rules list --json
113
+ ```
114
+
115
+ The output is a JSON object with a `rules` array. Each rule has `id`, `name`, `status`, and `repos` fields. Compare each proposed rule's name and intent against this list. Skip any that substantially overlap with an existing rule.
116
+
117
+ ### Step 6: Present proposed rules to the user
118
+
119
+ Before creating anything, show the user a table of proposed rules:
120
+
121
+ ```
122
+ ## Proposed Rules from PR Mining
123
+
124
+ | # | Rule Name | Source | Reviewer | Why |
125
+ |---|-----------|--------|----------|-----|
126
+ | 1 | AwaitGoroutinesBeforeExit | PR #260 | @feifanzhou | goroutine leak in CLI |
127
+ ```
128
+
129
+ Ask: "Want me to create all of these, or pick specific ones?"
130
+
131
+ If the user says "go ahead" or "all", proceed. If they pick specific ones, only create those.
132
+
133
+ ### Step 7: Create rules via `tanagram rules create`
134
+
135
+ The Tanagram CLI has a `rules` subcommand with full CRUD. To create a rule:
136
+
137
+ ```bash
138
+ tanagram rules create \
139
+ --name "RuleName" \
140
+ --description "What the rule catches and why it matters. Include the anti-pattern and the correct pattern." \
141
+ --repos "owner/repo"
142
+ ```
143
+
144
+ **Required flags:**
145
+ - `--name` — The rule name (PascalCase, specific, self-explanatory)
146
+ - `--repos` — Comma-separated repo slugs like `owner/name` (e.g., `tanagram/monorepo`) or raw repo IDs
147
+
148
+ **Optional flags:**
149
+ - `--description` — What the rule catches, why, anti-pattern, correct pattern
150
+ - `--substrate tql` — The rule engine (defaults to `tql`, don't change this)
151
+ - `--json` — Get structured JSON output (useful for verifying success)
152
+
153
+ **How it works under the hood:**
154
+ 1. The CLI authenticates via the stored JWT at `~/.tanagram/token`
155
+ 2. If `--repos` contains `/` (slug format), it calls `GET /api/repos/` to resolve the slug to an internal repo ID
156
+ 3. It `POST`s to `/api/policies/` with the name, description, and resolved repo IDs
157
+ 4. The backend creates the rule with status `being_rewritten` — the backend asynchronously compiles the natural-language description into a TQL policy (no action needed from us)
158
+ 5. On success, the response includes the created rule's `id`, `name`, and `policy_repositories`
159
+
160
+ **Success looks like:**
161
+ ```json
162
+ {
163
+ "success": true,
164
+ "rule": {
165
+ "id": "pol_abc123",
166
+ "name": "RuleName",
167
+ "enabled_status": "being_rewritten",
168
+ "policy_repositories": [{"id": "gitrepo_xyz", "name": "monorepo", "owner": "tanagram"}]
169
+ }
170
+ }
171
+ ```
172
+
173
+ **Failure modes and fixes:**
174
+ - `"Not authenticated"` → Run `tanagram login` first
175
+ - `"repository X not found"` → The repo slug doesn't match any repo the user has connected. Run `tanagram rules list --json` to see which repos are available in existing rules, or ask the user for the correct slug.
176
+ - `"validation error"` → Usually means `--name` or `--repos` is missing
177
+
178
+ **Other CRUD commands (for reference):**
179
+ ```bash
180
+ tanagram rules list [--json] [--offline] # List all rules
181
+ tanagram rules get <rule-id> [--json] [--tql] # Get rule details
182
+ tanagram rules update <rule-id> --name "..." # Update rule
183
+ tanagram rules delete <rule-id> # Delete rule
184
+ ```
185
+
186
+ **Rule naming conventions:**
187
+ - Use PascalCase (e.g., `AwaitGoroutinesBeforeExit`)
188
+ - Be specific, not generic (e.g., `NoObjectLiteralsInUseEffectDeps` not `FixUseEffect`)
189
+ - Name should be self-explanatory without reading the description
190
+
191
+ **Rule description must include:**
192
+ 1. What the rule catches (the anti-pattern)
193
+ 2. Why it matters (the consequence)
194
+ 3. The correct alternative
195
+ 4. A concrete anti-pattern → correct-pattern example
196
+
197
+ **Example of a well-formed create command:**
198
+ ```bash
199
+ tanagram rules create \
200
+ --name "SelectThenInsertRequiresUpsert" \
201
+ --description "SELECT-then-INSERT patterns without concurrency protection cause race conditions. When two concurrent requests check for existence simultaneously, both find nothing and both attempt to INSERT, causing IntegrityError on unique constraints. Anti-pattern: record = SELECT ... WHERE user_id = X; if not record: INSERT INTO ...; — this has a TOCTOU race. Correct: use INSERT ... ON CONFLICT DO UPDATE (PostgreSQL upsert), or wrap the operation in a try/except IntegrityError with a retry." \
202
+ --repos "tanagram/monorepo"
203
+ ```
204
+
205
+ ### Step 8: Report results
206
+
207
+ Present a summary table to the user:
208
+
209
+ ```
210
+ ## Mining Results for owner/repo
211
+
212
+ Scanned: N PRs, M human review comments found
213
+ Rules created: K
214
+
215
+ | # | Rule | ID | Source PR | Reviewer |
216
+ |---|------|----|----------|----------|
217
+ | 1 | RuleName | pol_abc123 | #123 | @reviewer |
218
+
219
+ Skipped (duplicates of existing rules): [list if any]
220
+ ```
221
+
222
+ ## Efficiency Tips
223
+
224
+ - Use `Agent` tool to parallelize: one agent for PR comment mining, another to study the codebase
225
+ - Process PRs in batches to avoid rate limiting
226
+ - Focus on merged PRs first (they have the most complete review cycles)
227
+ - Prioritize PRs with multiple reviewers (more signal)
228
+ - Scan the most recent PRs first (conventions evolve)
229
+ - Run all `tanagram rules create` commands in parallel (they're independent)
230
+
231
+ ## Example Usage
232
+
233
+ ```
234
+ User: /tanagram-mine tanagram/monorepo
235
+ User: /tanagram-mine facebook/react --limit 100
236
+ User: /tanagram-mine # auto-detects from current repo
237
+ User: mine rules from the last 20 PRs
238
+ ```
package/uninstall.js CHANGED
@@ -6,6 +6,7 @@ const os = require('os');
6
6
  const https = require('https');
7
7
  const crypto = require('crypto');
8
8
  const pkg = require('./package.json');
9
+ const { SKILLS } = require('./skills.config');
9
10
 
10
11
  const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
11
12
  const POSTHOG_HOST = 'phe.tanagram.ai';
@@ -58,18 +59,15 @@ function isCIEnvironment() {
58
59
  );
59
60
  }
60
61
 
61
- function removeClaudeSkill() {
62
- const skillsDir = path.join(os.homedir(), '.claude', 'skills', 'tanagram');
62
+ function removeClaudeSkills() {
63
+ for (const skill of SKILLS) {
64
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills', skill);
63
65
 
64
- try {
65
66
  if (fs.existsSync(skillsDir)) {
66
67
  fs.rmSync(skillsDir, { recursive: true });
67
- console.error('✓ Tanagram skill removed');
68
- track('cli.skill.uninstall.success');
68
+ console.error(`✓ ${skill} skill removed`);
69
+ track('cli.skill.uninstall.success', { skill });
69
70
  }
70
- } catch (err) {
71
- console.error('Warning: Failed to remove Tanagram skill:', err.message);
72
- track('cli.skill.uninstall.failure', { error: err.message });
73
71
  }
74
72
  }
75
73
 
@@ -134,7 +132,7 @@ function removeOpenCode() {
134
132
  // Main uninstall flow
135
133
  track('cli.uninstall.start');
136
134
 
137
- removeClaudeSkill();
135
+ removeClaudeSkills();
138
136
  removeStopHook();
139
137
  removeOpenCode();
140
138