@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.
- package/dist/npm/darwin-arm64/tanagram +0 -0
- package/dist/npm/darwin-x64/tanagram +0 -0
- package/dist/npm/linux-arm64/tanagram +0 -0
- package/dist/npm/linux-x64/tanagram +0 -0
- package/dist/npm/tanagram_0.5.63_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.63_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.63_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.63_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.63_windows_amd64.zip +0 -0
- package/dist/npm/win32-x64/tanagram.exe +0 -0
- package/install.js +28 -20
- package/package.json +1 -1
- package/skills/tanagram-mine/SKILL.md +238 -0
- package/uninstall.js +7 -9
- package/dist/npm/tanagram_0.5.62_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.62_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.62_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.62_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.5.62_windows_amd64.zip +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
|
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',
|
|
141
|
-
const skillsTargetDir = path.join(os.homedir(), '.claude', 'skills',
|
|
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(`✓
|
|
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(
|
|
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
|
|
273
|
-
|
|
280
|
+
// Install Claude skills
|
|
281
|
+
installClaudeSkills();
|
|
274
282
|
|
|
275
283
|
// Install Stop hook for Claude Code
|
|
276
284
|
installStopHook();
|
package/package.json
CHANGED
|
@@ -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
|
|
62
|
-
const
|
|
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(
|
|
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
|
-
|
|
135
|
+
removeClaudeSkills();
|
|
138
136
|
removeStopHook();
|
|
139
137
|
removeOpenCode();
|
|
140
138
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|