@hustle-together/api-dev-tools 1.0.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.
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: Stop
4
+ Purpose: Check if all required phases are complete before allowing stop
5
+
6
+ This hook runs when Claude tries to stop/end the conversation.
7
+ It checks api-dev-state.json to ensure critical workflow phases completed.
8
+
9
+ Returns:
10
+ - {"decision": "approve"} - Allow stopping
11
+ - {"decision": "block", "reason": "..."} - Prevent stopping with explanation
12
+ """
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ # State file is in .claude/ directory (sibling to hooks/)
18
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
19
+
20
+ # Phases that MUST be complete before stopping
21
+ # These are the critical phases - others are optional
22
+ REQUIRED_PHASES = [
23
+ ("research_initial", "Initial research (Context7/WebSearch)"),
24
+ ("tdd_red", "TDD Red phase (failing tests written)"),
25
+ ("tdd_green", "TDD Green phase (tests passing)"),
26
+ ]
27
+
28
+ # Phases that SHOULD be complete (warning but don't block)
29
+ RECOMMENDED_PHASES = [
30
+ ("interview", "User interview"),
31
+ ("schema_creation", "Schema creation"),
32
+ ("documentation", "Documentation updates"),
33
+ ]
34
+
35
+
36
+ def main():
37
+ # If no state file, we're not in an API workflow - allow stop
38
+ if not STATE_FILE.exists():
39
+ print(json.dumps({"decision": "approve"}))
40
+ sys.exit(0)
41
+
42
+ # Load state
43
+ try:
44
+ state = json.loads(STATE_FILE.read_text())
45
+ except json.JSONDecodeError:
46
+ # Corrupted state, allow stop
47
+ print(json.dumps({"decision": "approve"}))
48
+ sys.exit(0)
49
+
50
+ phases = state.get("phases", {})
51
+
52
+ # Check if workflow was even started
53
+ research = phases.get("research_initial", {})
54
+ if research.get("status") == "not_started":
55
+ # Workflow not started, allow stop
56
+ print(json.dumps({"decision": "approve"}))
57
+ sys.exit(0)
58
+
59
+ # Check required phases
60
+ incomplete_required = []
61
+ for phase_key, phase_name in REQUIRED_PHASES:
62
+ phase = phases.get(phase_key, {})
63
+ status = phase.get("status", "not_started")
64
+ if status != "complete":
65
+ incomplete_required.append(f" - {phase_name} ({status})")
66
+
67
+ # Check recommended phases
68
+ incomplete_recommended = []
69
+ for phase_key, phase_name in RECOMMENDED_PHASES:
70
+ phase = phases.get(phase_key, {})
71
+ status = phase.get("status", "not_started")
72
+ if status != "complete":
73
+ incomplete_recommended.append(f" - {phase_name} ({status})")
74
+
75
+ # Block if required phases incomplete
76
+ if incomplete_required:
77
+ reason_parts = ["❌ API workflow has REQUIRED phases incomplete:\n"]
78
+ reason_parts.extend(incomplete_required)
79
+
80
+ if incomplete_recommended:
81
+ reason_parts.append("\n\n⚠️ Also recommended but not complete:")
82
+ reason_parts.extend(incomplete_recommended)
83
+
84
+ reason_parts.append("\n\nTo continue:")
85
+ reason_parts.append(" 1. Complete required phases above")
86
+ reason_parts.append(" 2. Use /api-status to see detailed progress")
87
+ reason_parts.append(" 3. Or manually mark phases complete in .claude/api-dev-state.json")
88
+
89
+ print(json.dumps({
90
+ "decision": "block",
91
+ "reason": "\n".join(reason_parts)
92
+ }))
93
+ sys.exit(0)
94
+
95
+ # Warn about recommended phases but allow
96
+ if incomplete_recommended:
97
+ # Allow but the reason will be shown to user
98
+ print(json.dumps({
99
+ "decision": "approve",
100
+ "message": f"""⚠️ API workflow completing with optional phases pending:
101
+ {chr(10).join(incomplete_recommended)}
102
+
103
+ Consider running /api-status to review what was skipped."""
104
+ }))
105
+ sys.exit(0)
106
+
107
+ # All phases complete
108
+ print(json.dumps({
109
+ "decision": "approve",
110
+ "message": "✅ API workflow completed successfully!"
111
+ }))
112
+ sys.exit(0)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Block writing API code if research phase not complete
5
+
6
+ This hook runs BEFORE Claude can write or edit files in /api/ directories.
7
+ It checks the api-dev-state.json file to ensure research was completed first.
8
+
9
+ Returns:
10
+ - {"permissionDecision": "allow"} - Let the tool run
11
+ - {"permissionDecision": "deny", "reason": "..."} - Block with explanation
12
+ """
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ # State file is in .claude/ directory (sibling to hooks/)
18
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
19
+
20
+
21
+ def main():
22
+ # Read hook input from stdin (Claude Code passes tool info as JSON)
23
+ try:
24
+ input_data = json.load(sys.stdin)
25
+ except json.JSONDecodeError:
26
+ # If we can't parse input, allow (fail open for safety)
27
+ print(json.dumps({"permissionDecision": "allow"}))
28
+ sys.exit(0)
29
+
30
+ tool_name = input_data.get("tool_name", "")
31
+ tool_input = input_data.get("tool_input", {})
32
+
33
+ # Get the file path being written/edited
34
+ file_path = tool_input.get("file_path", "")
35
+
36
+ # Only enforce for API route files
37
+ # Check for both /api/ and /api-test/ patterns
38
+ if "/api/" not in file_path and "/api-test/" not in file_path:
39
+ # Not an API file, allow without checking
40
+ print(json.dumps({"permissionDecision": "allow"}))
41
+ sys.exit(0)
42
+
43
+ # Also skip for test files - tests should be written before research completes
44
+ # (TDD Red phase)
45
+ if ".test." in file_path or "/__tests__/" in file_path:
46
+ print(json.dumps({"permissionDecision": "allow"}))
47
+ sys.exit(0)
48
+
49
+ # Skip for documentation/config files
50
+ if file_path.endswith(".md") or file_path.endswith(".json"):
51
+ print(json.dumps({"permissionDecision": "allow"}))
52
+ sys.exit(0)
53
+
54
+ # Check if state file exists
55
+ if not STATE_FILE.exists():
56
+ print(json.dumps({
57
+ "permissionDecision": "deny",
58
+ "reason": """❌ API development state not initialized.
59
+
60
+ Before writing API implementation code, you must:
61
+ 1. Run /api-create [endpoint-name] to start the workflow
62
+ OR
63
+ 2. Run /api-research [library-name] to research dependencies
64
+
65
+ This ensures you're working with current documentation, not outdated training data."""
66
+ }))
67
+ sys.exit(0)
68
+
69
+ # Load and check state
70
+ try:
71
+ state = json.loads(STATE_FILE.read_text())
72
+ except json.JSONDecodeError:
73
+ # Corrupted state file, allow but warn
74
+ print(json.dumps({"permissionDecision": "allow"}))
75
+ sys.exit(0)
76
+
77
+ # Check research phase status
78
+ phases = state.get("phases", {})
79
+ research = phases.get("research_initial", {})
80
+ research_status = research.get("status", "not_started")
81
+
82
+ if research_status != "complete":
83
+ sources_count = len(research.get("sources", []))
84
+ print(json.dumps({
85
+ "permissionDecision": "deny",
86
+ "reason": f"""❌ Cannot write API implementation code yet.
87
+
88
+ RESEARCH PHASE INCOMPLETE
89
+ Current status: {research_status}
90
+ Sources consulted: {sources_count}
91
+
92
+ REQUIRED ACTIONS:
93
+ 1. Complete research phase first
94
+ 2. Run: /api-research [library-name]
95
+ 3. Ensure Context7 or WebSearch has been used
96
+
97
+ WHY THIS MATTERS:
98
+ - Implementation must match CURRENT API documentation
99
+ - Training data may be outdated
100
+ - All parameters must be discovered before coding
101
+
102
+ Once research is complete, you can proceed with implementation."""
103
+ }))
104
+ sys.exit(0)
105
+
106
+ # Research complete, allow writing
107
+ print(json.dumps({"permissionDecision": "allow"}))
108
+ sys.exit(0)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for WebSearch, WebFetch, Context7 MCP
4
+ Purpose: Track all research activity in the state file
5
+
6
+ This hook runs AFTER Claude uses research tools (WebSearch, WebFetch, Context7).
7
+ It logs each research action to api-dev-state.json for:
8
+ - Auditing what research was done
9
+ - Verifying prerequisites before allowing implementation
10
+ - Providing visibility to the user
11
+
12
+ Returns:
13
+ - {"continue": true} - Always continues (logging only, no blocking)
14
+ """
15
+ import json
16
+ import sys
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ # State file is in .claude/ directory (sibling to hooks/)
21
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
22
+
23
+
24
+ def main():
25
+ # Read hook input from stdin
26
+ try:
27
+ input_data = json.load(sys.stdin)
28
+ except json.JSONDecodeError:
29
+ # Can't parse, just continue
30
+ print(json.dumps({"continue": True}))
31
+ sys.exit(0)
32
+
33
+ tool_name = input_data.get("tool_name", "")
34
+ tool_input = input_data.get("tool_input", {})
35
+ tool_output = input_data.get("tool_output", {})
36
+
37
+ # Only track research-related tools
38
+ research_tools = ["WebSearch", "WebFetch", "mcp__context7"]
39
+ is_research_tool = any(t in tool_name for t in research_tools)
40
+
41
+ if not is_research_tool:
42
+ print(json.dumps({"continue": True}))
43
+ sys.exit(0)
44
+
45
+ # Load or create state file
46
+ if STATE_FILE.exists():
47
+ try:
48
+ state = json.loads(STATE_FILE.read_text())
49
+ except json.JSONDecodeError:
50
+ state = create_initial_state()
51
+ else:
52
+ state = create_initial_state()
53
+
54
+ # Get or create research phase
55
+ phases = state.setdefault("phases", {})
56
+ research = phases.setdefault("research_initial", {
57
+ "status": "in_progress",
58
+ "sources": [],
59
+ "started_at": datetime.now().isoformat()
60
+ })
61
+
62
+ # Update status if not started
63
+ if research.get("status") == "not_started":
64
+ research["status"] = "in_progress"
65
+ research["started_at"] = datetime.now().isoformat()
66
+
67
+ # Get sources list
68
+ sources = research.setdefault("sources", [])
69
+
70
+ # Create source entry based on tool type
71
+ timestamp = datetime.now().isoformat()
72
+
73
+ if "context7" in tool_name.lower():
74
+ source_entry = {
75
+ "type": "context7",
76
+ "tool": tool_name,
77
+ "input": sanitize_input(tool_input),
78
+ "timestamp": timestamp,
79
+ "success": True
80
+ }
81
+ # Extract library info if available
82
+ if "libraryName" in tool_input:
83
+ source_entry["library"] = tool_input["libraryName"]
84
+ if "libraryId" in tool_input:
85
+ source_entry["library_id"] = tool_input["libraryId"]
86
+
87
+ elif tool_name == "WebSearch":
88
+ source_entry = {
89
+ "type": "websearch",
90
+ "query": tool_input.get("query", ""),
91
+ "timestamp": timestamp,
92
+ "success": True
93
+ }
94
+
95
+ elif tool_name == "WebFetch":
96
+ source_entry = {
97
+ "type": "webfetch",
98
+ "url": tool_input.get("url", ""),
99
+ "timestamp": timestamp,
100
+ "success": True
101
+ }
102
+
103
+ else:
104
+ # Generic research tool
105
+ source_entry = {
106
+ "type": "other",
107
+ "tool": tool_name,
108
+ "timestamp": timestamp,
109
+ "success": True
110
+ }
111
+
112
+ # Add to sources list
113
+ sources.append(source_entry)
114
+
115
+ # Update last activity timestamp
116
+ research["last_activity"] = timestamp
117
+ research["source_count"] = len(sources)
118
+
119
+ # Check if we have enough sources to consider research "complete"
120
+ # More robust criteria:
121
+ # - At least 2 sources total (prevents single accidental search from completing)
122
+ # - At least one of: Context7 docs fetch, WebFetch of docs page
123
+ # - At least one search (WebSearch or Context7 resolve)
124
+ context7_count = sum(1 for s in sources if s.get("type") == "context7")
125
+ websearch_count = sum(1 for s in sources if s.get("type") == "websearch")
126
+ webfetch_count = sum(1 for s in sources if s.get("type") == "webfetch")
127
+ total_sources = len(sources)
128
+
129
+ # Minimum threshold: 2+ sources with at least one being docs-related
130
+ has_docs = webfetch_count >= 1 or context7_count >= 1
131
+ has_search = websearch_count >= 1 or context7_count >= 1
132
+ sufficient = total_sources >= 2 and has_docs and has_search
133
+
134
+ # Auto-complete research if sufficient sources
135
+ if sufficient:
136
+ if research.get("status") == "in_progress":
137
+ research["status"] = "complete"
138
+ research["completed_at"] = timestamp
139
+ research["completion_reason"] = "sufficient_sources"
140
+ research["completion_summary"] = {
141
+ "total_sources": total_sources,
142
+ "context7_calls": context7_count,
143
+ "web_searches": websearch_count,
144
+ "doc_fetches": webfetch_count
145
+ }
146
+
147
+ # Save state file
148
+ STATE_FILE.write_text(json.dumps(state, indent=2))
149
+
150
+ # Return success
151
+ print(json.dumps({"continue": True}))
152
+ sys.exit(0)
153
+
154
+
155
+ def create_initial_state():
156
+ """Create initial state structure"""
157
+ return {
158
+ "version": "1.0.0",
159
+ "created_at": datetime.now().isoformat(),
160
+ "phases": {
161
+ "scope": {"status": "not_started"},
162
+ "research_initial": {"status": "not_started", "sources": []},
163
+ "interview": {"status": "not_started"},
164
+ "research_deep": {"status": "not_started", "sources": []},
165
+ "schema_creation": {"status": "not_started"},
166
+ "environment_check": {"status": "not_started"},
167
+ "tdd_red": {"status": "not_started"},
168
+ "tdd_green": {"status": "not_started"},
169
+ "tdd_refactor": {"status": "not_started"},
170
+ "documentation": {"status": "not_started"}
171
+ },
172
+ "verification": {
173
+ "all_sources_fetched": False,
174
+ "schema_matches_docs": False,
175
+ "tests_cover_params": False,
176
+ "all_tests_passing": False
177
+ }
178
+ }
179
+
180
+
181
+ def sanitize_input(tool_input):
182
+ """Remove potentially sensitive data from input before logging"""
183
+ sanitized = {}
184
+ for key, value in tool_input.items():
185
+ # Skip API keys or tokens
186
+ if any(sensitive in key.lower() for sensitive in ["key", "token", "secret", "password"]):
187
+ sanitized[key] = "[REDACTED]"
188
+ else:
189
+ sanitized[key] = value
190
+ return sanitized
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@hustle-together/api-dev-tools",
3
+ "version": "1.0.0",
4
+ "description": "Interview-driven API development workflow for Claude Code - Automates research, testing, and documentation",
5
+ "main": "bin/cli.js",
6
+ "bin": {
7
+ "api-dev-tools": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "commands/",
12
+ "hooks/",
13
+ "templates/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "test": "node bin/cli.js --scope=project"
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "claude-code",
23
+ "api-development",
24
+ "tdd",
25
+ "test-driven-development",
26
+ "interview-driven",
27
+ "api-testing",
28
+ "documentation",
29
+ "workflow",
30
+ "automation"
31
+ ],
32
+ "author": "Hustle Together",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/hustle-together/api-dev-tools.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/hustle-together/api-dev-tools/issues"
40
+ },
41
+ "homepage": "https://github.com/hustle-together/api-dev-tools#readme",
42
+ "engines": {
43
+ "node": ">=14.0.0"
44
+ }
45
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "created_at": null,
4
+ "endpoint": null,
5
+ "library": null,
6
+ "phases": {
7
+ "scope": {
8
+ "status": "not_started",
9
+ "description": "Initial scope understanding"
10
+ },
11
+ "research_initial": {
12
+ "status": "not_started",
13
+ "sources": [],
14
+ "description": "Context7/WebSearch research for live documentation"
15
+ },
16
+ "interview": {
17
+ "status": "not_started",
18
+ "questions": [],
19
+ "description": "Structured interview about requirements"
20
+ },
21
+ "research_deep": {
22
+ "status": "not_started",
23
+ "sources": [],
24
+ "description": "Deep dive based on interview answers"
25
+ },
26
+ "schema_creation": {
27
+ "status": "not_started",
28
+ "schema_file": null,
29
+ "description": "Zod schema creation from research"
30
+ },
31
+ "environment_check": {
32
+ "status": "not_started",
33
+ "keys_verified": [],
34
+ "keys_missing": [],
35
+ "description": "API key and environment verification"
36
+ },
37
+ "tdd_red": {
38
+ "status": "not_started",
39
+ "test_file": null,
40
+ "test_count": 0,
41
+ "description": "Write failing tests first"
42
+ },
43
+ "tdd_green": {
44
+ "status": "not_started",
45
+ "implementation_file": null,
46
+ "description": "Minimal implementation to pass tests"
47
+ },
48
+ "tdd_refactor": {
49
+ "status": "not_started",
50
+ "description": "Code cleanup while keeping tests green"
51
+ },
52
+ "documentation": {
53
+ "status": "not_started",
54
+ "files_updated": [],
55
+ "description": "Update manifests, OpenAPI, examples"
56
+ }
57
+ },
58
+ "verification": {
59
+ "all_sources_fetched": false,
60
+ "schema_matches_docs": false,
61
+ "tests_cover_params": false,
62
+ "all_tests_passing": false,
63
+ "coverage_percent": null
64
+ }
65
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-research.py"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "WebSearch|WebFetch|mcp__context7.*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/track-tool-use.py"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "Stop": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/api-workflow-check.py"
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
36
+ }