claude-dev-env 1.2.1 → 1.7.0
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/agents/project-context-loader.md +1 -1
- package/bin/install.mjs +76 -0
- package/docs/PR_DESCRIPTION_GUIDE.md +95 -0
- package/hooks/blocking/block-main-commit.py +13 -1
- package/hooks/blocking/pr-description-enforcer.py +68 -43
- package/hooks/hooks.json +0 -20
- package/hooks/validation/mypy_validator.py +35 -0
- package/hooks/workflow/auto-formatter.py +40 -1
- package/package.json +8 -2
- package/skills/npm-creator/SKILL.md +7 -3
- package/skills/skill-writer/REFERENCE.md +160 -122
- package/skills/skill-writer/SKILL.md +131 -197
- package/LICENSE +0 -21
- package/README.md +0 -247
- package/hooks/blocking/docker-settings-guard.py +0 -44
- package/hooks/blocking/parallel-task-blocker.py +0 -69
- package/hooks/blocking/pyautogui-scroll-blocker.py +0 -74
- package/hooks/session/bulk-edit-reminder.py +0 -30
- package/hooks/session/code-rules-reminder.py +0 -97
- package/hooks/session/compact-context-reinject.py +0 -39
- package/hooks/session/hook-structure-context.py +0 -140
- package/hooks/validation/code-style-validator.py +0 -145
- package/hooks/validation/e2e-test-validator.py +0 -142
- package/skills/agent-prompt/SKILL.md +0 -102
- package/skills/prompt-generator/REFERENCE.md +0 -150
- package/skills/prompt-generator/SKILL.md +0 -154
package/bin/install.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { join, dirname, resolve, relative } from 'node:path';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
8
9
|
|
|
9
10
|
const CLAUDE_HOME = join(homedir(), '.claude');
|
|
10
11
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -12,6 +13,7 @@ const MANIFEST_FILE = join(CLAUDE_HOME, '.claude-dev-env-manifest.json');
|
|
|
12
13
|
const PACKAGE_NAME = 'claude-dev-env';
|
|
13
14
|
|
|
14
15
|
const CONTENT_DIRECTORIES = ['rules', 'docs', 'commands', 'agents'];
|
|
16
|
+
const WORKSPACE_SIBLINGS = ['claude-journal', 'claude-deep-research', 'claude-prompt-tools'];
|
|
15
17
|
|
|
16
18
|
function detectPython() {
|
|
17
19
|
const candidates = [
|
|
@@ -114,6 +116,67 @@ function mergeHooks(pythonCommand) {
|
|
|
114
116
|
return groupCount;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function findSiblingPackage(packageName) {
|
|
120
|
+
const workspaceRoot = resolve(PACKAGE_ROOT, '..', '..');
|
|
121
|
+
const workspacePath = join(workspaceRoot, 'packages', packageName);
|
|
122
|
+
if (existsSync(join(workspacePath, 'package.json'))) {
|
|
123
|
+
return workspacePath;
|
|
124
|
+
}
|
|
125
|
+
const require = createRequire(import.meta.url);
|
|
126
|
+
try {
|
|
127
|
+
const packageJsonPath = require.resolve(join(packageName, 'package.json'));
|
|
128
|
+
return dirname(packageJsonPath);
|
|
129
|
+
} catch { /* not found */ }
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function installSiblingContent(siblingRoot, allInstalledFiles) {
|
|
134
|
+
const siblingName = JSON.parse(readFileSync(join(siblingRoot, 'package.json'), 'utf8')).name;
|
|
135
|
+
console.log(` Installing sibling: ${siblingName}`);
|
|
136
|
+
let totalFiles = 0;
|
|
137
|
+
const skillsSource = join(siblingRoot, 'skills');
|
|
138
|
+
if (existsSync(skillsSource)) {
|
|
139
|
+
const skillDirs = readdirSync(skillsSource, { withFileTypes: true }).filter(entry => entry.isDirectory());
|
|
140
|
+
for (const skillDir of skillDirs) {
|
|
141
|
+
const stats = copyTree(join(skillsSource, skillDir.name), join(CLAUDE_HOME, 'skills', skillDir.name));
|
|
142
|
+
allInstalledFiles.push(...stats.paths);
|
|
143
|
+
totalFiles += stats.created + stats.updated;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const agentsSource = join(siblingRoot, 'agents');
|
|
147
|
+
if (existsSync(agentsSource)) {
|
|
148
|
+
const stats = copyTree(agentsSource, join(CLAUDE_HOME, 'agents'));
|
|
149
|
+
allInstalledFiles.push(...stats.paths);
|
|
150
|
+
totalFiles += stats.created + stats.updated;
|
|
151
|
+
}
|
|
152
|
+
const rulesSource = join(siblingRoot, 'rules');
|
|
153
|
+
if (existsSync(rulesSource)) {
|
|
154
|
+
const stats = copyTree(rulesSource, join(CLAUDE_HOME, 'rules'));
|
|
155
|
+
allInstalledFiles.push(...stats.paths);
|
|
156
|
+
totalFiles += stats.created + stats.updated;
|
|
157
|
+
}
|
|
158
|
+
const hooksSource = join(siblingRoot, 'hooks');
|
|
159
|
+
if (existsSync(hooksSource)) {
|
|
160
|
+
const filesToCopy = collectFiles(hooksSource).filter(file => !file.endsWith('hooks.json'));
|
|
161
|
+
const hooksDestination = join(CLAUDE_HOME, 'hooks');
|
|
162
|
+
for (const sourceFile of filesToCopy) {
|
|
163
|
+
const relativePath = relative(hooksSource, sourceFile);
|
|
164
|
+
const destFile = join(hooksDestination, relativePath);
|
|
165
|
+
mkdirSync(dirname(destFile), { recursive: true });
|
|
166
|
+
const existed = existsSync(destFile);
|
|
167
|
+
copyFileSync(sourceFile, destFile);
|
|
168
|
+
allInstalledFiles.push(destFile);
|
|
169
|
+
totalFiles++;
|
|
170
|
+
if (existed) {
|
|
171
|
+
console.log(` ↻ ${join('hooks', relativePath)} (updated)`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(` ✓ ${join('hooks', relativePath)} (new)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return totalFiles;
|
|
178
|
+
}
|
|
179
|
+
|
|
117
180
|
function writeManifest(installedFiles) {
|
|
118
181
|
const manifest = { package: PACKAGE_NAME, version: '1.0.0', installedAt: new Date().toISOString(), files: installedFiles };
|
|
119
182
|
writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2) + '\n');
|
|
@@ -153,6 +216,16 @@ function install() {
|
|
|
153
216
|
summary.skills = { created: skillsCreated, updated: skillsUpdated, paths: skillPaths };
|
|
154
217
|
allInstalledFiles.push(...skillPaths);
|
|
155
218
|
}
|
|
219
|
+
let siblingCount = 0;
|
|
220
|
+
for (const siblingName of WORKSPACE_SIBLINGS) {
|
|
221
|
+
const siblingRoot = findSiblingPackage(siblingName);
|
|
222
|
+
if (siblingRoot) {
|
|
223
|
+
siblingCount += installSiblingContent(siblingRoot, allInstalledFiles);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (siblingCount > 0) {
|
|
227
|
+
summary.siblings = siblingCount;
|
|
228
|
+
}
|
|
156
229
|
const hooksSource = join(PACKAGE_ROOT, 'hooks');
|
|
157
230
|
if (existsSync(hooksSource)) {
|
|
158
231
|
const hooksDestination = join(CLAUDE_HOME, 'hooks');
|
|
@@ -186,6 +259,9 @@ function install() {
|
|
|
186
259
|
const { created, updated } = summary.skills;
|
|
187
260
|
console.log(` skills: ${created + updated} files (${created} new, ${updated} updated)`);
|
|
188
261
|
}
|
|
262
|
+
if (summary.siblings) {
|
|
263
|
+
console.log(` workspace siblings: ${summary.siblings} files`);
|
|
264
|
+
}
|
|
189
265
|
if (summary.hookFiles) {
|
|
190
266
|
console.log(` hooks: ${summary.hookFiles.created + summary.hookFiles.updated} files, ${summary.hookGroups} groups in settings.json`);
|
|
191
267
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# GitHub PR Summary Writing Guide for AI
|
|
2
|
+
|
|
3
|
+
Use this guide when writing pull request descriptions. Follow these best practices to create clear, professional, and reviewable PR summaries.
|
|
4
|
+
|
|
5
|
+
## Required Sections
|
|
6
|
+
|
|
7
|
+
### What (Changes)
|
|
8
|
+
|
|
9
|
+
- Concise statement of what was changed
|
|
10
|
+
- What files or systems were modified
|
|
11
|
+
- What functionality was added, removed, or improved
|
|
12
|
+
- Keep to 2-3 sentences maximum
|
|
13
|
+
|
|
14
|
+
### Why (Problem/Context)
|
|
15
|
+
|
|
16
|
+
- Explain the problem this PR solves
|
|
17
|
+
- Provide business or technical context
|
|
18
|
+
- Reference related issue numbers using `#123` or `Fixes #123`, `Closes #456`
|
|
19
|
+
- If no issue exists, briefly explain the motivation
|
|
20
|
+
|
|
21
|
+
### How (Approach/Solution)
|
|
22
|
+
|
|
23
|
+
- Describe your implementation approach
|
|
24
|
+
- Explain any design decisions or trade-offs
|
|
25
|
+
- Include architectural changes if applicable
|
|
26
|
+
- Note any breaking changes prominently
|
|
27
|
+
|
|
28
|
+
## Supporting Details
|
|
29
|
+
|
|
30
|
+
### Testing and Quality
|
|
31
|
+
|
|
32
|
+
- What tests were added/modified
|
|
33
|
+
- How to manually verify the changes (if applicable)
|
|
34
|
+
- Any areas of concern or limitations
|
|
35
|
+
- Performance impact (if relevant)
|
|
36
|
+
|
|
37
|
+
### Dependencies and Risk
|
|
38
|
+
|
|
39
|
+
- New dependencies introduced (if any)
|
|
40
|
+
- Backward compatibility status
|
|
41
|
+
- Potential side effects
|
|
42
|
+
- Migration steps (if needed)
|
|
43
|
+
|
|
44
|
+
## Optional but Valuable
|
|
45
|
+
|
|
46
|
+
### Related Issues/PRs
|
|
47
|
+
|
|
48
|
+
- Link to dependent PRs or issues
|
|
49
|
+
- Note any follow-up work needed
|
|
50
|
+
|
|
51
|
+
### Screenshots/Examples (for UI changes)
|
|
52
|
+
|
|
53
|
+
- Before/after comparisons when visual changes are involved
|
|
54
|
+
|
|
55
|
+
### Reviewer Guidance
|
|
56
|
+
|
|
57
|
+
- Specific areas to focus on
|
|
58
|
+
- Questions for reviewers
|
|
59
|
+
- Deployment considerations
|
|
60
|
+
|
|
61
|
+
## Tone and Style Guidelines
|
|
62
|
+
|
|
63
|
+
- Be clear and concise -- reviewers scan quickly
|
|
64
|
+
- Use second person sparingly -- focus on what the code does, not what the reviewer should do
|
|
65
|
+
- Avoid jargon -- explain technical terms if non-obvious
|
|
66
|
+
- Use markdown formatting -- bullets, code blocks, headers for readability
|
|
67
|
+
- Be honest about limitations -- acknowledge trade-offs and known issues
|
|
68
|
+
- Assume reviewers are unfamiliar -- provide sufficient context
|
|
69
|
+
|
|
70
|
+
## What to Avoid
|
|
71
|
+
|
|
72
|
+
- Vague statements like "fix bug" or "update code"
|
|
73
|
+
- AI-generated summaries without human verification
|
|
74
|
+
- Large walls of text -- break into sections
|
|
75
|
+
- Repeating information from commit messages
|
|
76
|
+
- References to temporary branch names or internal jargon without context
|
|
77
|
+
|
|
78
|
+
## Example Structure
|
|
79
|
+
|
|
80
|
+
```markdown
|
|
81
|
+
## Description
|
|
82
|
+
Brief 1-2 sentence overview of the change.
|
|
83
|
+
|
|
84
|
+
## Why
|
|
85
|
+
Problem/context and reference to related issue (#123).
|
|
86
|
+
|
|
87
|
+
## How
|
|
88
|
+
Implementation approach and design decisions.
|
|
89
|
+
|
|
90
|
+
## Testing
|
|
91
|
+
How this was tested and verified.
|
|
92
|
+
|
|
93
|
+
## Risk Assessment
|
|
94
|
+
Any breaking changes, dependencies, or concerns.
|
|
95
|
+
```
|
|
@@ -123,9 +123,21 @@ def parse_bash_command_from_stdin() -> str:
|
|
|
123
123
|
return hook_event.get("tool_input", {}).get("command", "")
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
DRAFT_PR_INSTRUCTION = (
|
|
127
|
+
" Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
|
|
128
|
+
"(2) commit your changes there, "
|
|
129
|
+
"(3) push with `git push -u origin <branch-name>`, "
|
|
130
|
+
"(4) create a draft PR with `gh pr create --draft`. "
|
|
131
|
+
"If you must commit to main, the user needs to approve explicitly."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
126
135
|
def build_denial_response(branch_name: str, repo_dir: str | None) -> dict:
|
|
127
136
|
location = f" in {repo_dir}" if repo_dir else ""
|
|
128
|
-
denial_reason =
|
|
137
|
+
denial_reason = (
|
|
138
|
+
f"BLOCKED: Direct commit to '{branch_name}'{location} is not allowed."
|
|
139
|
+
+ DRAFT_PR_INSTRUCTION
|
|
140
|
+
)
|
|
129
141
|
|
|
130
142
|
return {
|
|
131
143
|
"hookSpecificOutput": {
|
|
@@ -1,9 +1,67 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import re
|
|
3
4
|
import sys
|
|
4
5
|
|
|
6
|
+
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
7
|
+
PR_GUIDE_PATH = os.path.join(PLUGIN_ROOT, "docs", "PR_DESCRIPTION_GUIDE.md")
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
REQUIRED_PR_SECTION_HEADERS = [
|
|
10
|
+
"description",
|
|
11
|
+
"why",
|
|
12
|
+
"how",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
MINIMUM_PR_BODY_LENGTH = 50
|
|
16
|
+
|
|
17
|
+
VAGUE_LANGUAGE_PATTERN = re.compile(
|
|
18
|
+
r'\b(fix(?:ed)? (?:bug|issue|it)|update(?:d)? code|minor changes|various (?:fixes|updates|improvements))\b',
|
|
19
|
+
re.IGNORECASE,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_body_from_command(command: str) -> str:
|
|
24
|
+
heredoc_match = re.search(r'--body\s+"\$\(cat <<', command)
|
|
25
|
+
if heredoc_match:
|
|
26
|
+
return command[heredoc_match.start():]
|
|
27
|
+
|
|
28
|
+
body_match = re.search(r'--body\s+"([^"]*)"', command) or re.search(r"--body\s+'([^']*)'", command)
|
|
29
|
+
if body_match:
|
|
30
|
+
return body_match.group(1)
|
|
31
|
+
|
|
32
|
+
short_flag_match = re.search(r'-b\s+"([^"]*)"', command) or re.search(r"-b\s+'([^']*)'", command)
|
|
33
|
+
if short_flag_match:
|
|
34
|
+
return short_flag_match.group(1)
|
|
35
|
+
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_pr_body(body: str) -> list[str]:
|
|
40
|
+
violations = []
|
|
41
|
+
body_lower = body.lower()
|
|
42
|
+
|
|
43
|
+
missing_required_sections = [
|
|
44
|
+
header for header in REQUIRED_PR_SECTION_HEADERS
|
|
45
|
+
if f"## {header}" not in body_lower and f"**{header}" not in body_lower
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
if missing_required_sections:
|
|
49
|
+
formatted_sections = ", ".join(f"'{each_section.title()}'" for each_section in missing_required_sections)
|
|
50
|
+
violations.append(f"Missing required section(s): {formatted_sections}")
|
|
51
|
+
|
|
52
|
+
stripped_body = re.sub(r'#.*', '', body).strip()
|
|
53
|
+
stripped_body = re.sub(r'\*\*.*?\*\*', '', stripped_body).strip()
|
|
54
|
+
if len(stripped_body) < MINIMUM_PR_BODY_LENGTH:
|
|
55
|
+
violations.append("PR body too short -- provide meaningful context for reviewers")
|
|
56
|
+
|
|
57
|
+
vague_matches = VAGUE_LANGUAGE_PATTERN.findall(body)
|
|
58
|
+
if vague_matches:
|
|
59
|
+
violations.append(f"Vague language detected: {', '.join(vague_matches)} -- be specific about what changed and why")
|
|
60
|
+
|
|
61
|
+
return violations
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
7
65
|
try:
|
|
8
66
|
input_data = json.load(sys.stdin)
|
|
9
67
|
except json.JSONDecodeError:
|
|
@@ -19,62 +77,29 @@ def main():
|
|
|
19
77
|
|
|
20
78
|
is_pr_create = "gh pr create" in command and ("--body" in command or "-b " in command)
|
|
21
79
|
is_pr_edit = "gh pr edit" in command and "--body" in command
|
|
22
|
-
is_commit = re.search(r'git commit\b', command) and ("-m " in command or "-m\"" in command or "-m'" in command)
|
|
23
80
|
|
|
24
|
-
if not (is_pr_create or is_pr_edit
|
|
81
|
+
if not (is_pr_create or is_pr_edit):
|
|
25
82
|
sys.exit(0)
|
|
26
83
|
|
|
27
|
-
body =
|
|
28
|
-
if is_pr_create or is_pr_edit:
|
|
29
|
-
body_match = re.search(r'--body\s+"([^"]*)"', command) or re.search(r"--body\s+'([^']*)'", command)
|
|
30
|
-
if body_match:
|
|
31
|
-
body = body_match.group(1)
|
|
32
|
-
heredoc_match = re.search(r'--body\s+"\$\(cat <<', command)
|
|
33
|
-
if heredoc_match:
|
|
34
|
-
body = command[heredoc_match.start():]
|
|
35
|
-
|
|
36
|
-
if is_commit:
|
|
37
|
-
msg_match = re.search(r'-m\s+"([^"]*)"', command) or re.search(r"-m\s+'([^']*)'", command)
|
|
38
|
-
if msg_match:
|
|
39
|
-
body = msg_match.group(1)
|
|
40
|
-
heredoc_match = re.search(r'-m\s+"\$\(cat <<', command)
|
|
41
|
-
if heredoc_match:
|
|
42
|
-
body = command[heredoc_match.start():]
|
|
84
|
+
body = extract_body_from_command(command)
|
|
43
85
|
|
|
44
86
|
if not body:
|
|
45
87
|
sys.exit(0)
|
|
46
88
|
|
|
47
|
-
violations =
|
|
48
|
-
|
|
49
|
-
if is_pr_create or is_pr_edit:
|
|
50
|
-
if "## Summary" not in body and "## summary" not in body.lower():
|
|
51
|
-
violations.append("Missing '## Summary' section")
|
|
52
|
-
|
|
53
|
-
has_file_bold = bool(re.search(r'\*\*\w+\.\w+\*\*', body))
|
|
54
|
-
has_bullet_section = bool(re.search(r'###.*(?:test|config|fix)', body, re.IGNORECASE))
|
|
55
|
-
|
|
56
|
-
if not has_file_bold and not has_bullet_section:
|
|
57
|
-
violations.append("Production changes must be grouped by file with **filename** bold headers explaining WHY")
|
|
58
|
-
|
|
59
|
-
jargon_patterns = [
|
|
60
|
-
(r'\bDexie\b', "Dexie (say 'database' or 'local database')"),
|
|
61
|
-
(r'\bReact Query\b', "React Query (say 'cache' or 'data cache')"),
|
|
62
|
-
(r'\bsyncStatus\b', "syncStatus (describe the behavior, not the field)"),
|
|
63
|
-
(r'\blocalUpdatedAt\b', "localUpdatedAt (describe the behavior, not the field)"),
|
|
64
|
-
(r'\bpullStartedAt\b', "pullStartedAt (describe the behavior, not the field)"),
|
|
65
|
-
(r'\buseMutation\b', "useMutation (describe what it does for the user)"),
|
|
66
|
-
]
|
|
67
|
-
for pattern, name in jargon_patterns:
|
|
68
|
-
if re.search(pattern, body):
|
|
69
|
-
violations.append(f"Jargon detected: {name}")
|
|
89
|
+
violations = validate_pr_body(body)
|
|
70
90
|
|
|
71
91
|
if violations:
|
|
72
92
|
violation_list = "; ".join(violations)
|
|
93
|
+
pr_guide_reference = f" @{PR_GUIDE_PATH}" if os.path.exists(PR_GUIDE_PATH) else ""
|
|
94
|
+
denial_reason = (
|
|
95
|
+
f"BLOCKED: [PR_DESCRIPTION] {violation_list}. "
|
|
96
|
+
f"Follow the PR description guide:{pr_guide_reference}"
|
|
97
|
+
)
|
|
73
98
|
result = {
|
|
74
99
|
"hookSpecificOutput": {
|
|
75
100
|
"hookEventName": "PreToolUse",
|
|
76
101
|
"permissionDecision": "deny",
|
|
77
|
-
"permissionDecisionReason":
|
|
102
|
+
"permissionDecisionReason": denial_reason,
|
|
78
103
|
}
|
|
79
104
|
}
|
|
80
105
|
print(json.dumps(result))
|
package/hooks/hooks.json
CHANGED
|
@@ -44,11 +44,6 @@
|
|
|
44
44
|
"type": "command",
|
|
45
45
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/code-style-validator.py",
|
|
46
46
|
"timeout": 15
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"type": "command",
|
|
50
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/docker-settings-guard.py",
|
|
51
|
-
"timeout": 15
|
|
52
47
|
}
|
|
53
48
|
]
|
|
54
49
|
},
|
|
@@ -117,11 +112,6 @@
|
|
|
117
112
|
{
|
|
118
113
|
"matcher": "",
|
|
119
114
|
"hooks": [
|
|
120
|
-
{
|
|
121
|
-
"type": "command",
|
|
122
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/hook-structure-context.py",
|
|
123
|
-
"timeout": 10
|
|
124
|
-
},
|
|
125
115
|
{
|
|
126
116
|
"type": "command",
|
|
127
117
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/bulk-edit-reminder.py",
|
|
@@ -136,16 +126,6 @@
|
|
|
136
126
|
}
|
|
137
127
|
],
|
|
138
128
|
"SessionStart": [
|
|
139
|
-
{
|
|
140
|
-
"matcher": "compact",
|
|
141
|
-
"hooks": [
|
|
142
|
-
{
|
|
143
|
-
"type": "command",
|
|
144
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/compact-context-reinject.py",
|
|
145
|
-
"timeout": 10
|
|
146
|
-
}
|
|
147
|
-
]
|
|
148
|
-
},
|
|
149
129
|
{
|
|
150
130
|
"matcher": "",
|
|
151
131
|
"hooks": [
|
|
@@ -11,12 +11,27 @@ This catches:
|
|
|
11
11
|
Works in both WSL and Windows for any Python project with a git root.
|
|
12
12
|
Project root is discovered via CLAUDE_PROJECT_ROOT env var or git rev-parse.
|
|
13
13
|
"""
|
|
14
|
+
import importlib
|
|
14
15
|
import json
|
|
15
16
|
import os
|
|
16
17
|
import platform
|
|
17
18
|
import subprocess
|
|
18
19
|
import sys
|
|
19
20
|
from pathlib import Path
|
|
21
|
+
from types import ModuleType
|
|
22
|
+
|
|
23
|
+
NOTIFICATION_UTILS_DIRECTORY = os.path.join(
|
|
24
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
|
|
25
|
+
)
|
|
26
|
+
sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_notification_utils() -> ModuleType | None:
|
|
30
|
+
try:
|
|
31
|
+
return importlib.import_module("notification_utils")
|
|
32
|
+
except ImportError:
|
|
33
|
+
return None
|
|
34
|
+
|
|
20
35
|
|
|
21
36
|
IS_WINDOWS = platform.system() == "Windows"
|
|
22
37
|
|
|
@@ -104,6 +119,25 @@ def format_error_summary(all_error_lines: list[str]) -> str:
|
|
|
104
119
|
return error_summary
|
|
105
120
|
|
|
106
121
|
|
|
122
|
+
def send_block_notification(error_summary: str) -> None:
|
|
123
|
+
notification_module = load_notification_utils()
|
|
124
|
+
if notification_module is None:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
notification_title = "Mypy Type Errors"
|
|
128
|
+
notification_body = f"Write blocked: {error_summary[:200]}"
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if notification_module.is_wsl():
|
|
132
|
+
notification_module.notify_wsl(notification_title, notification_body)
|
|
133
|
+
elif platform.system() == "Linux":
|
|
134
|
+
notification_module.notify_linux()
|
|
135
|
+
elif platform.system() == "Windows":
|
|
136
|
+
notification_module.notify_windows(notification_title, notification_body)
|
|
137
|
+
except (AttributeError, OSError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
107
141
|
def build_block_response(error_summary: str) -> dict[str, str | dict[str, str]]:
|
|
108
142
|
return {
|
|
109
143
|
"decision": "block",
|
|
@@ -171,6 +205,7 @@ def main() -> None:
|
|
|
171
205
|
sys.exit(0)
|
|
172
206
|
|
|
173
207
|
error_summary = format_error_summary(all_error_lines)
|
|
208
|
+
send_block_notification(error_summary)
|
|
174
209
|
block_response = build_block_response(error_summary)
|
|
175
210
|
print(json.dumps(block_response))
|
|
176
211
|
sys.exit(0)
|
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
import importlib
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
6
|
+
import platform
|
|
5
7
|
import subprocess
|
|
6
8
|
import sys
|
|
7
9
|
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
|
|
12
|
+
NOTIFICATION_UTILS_DIRECTORY = os.path.join(
|
|
13
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
|
|
14
|
+
)
|
|
15
|
+
sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_notification_utils() -> ModuleType | None:
|
|
19
|
+
try:
|
|
20
|
+
return importlib.import_module("notification_utils")
|
|
21
|
+
except ImportError:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def send_format_notification(file_path: str, formatter_name: str) -> None:
|
|
26
|
+
notification_module = load_notification_utils()
|
|
27
|
+
if notification_module is None:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
notification_title = "Auto-Formatter"
|
|
31
|
+
notification_body = f"{formatter_name} formatted: {Path(file_path).name}"
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
if notification_module.is_wsl():
|
|
35
|
+
notification_module.notify_wsl(notification_title, notification_body)
|
|
36
|
+
elif platform.system() == "Linux":
|
|
37
|
+
notification_module.notify_linux()
|
|
38
|
+
elif platform.system() == "Windows":
|
|
39
|
+
notification_module.notify_windows(notification_title, notification_body)
|
|
40
|
+
except (AttributeError, OSError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
8
43
|
|
|
9
44
|
PYTHON_EXTENSIONS = {".py"}
|
|
10
45
|
JS_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}
|
|
@@ -89,6 +124,8 @@ def main() -> None:
|
|
|
89
124
|
try:
|
|
90
125
|
format_run = subprocess.run(each_formatter_command, capture_output=True, text=True, timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
|
|
91
126
|
if format_run.returncode == 0:
|
|
127
|
+
formatter_name = each_formatter_command[0] if each_formatter_command[0] != sys.executable else each_formatter_command[2]
|
|
128
|
+
send_format_notification(file_path, formatter_name)
|
|
92
129
|
break
|
|
93
130
|
except FileNotFoundError:
|
|
94
131
|
continue
|
|
@@ -98,12 +135,14 @@ def main() -> None:
|
|
|
98
135
|
if not has_prettier_config(file_path):
|
|
99
136
|
sys.exit(0)
|
|
100
137
|
try:
|
|
101
|
-
subprocess.run(
|
|
138
|
+
prettier_run = subprocess.run(
|
|
102
139
|
["npx", "--yes", "prettier", "--write", file_path],
|
|
103
140
|
capture_output=True,
|
|
104
141
|
text=True,
|
|
105
142
|
timeout=JS_FORMAT_TIMEOUT_SECONDS,
|
|
106
143
|
)
|
|
144
|
+
if prettier_run.returncode == 0:
|
|
145
|
+
send_format_notification(file_path, "prettier")
|
|
107
146
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
108
147
|
pass
|
|
109
148
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"skills/",
|
|
16
16
|
"hooks/"
|
|
17
17
|
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"claude-journal": "^1.3.0",
|
|
20
|
+
"claude-deep-research": "^1.0.0",
|
|
21
|
+
"claude-prompt-tools": "^1.0.0"
|
|
22
|
+
},
|
|
18
23
|
"keywords": [
|
|
19
24
|
"claude-code",
|
|
20
25
|
"plugin",
|
|
@@ -25,6 +30,7 @@
|
|
|
25
30
|
"license": "MIT",
|
|
26
31
|
"repository": {
|
|
27
32
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/jl-cmd/claude-code-config.git"
|
|
33
|
+
"url": "git+https://github.com/jl-cmd/claude-code-config.git",
|
|
34
|
+
"directory": "packages/claude-dev-env"
|
|
29
35
|
}
|
|
30
36
|
}
|
|
@@ -29,7 +29,7 @@ Scan the current repo root for these directories:
|
|
|
29
29
|
| `commands/` | Slash commands (.md) | `~/.claude/commands/` |
|
|
30
30
|
| `agents/` | Agent definitions (.md) | `~/.claude/agents/` |
|
|
31
31
|
| `skills/` | Skill packages (subdirs) | `~/.claude/skills/` |
|
|
32
|
-
| `hooks/` | Hook scripts + hooks.json | Merge into `~/.claude/settings.json` |
|
|
32
|
+
| `hooks/` | Hook scripts (+ optional hooks.json manifest) | Merge into `~/.claude/settings.json` |
|
|
33
33
|
|
|
34
34
|
Use Glob to find which directories exist and count files in each. Report findings to the user.
|
|
35
35
|
|
|
@@ -97,6 +97,10 @@ If hooks/hooks.json exists:
|
|
|
97
97
|
- Append new groups
|
|
98
98
|
- Write settings.json with `JSON.stringify(data, null, 4)`
|
|
99
99
|
|
|
100
|
+
Important runtime note:
|
|
101
|
+
- `~/.claude/settings.json` is the runtime source of truth for hooks.
|
|
102
|
+
- `hooks/hooks.json` is a packaging/install manifest input for merge workflows, not a runtime file Claude reads directly.
|
|
103
|
+
|
|
100
104
|
Print summary:
|
|
101
105
|
```
|
|
102
106
|
Installed <package-name>:
|
|
@@ -176,8 +180,8 @@ When ready to publish: `npm publish`
|
|
|
176
180
|
## Remember
|
|
177
181
|
|
|
178
182
|
- Zero external dependencies — only Node.js built-ins
|
|
179
|
-
- hooks.json in the repo stays canonical (python3 + ${CLAUDE_PLUGIN_ROOT})
|
|
180
|
-
- Rewriting happens only in the destination settings.json
|
|
183
|
+
- If present, hooks.json in the repo stays canonical (python3 + ${CLAUDE_PLUGIN_ROOT})
|
|
184
|
+
- Rewriting happens only in the destination settings.json (runtime source of truth)
|
|
181
185
|
- path.join() everywhere — never concatenate paths with `/` or `\`
|
|
182
186
|
- Idempotent: running twice produces the same result
|
|
183
187
|
- Log every file action so the user sees what changed
|