ima-claude 2.9.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/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: Enforce Atlassian MCP prerequisites before tool calls.
|
|
4
|
+
|
|
5
|
+
Three checks combined:
|
|
6
|
+
H3 — cloudId bootstrap: getAccessibleAtlassianResources must be called first
|
|
7
|
+
H4 — transitions: getTransitionsForJiraIssue must precede transitionJiraIssue
|
|
8
|
+
M5 — ADF body: Confluence page body must be a JSON string, not a raw object
|
|
9
|
+
|
|
10
|
+
State is tracked in a JSON file that expires after 1 hour (new session boundary).
|
|
11
|
+
Exit code 0 = allow tool to proceed (soft warnings only).
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
STATE_FILE = os.path.expanduser("~/.claude/.atlassian_session_state")
|
|
19
|
+
STALENESS_SECONDS = 3600 # 1 hour — new session after this gap
|
|
20
|
+
|
|
21
|
+
BOOTSTRAP_TOOL = "mcp__claude_ai_Atlassian__getAccessibleAtlassianResources"
|
|
22
|
+
TRANSITIONS_TOOL = "mcp__claude_ai_Atlassian__getTransitionsForJiraIssue"
|
|
23
|
+
TRANSITION_ISSUE_TOOL = "mcp__claude_ai_Atlassian__transitionJiraIssue"
|
|
24
|
+
CONFLUENCE_WRITE_TOOLS = {
|
|
25
|
+
"mcp__claude_ai_Atlassian__createConfluencePage",
|
|
26
|
+
"mcp__claude_ai_Atlassian__updateConfluencePage",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
BOOTSTRAP_WARNING = """Atlassian bootstrap: Call getAccessibleAtlassianResources first to obtain cloudId.
|
|
30
|
+
mcp__claude_ai_Atlassian__getAccessibleAtlassianResources
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
TRANSITIONS_WARNING = """Transition IDs are issue-specific — call getTransitionsForJiraIssue first.
|
|
34
|
+
mcp__claude_ai_Atlassian__getTransitionsForJiraIssue issueIdOrKey: "{key}"
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
ADF_WARNING = """ADF body must be a JSON string (JSON.stringify'd), not a raw object — #1 cause of Confluence failures.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_state():
|
|
42
|
+
"""Load session state, returning defaults if missing or stale."""
|
|
43
|
+
default = {"bootstrapped": False, "transitions_fetched": False, "timestamp": 0.0}
|
|
44
|
+
if not os.path.exists(STATE_FILE):
|
|
45
|
+
return default
|
|
46
|
+
try:
|
|
47
|
+
with open(STATE_FILE, "r") as f:
|
|
48
|
+
state = json.load(f)
|
|
49
|
+
if (time.time() - state.get("timestamp", 0)) > STALENESS_SECONDS:
|
|
50
|
+
return default
|
|
51
|
+
return state
|
|
52
|
+
except (json.JSONDecodeError, OSError):
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_state(state):
|
|
57
|
+
"""Write session state to disk."""
|
|
58
|
+
state_dir = os.path.dirname(STATE_FILE)
|
|
59
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
60
|
+
state["timestamp"] = time.time()
|
|
61
|
+
with open(STATE_FILE, "w") as f:
|
|
62
|
+
json.dump(state, f)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
input_data = json.load(sys.stdin)
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
|
|
70
|
+
tool_name = input_data.get("tool_name", "")
|
|
71
|
+
tool_input = input_data.get("tool_input", {})
|
|
72
|
+
|
|
73
|
+
# Only act on Atlassian MCP tools
|
|
74
|
+
if not tool_name.startswith("mcp__claude_ai_Atlassian__"):
|
|
75
|
+
sys.exit(0)
|
|
76
|
+
|
|
77
|
+
state = load_state()
|
|
78
|
+
|
|
79
|
+
# H3: If this IS the bootstrap tool, mark and exit silently
|
|
80
|
+
if tool_name == BOOTSTRAP_TOOL:
|
|
81
|
+
state["bootstrapped"] = True
|
|
82
|
+
save_state(state)
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
# H4: If this IS the transitions fetch tool, mark and exit silently
|
|
86
|
+
if tool_name == TRANSITIONS_TOOL:
|
|
87
|
+
state["transitions_fetched"] = True
|
|
88
|
+
save_state(state)
|
|
89
|
+
sys.exit(0)
|
|
90
|
+
|
|
91
|
+
warnings = []
|
|
92
|
+
|
|
93
|
+
# H3: Any other Atlassian tool requires bootstrap first
|
|
94
|
+
if not state["bootstrapped"]:
|
|
95
|
+
warnings.append(BOOTSTRAP_WARNING)
|
|
96
|
+
|
|
97
|
+
# H4: transitionJiraIssue requires getTransitionsForJiraIssue first
|
|
98
|
+
if tool_name == TRANSITION_ISSUE_TOOL and not state["transitions_fetched"]:
|
|
99
|
+
issue_key = tool_input.get("issueIdOrKey", "<issueIdOrKey>")
|
|
100
|
+
warnings.append(TRANSITIONS_WARNING.format(key=issue_key))
|
|
101
|
+
|
|
102
|
+
# M5: ADF body must be a JSON string, not a raw Python dict
|
|
103
|
+
if tool_name in CONFLUENCE_WRITE_TOOLS:
|
|
104
|
+
content_format = tool_input.get("contentFormat", "")
|
|
105
|
+
body = tool_input.get("body")
|
|
106
|
+
if content_format == "adf" and isinstance(body, dict):
|
|
107
|
+
warnings.append(ADF_WARNING)
|
|
108
|
+
|
|
109
|
+
for warning in warnings:
|
|
110
|
+
print(warning, file=sys.stderr)
|
|
111
|
+
|
|
112
|
+
sys.exit(0)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: BLOCK sed file editing via Bash.
|
|
4
|
+
|
|
5
|
+
Hard guard (exit 1 = block execution). sed edits are a symptom of a broken
|
|
6
|
+
workflow — Claude either didn't read the file first, didn't use Serena's
|
|
7
|
+
symbolic editing, or is working with a file that's too large.
|
|
8
|
+
|
|
9
|
+
Allowed through:
|
|
10
|
+
- Piped sed transforms (no -i, no file redirect)
|
|
11
|
+
- Data pipelines (echo ... | sed ...)
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
BLOCK_MESSAGE = """🚫 BLOCKED: sed file editing is never the right approach.
|
|
18
|
+
|
|
19
|
+
You're using sed because something went wrong. Stop and fix the root cause:
|
|
20
|
+
|
|
21
|
+
1. READ the file first — Edit/Write require a prior Read (did you skip this?)
|
|
22
|
+
2. Use Serena symbolic editing — replace_symbol_body, insert_after_symbol, insert_before_symbol
|
|
23
|
+
3. Use Edit tool for targeted string replacements
|
|
24
|
+
4. If the file is too large to read (>500 lines), that's a separate problem — the file needs refactoring
|
|
25
|
+
|
|
26
|
+
DO NOT retry with sed. Go back to step 1."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_sed_file_edit(command: str) -> bool:
|
|
30
|
+
"""Detect sed commands that mutate files (not piped transforms)."""
|
|
31
|
+
# sed -i (in-place edit) in any flag position
|
|
32
|
+
if re.search(r"\bsed\b.*\s-[^\s]*i", command):
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
# sed ... > file or sed ... >> file (redirect output to file)
|
|
36
|
+
if re.search(r"\bsed\b.+>{1,2}\s*\S+", command):
|
|
37
|
+
# But not if sed input is piped (e.g., echo x | sed ... > file is borderline,
|
|
38
|
+
# but still a file mutation via sed — block it)
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
input_data = json.load(sys.stdin)
|
|
46
|
+
except json.JSONDecodeError as e:
|
|
47
|
+
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
tool_name = input_data.get("tool_name", "")
|
|
51
|
+
tool_input = input_data.get("tool_input", {})
|
|
52
|
+
command = tool_input.get("command", "")
|
|
53
|
+
|
|
54
|
+
if tool_name != "Bash" or not command:
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
|
|
57
|
+
if is_sed_file_edit(command):
|
|
58
|
+
print(BLOCK_MESSAGE, file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook: Inject ima-claude foundational context into every session.
|
|
3
|
+
# Injects ima-claude foundational context (persona, agents, MCP routing).
|
|
4
|
+
# stdout goes into Claude's context.
|
|
5
|
+
|
|
6
|
+
cat << 'BOOTSTRAP'
|
|
7
|
+
## ima-claude: Active Plugin
|
|
8
|
+
|
|
9
|
+
### Default Persona: The Practitioner
|
|
10
|
+
|
|
11
|
+
A 25-year software development veteran. FP-first, composition-minded, anti-over-engineering.
|
|
12
|
+
Uses "we" not "I" — collaborative, humble, light-hearted. "Slow is smooth, smooth is fast."
|
|
13
|
+
|
|
14
|
+
### Memory Bootstrap
|
|
15
|
+
|
|
16
|
+
At session start, check memory before asking questions:
|
|
17
|
+
- Vestige: `mcp__vestige__search` for user preferences and project context
|
|
18
|
+
- Vestige: `mcp__vestige__intention action: "check"` for pending reminders
|
|
19
|
+
- Serena: `mcp__serena__list_memories` if in a Serena-activated project
|
|
20
|
+
|
|
21
|
+
### Memory Routing
|
|
22
|
+
|
|
23
|
+
| Store what | Where | Why |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Decisions, preferences, patterns, bugs | Vestige `smart_ingest` | Fades naturally if not referenced |
|
|
26
|
+
| Reference material (docs, standards, PRDs) | Qdrant `qdrant-store` | Permanent library |
|
|
27
|
+
| Session state, task progress | Serena `write_memory` | Project-scoped workbench |
|
|
28
|
+
| Future reminders | Vestige `intention` | Surfaces at next session |
|
|
29
|
+
|
|
30
|
+
Auto-store: "I prefer..." → Vestige preference. "Let's go with X because..." → Vestige decision. "The reason this failed..." → Vestige bug.
|
|
31
|
+
|
|
32
|
+
After completing work: store outcome in Vestige, reference material in Qdrant, session state in Serena.
|
|
33
|
+
|
|
34
|
+
### Orchestrator Protocol
|
|
35
|
+
|
|
36
|
+
You are the Orchestrator. Plan and delegate. Do NOT implement directly.
|
|
37
|
+
- Non-trivial work → `/ima-claude:task-planner` (decompose) → `/ima-claude:task-runner` (delegate)
|
|
38
|
+
- Trivial = single file, < 5 lines, no judgment calls
|
|
39
|
+
- Model selection: opus for orchestration, sonnet for implementation (default), haiku for lookups
|
|
40
|
+
|
|
41
|
+
### Available Agents
|
|
42
|
+
|
|
43
|
+
Delegate to named agents — they enforce model, tools, and permissions automatically.
|
|
44
|
+
|
|
45
|
+
| Agent | Model | Mode | Use For |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `ima-claude:explorer` | haiku | read-only | File discovery, codebase exploration |
|
|
48
|
+
| `ima-claude:implementer` | sonnet | full | Feature dev, bug fixes, refactoring |
|
|
49
|
+
| `ima-claude:reviewer` | sonnet | read-only | Code review, security audit, FP checks |
|
|
50
|
+
| `ima-claude:wp-developer` | sonnet | full | WordPress plugins, themes, WP-CLI, forms |
|
|
51
|
+
| `ima-claude:memory` | sonnet | full | Memory search, storage, consolidation |
|
|
52
|
+
|
|
53
|
+
### Code Navigation (Serena — REQUIRED when installed)
|
|
54
|
+
|
|
55
|
+
**Always prefer Serena over Read/Grep for code investigation.** 40-70% token savings.
|
|
56
|
+
|
|
57
|
+
| Instead of | Use |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Read file to understand structure | `mcp__serena__jet_brains_get_symbols_overview relative_path: "..."` |
|
|
60
|
+
| Grep for class/function definition | `mcp__serena__jet_brains_find_symbol name_path_pattern: "Name"` |
|
|
61
|
+
| Grep for callers/references | `mcp__serena__jet_brains_find_referencing_symbols name_path: "method"` |
|
|
62
|
+
|
|
63
|
+
Use Read only when you need the actual implementation body of a known, specific symbol.
|
|
64
|
+
|
|
65
|
+
### Complex Reasoning (Sequential Thinking — REQUIRED for analysis)
|
|
66
|
+
|
|
67
|
+
Use `mcp__sequential-thinking__sequentialthinking` before acting on:
|
|
68
|
+
- Debugging / root cause analysis / "why is this failing"
|
|
69
|
+
- Trade-off evaluation / "which approach"
|
|
70
|
+
- Architectural decisions / design choices
|
|
71
|
+
- Multi-step investigations where approach may change
|
|
72
|
+
|
|
73
|
+
### Other MCP Tools
|
|
74
|
+
|
|
75
|
+
| Signal | Tool |
|
|
76
|
+
|---|---|
|
|
77
|
+
| "latest", "2025/2026", "what's new" | Tavily |
|
|
78
|
+
| Library/framework API question | Context7 |
|
|
79
|
+
|
|
80
|
+
Before web tools: check Claude's knowledge → Context7 → then Tavily/WebFetch.
|
|
81
|
+
|
|
82
|
+
### Search Preference
|
|
83
|
+
|
|
84
|
+
Always prefer `rg` (ripgrep) over grep/find. Faster, respects .gitignore, recursive by default.
|
|
85
|
+
|
|
86
|
+
### Session Management
|
|
87
|
+
|
|
88
|
+
- `/ima-claude:save-session` — save to Serena memory
|
|
89
|
+
- `/ima-claude:resume-session` — load from Serena memory + Vestige context
|
|
90
|
+
BOOTSTRAP
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook: Warn about hardcoded CSS when Bootstrap utilities are available.
|
|
4
|
+
|
|
5
|
+
Checks (soft warning only, exit 0):
|
|
6
|
+
M3 — Inline styles in HTML/PHP that have Bootstrap 5 utility equivalents
|
|
7
|
+
M3 — CSS properties in .scss/.css only when Bootstrap context is detected
|
|
8
|
+
|
|
9
|
+
Applies to: Edit, Write on .html, .php, .scss, .css files.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
HTML_PHP_EXTENSIONS = (".html", ".php")
|
|
16
|
+
CSS_EXTENSIONS = (".scss", ".css")
|
|
17
|
+
ALL_EXTENSIONS = HTML_PHP_EXTENSIONS + CSS_EXTENSIONS
|
|
18
|
+
|
|
19
|
+
# Bootstrap context signals in CSS/SCSS files
|
|
20
|
+
BOOTSTRAP_CONTEXT = re.compile(
|
|
21
|
+
r"@import\s+['\"].*bootstrap|"
|
|
22
|
+
r"\$spacer\b|"
|
|
23
|
+
r"\bbs-"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Inline style patterns with their Bootstrap equivalents
|
|
27
|
+
INLINE_STYLE_PATTERNS = [
|
|
28
|
+
(re.compile(r'style=["\'][^"\']*margin', re.IGNORECASE), "margin-*", "m-* / mt-* / mb-* / ms-* / me-*"),
|
|
29
|
+
(re.compile(r'style=["\'][^"\']*padding', re.IGNORECASE), "padding-*", "p-* / pt-* / pb-* / ps-* / pe-*"),
|
|
30
|
+
(re.compile(r'style=["\'][^"\']*display\s*:\s*flex', re.IGNORECASE), "display: flex", "d-flex"),
|
|
31
|
+
(re.compile(r'style=["\'][^"\']*display\s*:\s*none', re.IGNORECASE), "display: none", "d-none"),
|
|
32
|
+
(re.compile(r'style=["\'][^"\']*text-align\s*:\s*center', re.IGNORECASE), "text-align: center", "text-center"),
|
|
33
|
+
(re.compile(r'style=["\'][^"\']*font-weight\s*:\s*bold', re.IGNORECASE), "font-weight: bold", "fw-bold"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
WARNING_HEADER = "⚠️ Hardcoded CSS detected — Bootstrap 5 utilities available:"
|
|
37
|
+
WARNING_EXAMPLES = (
|
|
38
|
+
' style="margin-top: 16px" → class="mt-3"\n'
|
|
39
|
+
' style="display: flex" → class="d-flex"\n'
|
|
40
|
+
' style="text-align: center" → class="text-center"\n'
|
|
41
|
+
" See /ima-bootstrap skill for utility-first patterns."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_bootstrap_context(content: str) -> bool:
|
|
46
|
+
return bool(BOOTSTRAP_CONTEXT.search(content))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_inline_style_issues(content: str) -> list[str]:
|
|
50
|
+
return [
|
|
51
|
+
f" {css_prop} → {utility}"
|
|
52
|
+
for pattern, css_prop, utility in INLINE_STYLE_PATTERNS
|
|
53
|
+
if pattern.search(content)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_content(tool_name: str, tool_input: dict) -> str:
|
|
58
|
+
if tool_name == "Write":
|
|
59
|
+
return tool_input.get("content", "")
|
|
60
|
+
# Edit: scan only new_string — what's being written now
|
|
61
|
+
return tool_input.get("new_string", "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
input_data = json.load(sys.stdin)
|
|
66
|
+
except json.JSONDecodeError:
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
tool_name = input_data.get("tool_name", "")
|
|
70
|
+
tool_input = input_data.get("tool_input", {})
|
|
71
|
+
file_path = tool_input.get("file_path", "")
|
|
72
|
+
|
|
73
|
+
if tool_name not in ("Edit", "Write"):
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
if not file_path.endswith(ALL_EXTENSIONS):
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
79
|
+
content = get_content(tool_name, tool_input)
|
|
80
|
+
if not content:
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
is_css_file = file_path.endswith(CSS_EXTENSIONS)
|
|
84
|
+
|
|
85
|
+
# CSS/SCSS files only warn when Bootstrap is confirmed in context
|
|
86
|
+
if is_css_file and not is_bootstrap_context(content):
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
issues = find_inline_style_issues(content)
|
|
90
|
+
if issues:
|
|
91
|
+
print(WARNING_HEADER, file=sys.stderr)
|
|
92
|
+
print(WARNING_EXAMPLES, file=sys.stderr)
|
|
93
|
+
|
|
94
|
+
sys.exit(0)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook: Warn about Composer autoload "files" entries that break PHPUnit.
|
|
4
|
+
|
|
5
|
+
M9 — Composer autoload files bug detection (from phpunit-wp skill).
|
|
6
|
+
|
|
7
|
+
After writing or editing a composer.json, checks whether autoload.files is populated.
|
|
8
|
+
Autoload files run BEFORE the test bootstrap defines ABSPATH/WPINC, causing WordPress
|
|
9
|
+
plugin files to fatal error during `composer install` in test environments.
|
|
10
|
+
Exit code 0 = soft warning via stderr.
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
WARNING = (
|
|
16
|
+
'⚠️ Composer autoload "files" detected — this can break PHPUnit tests.\n'
|
|
17
|
+
" Autoload files run BEFORE test bootstrap defines ABSPATH/WPINC.\n"
|
|
18
|
+
" WordPress plugin files will fatal error during `composer install`.\n"
|
|
19
|
+
" Fix: Move plugin files to autoload.classmap or load via bootstrap.\n"
|
|
20
|
+
" See /phpunit-wp skill for the full fix pattern."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_content(tool_name: str, tool_input: dict) -> str:
|
|
25
|
+
if tool_name == "Write":
|
|
26
|
+
return tool_input.get("content", "")
|
|
27
|
+
|
|
28
|
+
# Edit: file already written to disk — read it for the current state
|
|
29
|
+
file_path = tool_input.get("file_path", "")
|
|
30
|
+
try:
|
|
31
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
32
|
+
return f.read()
|
|
33
|
+
except OSError:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def has_autoload_files(content: str) -> bool:
|
|
38
|
+
try:
|
|
39
|
+
data = json.loads(content)
|
|
40
|
+
except (json.JSONDecodeError, ValueError):
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
files = data.get("autoload", {}).get("files", [])
|
|
44
|
+
return isinstance(files, list) and len(files) > 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
input_data = json.load(sys.stdin)
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
|
|
52
|
+
tool_name = input_data.get("tool_name", "")
|
|
53
|
+
tool_input = input_data.get("tool_input", {})
|
|
54
|
+
file_path = tool_input.get("file_path", "")
|
|
55
|
+
|
|
56
|
+
if tool_name not in ("Edit", "Write"):
|
|
57
|
+
sys.exit(0)
|
|
58
|
+
|
|
59
|
+
if not file_path.endswith("composer.json"):
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
content = get_content(tool_name, tool_input)
|
|
63
|
+
|
|
64
|
+
if not content:
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
if has_autoload_files(content):
|
|
68
|
+
print(WARNING, file=sys.stderr)
|
|
69
|
+
|
|
70
|
+
sys.exit(0)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook: Warn when a Markdown file is written to the project root.
|
|
4
|
+
|
|
5
|
+
M10 — Markdown files scattered in project root.
|
|
6
|
+
|
|
7
|
+
After a Write to a .md file, checks whether the file lands at the root level of the
|
|
8
|
+
project rather than in a designated docs subdirectory. Exempt files (README.md, etc.)
|
|
9
|
+
and files in docs/, .claude/, skills/, or hooks/ subdirectories are silently allowed.
|
|
10
|
+
Exit code 0 = soft warning via stderr.
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
EXEMPT_FILENAMES = {
|
|
18
|
+
"README.md",
|
|
19
|
+
"CLAUDE.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE.md",
|
|
22
|
+
"CONTRIBUTING.md",
|
|
23
|
+
"CODE_OF_CONDUCT.md",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
EXEMPT_DIRS = {"docs", ".claude", "skills", "hooks"}
|
|
27
|
+
|
|
28
|
+
WARNING = (
|
|
29
|
+
"⚠️ Markdown file written to project root — consider docs-organize structure:\n"
|
|
30
|
+
" docs/active/ — permanent documentation\n"
|
|
31
|
+
" docs/archive/ — historical records\n"
|
|
32
|
+
" docs/transient/ — ephemeral notes (git-ignored)"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_git_root(path: str) -> str:
|
|
37
|
+
"""Return the git repository root, or the file's directory as fallback.
|
|
38
|
+
|
|
39
|
+
Walks up from the file's directory to find the first existing ancestor,
|
|
40
|
+
so this works even when Claude is writing a file into a not-yet-created dir.
|
|
41
|
+
"""
|
|
42
|
+
search_dir = os.path.dirname(os.path.abspath(path))
|
|
43
|
+
while search_dir and not os.path.isdir(search_dir):
|
|
44
|
+
parent = os.path.dirname(search_dir)
|
|
45
|
+
if parent == search_dir:
|
|
46
|
+
break
|
|
47
|
+
search_dir = parent
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
cwd=search_dir,
|
|
55
|
+
)
|
|
56
|
+
if result.returncode == 0:
|
|
57
|
+
return result.stdout.strip()
|
|
58
|
+
except OSError:
|
|
59
|
+
pass
|
|
60
|
+
return search_dir
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_exempt(file_path: str, git_root: str) -> bool:
|
|
64
|
+
filename = os.path.basename(file_path)
|
|
65
|
+
if filename in EXEMPT_FILENAMES:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
rel = os.path.relpath(file_path, git_root)
|
|
69
|
+
parts = rel.split(os.sep)
|
|
70
|
+
|
|
71
|
+
# File is inside an exempt subdirectory
|
|
72
|
+
if len(parts) > 1 and parts[0] in EXEMPT_DIRS:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
input_data = json.load(sys.stdin)
|
|
80
|
+
except json.JSONDecodeError:
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
tool_name = input_data.get("tool_name", "")
|
|
84
|
+
tool_input = input_data.get("tool_input", {})
|
|
85
|
+
file_path = tool_input.get("file_path", "")
|
|
86
|
+
|
|
87
|
+
if tool_name != "Write":
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
if not file_path.endswith(".md"):
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
|
|
93
|
+
git_root = get_git_root(file_path)
|
|
94
|
+
rel = os.path.relpath(file_path, git_root)
|
|
95
|
+
|
|
96
|
+
# Only warn when the file is at the root level (no subdirectory component)
|
|
97
|
+
if os.path.dirname(rel) != "":
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
if is_exempt(file_path, git_root):
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
print(WARNING, file=sys.stderr)
|
|
104
|
+
sys.exit(0)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: Warn about using grep/find instead of ripgrep.
|
|
4
|
+
|
|
5
|
+
Warns (allows command to proceed):
|
|
6
|
+
- grep → suggests rg
|
|
7
|
+
- find -name → suggests rg --files -g pattern
|
|
8
|
+
|
|
9
|
+
The warning is shown to Claude, encouraging it to use rg for subsequent operations.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
VALIDATION_RULES = [
|
|
16
|
+
(
|
|
17
|
+
r"\bgrep\b(?!.*\|)",
|
|
18
|
+
"⚠️ PREFER ripgrep: Use 'rg' instead of 'grep' - faster, better defaults, respects .gitignore. "
|
|
19
|
+
"Please use 'rg' for the rest of this session. See /rg skill for usage.",
|
|
20
|
+
),
|
|
21
|
+
(
|
|
22
|
+
r"\bfind\s+\S+\s+-name\b",
|
|
23
|
+
"⚠️ PREFER ripgrep: Use 'rg --files -g \"pattern\"' instead of 'find -name' - faster, respects .gitignore. "
|
|
24
|
+
"Please use 'rg' for the rest of this session. See /rg skill for usage.",
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_command(command: str) -> list[str]:
|
|
30
|
+
issues = []
|
|
31
|
+
for pattern, message in VALIDATION_RULES:
|
|
32
|
+
if re.search(pattern, command):
|
|
33
|
+
issues.append(message)
|
|
34
|
+
return issues
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
input_data = json.load(sys.stdin)
|
|
39
|
+
except json.JSONDecodeError as e:
|
|
40
|
+
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
tool_name = input_data.get("tool_name", "")
|
|
44
|
+
tool_input = input_data.get("tool_input", {})
|
|
45
|
+
command = tool_input.get("command", "")
|
|
46
|
+
|
|
47
|
+
if tool_name != "Bash" or not command:
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
issues = validate_command(command)
|
|
51
|
+
|
|
52
|
+
if issues:
|
|
53
|
+
for message in issues:
|
|
54
|
+
print(f"{message}", file=sys.stderr)
|
|
55
|
+
# Exit code 0 allows command to proceed but stderr is shown to Claude as a warning
|
|
56
|
+
sys.exit(0)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook: Warn about custom FP utility definitions.
|
|
4
|
+
|
|
5
|
+
Checks (soft warning only, exit 0):
|
|
6
|
+
M1 — Custom pipe/compose using reduce/reduceRight
|
|
7
|
+
M1 — Custom curry with rest/spread params
|
|
8
|
+
M1 — Custom monads (Maybe, Either, Result, Option classes)
|
|
9
|
+
M1 — Custom pattern matching function named 'match'
|
|
10
|
+
|
|
11
|
+
Applies to: Edit, Write on .js, .ts, .mjs, .mts, .php files.
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
JS_PHP_EXTENSIONS = (".js", ".ts", ".mjs", ".mts", ".php")
|
|
18
|
+
|
|
19
|
+
# Import lines — these are safe, skip them
|
|
20
|
+
IMPORT_LINE = re.compile(r"^\s*(import|require|use)\b", re.MULTILINE)
|
|
21
|
+
|
|
22
|
+
FP_UTILITY_PATTERNS = [
|
|
23
|
+
# pipe/compose backed by reduce
|
|
24
|
+
re.compile(r"(function|const|let|var)\s+pipe\s*[=(].*reduce", re.DOTALL),
|
|
25
|
+
re.compile(r"(function|const|let|var)\s+compose\s*[=(].*reduce", re.DOTALL),
|
|
26
|
+
# PHP pipe/compose
|
|
27
|
+
re.compile(r"function\s+pipe\s*\("),
|
|
28
|
+
re.compile(r"function\s+compose\s*\("),
|
|
29
|
+
# curry definitions
|
|
30
|
+
re.compile(r"(function|const|let|var)\s+curry\s*[=(]"),
|
|
31
|
+
# monad classes
|
|
32
|
+
re.compile(r"class\s+(Maybe|Either|Result|Option)\b"),
|
|
33
|
+
# custom match pattern matching
|
|
34
|
+
re.compile(r"(function|const|let|var)\s+match\s*[=(][^;]*patterns", re.DOTALL),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
WARNING = (
|
|
38
|
+
"⚠️ Custom FP utility detected — js-fp/php-fp skills say: use native patterns instead.\n"
|
|
39
|
+
" No pipe/compose — use chained methods or intermediate variables\n"
|
|
40
|
+
" No curry — use closures and partial application naturally\n"
|
|
41
|
+
" No custom monads — use early returns, null coalescing, optional chaining"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_import_line(line: str) -> bool:
|
|
46
|
+
return bool(IMPORT_LINE.match(line))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def strip_import_lines(content: str) -> str:
|
|
50
|
+
return "\n".join(
|
|
51
|
+
line for line in content.splitlines()
|
|
52
|
+
if not is_import_line(line)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def has_fp_utility(content: str) -> bool:
|
|
57
|
+
cleaned = strip_import_lines(content)
|
|
58
|
+
return any(pattern.search(cleaned) for pattern in FP_UTILITY_PATTERNS)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_content(tool_name: str, tool_input: dict) -> str:
|
|
62
|
+
if tool_name == "Write":
|
|
63
|
+
return tool_input.get("content", "")
|
|
64
|
+
# Edit: scan only new_string — we care about what's being added
|
|
65
|
+
return tool_input.get("new_string", "")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
input_data = json.load(sys.stdin)
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
tool_name = input_data.get("tool_name", "")
|
|
74
|
+
tool_input = input_data.get("tool_input", {})
|
|
75
|
+
file_path = tool_input.get("file_path", "")
|
|
76
|
+
|
|
77
|
+
if tool_name not in ("Edit", "Write"):
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
if not file_path.endswith(JS_PHP_EXTENSIONS):
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
content = get_content(tool_name, tool_input)
|
|
84
|
+
if not content:
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
|
|
87
|
+
if has_fp_utility(content):
|
|
88
|
+
print(WARNING, file=sys.stderr)
|
|
89
|
+
|
|
90
|
+
sys.exit(0)
|