@hustle-together/api-dev-tools 1.7.0 → 1.7.1

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.
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Hook: UserPromptSubmit
4
- Purpose: Enforce research before answering external API/SDK questions
4
+ Purpose: ALWAYS enforce research before answering technical questions
5
5
 
6
- This hook runs BEFORE Claude processes the user's prompt. It detects
7
- questions about external APIs, SDKs, or services and injects context
8
- requiring Claude to research first before answering.
6
+ This hook runs BEFORE Claude processes the user's prompt. It aggressively
7
+ detects ANY technical question and requires comprehensive research using
8
+ BOTH Context7 AND multiple WebSearches before answering.
9
9
 
10
- Philosophy: "When in doubt, research. Training data is ALWAYS potentially outdated."
10
+ Philosophy: "ALWAYS research. Training data is NEVER trustworthy for technical info."
11
+
12
+ The hook triggers on:
13
+ - ANY mention of APIs, SDKs, libraries, packages, frameworks
14
+ - ANY technical "how to" or capability questions
15
+ - ANY code-related questions (functions, methods, parameters, types)
16
+ - ANY questions about tools, services, or platforms
17
+ - ANY request for implementation, editing, or changes
11
18
 
12
19
  Returns:
13
20
  - Prints context to stdout (injected into conversation)
@@ -23,134 +30,172 @@ from datetime import datetime
23
30
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
24
31
 
25
32
  # ============================================================================
26
- # PATTERN-BASED DETECTION
33
+ # AGGRESSIVE DETECTION PATTERNS
27
34
  # ============================================================================
28
35
 
29
- # Patterns that indicate external service/API mentions
30
- EXTERNAL_SERVICE_PATTERNS = [
31
- # Package names
32
- r"@[\w-]+/[\w-]+", # @scope/package
33
- r"\b[\w-]+-(?:sdk|api|js|ts|py)\b", # something-sdk, something-api, something-js
34
-
35
- # API/SDK keywords
36
- r"\b(?:api|sdk|library|package|module|framework)\b",
37
-
38
- # Technical implementation terms
39
- r"\b(?:endpoint|route|webhook|oauth|auth|token)\b",
40
-
41
- # Version references
42
- r"\bv?\d+\.\d+(?:\.\d+)?\b", # version numbers like v1.2.3, 2.0
43
-
44
- # Import/require patterns
45
- r"(?:import|require|from)\s+['\"][\w@/-]+['\"]",
36
+ # Technical terms that ALWAYS trigger research
37
+ TECHNICAL_TERMS = [
38
+ # Code/Development
39
+ r"\b(?:function|method|class|interface|type|schema|model)\b",
40
+ r"\b(?:parameter|argument|option|config|setting|property)\b",
41
+ r"\b(?:import|export|require|module|package|library|dependency)\b",
42
+ r"\b(?:api|sdk|framework|runtime|engine|platform)\b",
43
+ r"\b(?:endpoint|route|url|path|request|response|header)\b",
44
+ r"\b(?:database|query|table|collection|document|record)\b",
45
+ r"\b(?:authentication|authorization|token|key|secret|credential)\b",
46
+ r"\b(?:error|exception|bug|issue|problem|fix)\b",
47
+ r"\b(?:test|spec|coverage|mock|stub|fixture)\b",
48
+ r"\b(?:deploy|build|compile|bundle|publish|release)\b",
49
+ r"\b(?:install|setup|configure|initialize|migrate)\b",
50
+ r"\b(?:provider|service|client|server|handler|middleware)\b",
51
+ r"\b(?:stream|async|await|promise|callback|event)\b",
52
+ r"\b(?:component|widget|element|view|layout|template)\b",
53
+ r"\b(?:state|store|reducer|action|context|hook)\b",
54
+ r"\b(?:validate|parse|serialize|transform|convert)\b",
55
+
56
+ # Package patterns
57
+ r"@[\w-]+/[\w-]+", # @scope/package
58
+ r"\b[\w-]+-(?:sdk|api|js|ts|py|go|rs)\b", # something-sdk, something-api
59
+
60
+ # Version patterns
61
+ r"\bv?\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?\b", # v1.2.3, 2.0.0-beta
62
+
63
+ # File patterns
64
+ r"\b[\w-]+\.(?:ts|js|tsx|jsx|py|go|rs|json|yaml|yml|toml|env)\b",
46
65
  ]
47
66
 
48
- # Patterns that indicate asking about features/capabilities
49
- CAPABILITY_QUESTION_PATTERNS = [
50
- # "What does X support/have/do"
51
- r"what\s+(?:does|can|are|is)\s+\w+",
52
- r"what\s+\w+\s+(?:support|have|provide|offer)",
53
-
54
- # "Does X support/have"
55
- r"(?:does|can|will)\s+\w+\s+(?:support|have|handle|do|work)",
56
-
57
- # "How to/do" questions
58
- r"how\s+(?:to|do|does|can|should)\s+",
59
-
60
- # Lists and availability
61
- r"(?:list|show)\s+(?:of|all|available)",
62
- r"which\s+\w+\s+(?:are|is)\s+(?:available|supported)",
63
- r"all\s+(?:available|supported)\s+\w+",
64
-
65
- # Examples and implementation
66
- r"example\s+(?:of|for|using|with)",
67
- r"how\s+to\s+(?:use|implement|integrate|connect|setup|configure)",
67
+ # Question patterns that indicate asking about functionality
68
+ QUESTION_PATTERNS = [
69
+ # Direct questions
70
+ r"\b(?:what|which|where|when|why|how)\b",
71
+ r"\b(?:can|could|would|should|will|does|do|is|are)\b.*\?",
72
+
73
+ # Requests
74
+ r"\b(?:show|tell|explain|describe|list|find|get|give)\b",
75
+ r"\b(?:help|need|want|looking for|trying to)\b",
76
+
77
+ # Actions
78
+ r"\b(?:create|make|build|add|implement|write|generate)\b",
79
+ r"\b(?:update|change|modify|edit|fix|refactor|improve)\b",
80
+ r"\b(?:delete|remove|drop|clear|reset)\b",
81
+ r"\b(?:connect|integrate|link|sync|merge)\b",
82
+ r"\b(?:debug|trace|log|monitor|track)\b",
83
+
84
+ # Comparisons
85
+ r"\b(?:difference|compare|versus|vs|between|or)\b",
86
+ r"\b(?:better|best|recommended|preferred|alternative)\b",
68
87
  ]
69
88
 
70
- # Common external service/company names (partial list - patterns catch the rest)
71
- KNOWN_SERVICES = [
72
- # AI/ML
73
- "openai", "anthropic", "google", "gemini", "gpt", "claude", "llama",
74
- "groq", "perplexity", "mistral", "cohere", "huggingface", "replicate",
75
-
76
- # Cloud/Infrastructure
77
- "aws", "azure", "gcp", "vercel", "netlify", "cloudflare", "supabase",
78
- "firebase", "mongodb", "postgres", "redis", "elasticsearch",
79
-
80
- # APIs/Services
81
- "stripe", "twilio", "sendgrid", "mailchimp", "slack", "discord",
82
- "github", "gitlab", "bitbucket", "jira", "notion", "airtable",
83
- "shopify", "salesforce", "hubspot", "zendesk",
84
-
85
- # Data/Analytics
86
- "segment", "mixpanel", "amplitude", "datadog", "sentry", "grafana",
87
-
88
- # Media/Content
89
- "cloudinary", "imgix", "mux", "brandfetch", "unsplash", "pexels",
90
-
91
- # Auth
92
- "auth0", "okta", "clerk", "nextauth", "passport",
89
+ # Phrases that ALWAYS require research (no exceptions)
90
+ ALWAYS_RESEARCH_PHRASES = [
91
+ r"how (?:to|do|does|can|should|would)",
92
+ r"what (?:is|are|does|can|should)",
93
+ r"(?:does|can|will|should) .+ (?:support|have|handle|work|do)",
94
+ r"(?:list|show|get|find) (?:all|available|supported)",
95
+ r"example (?:of|for|using|with|code)",
96
+ r"(?:implement|add|create|build|write|generate) .+",
97
+ r"(?:update|change|modify|edit|fix) .+",
98
+ r"(?:configure|setup|install|deploy) .+",
99
+ r"(?:error|issue|problem|bug|not working)",
100
+ r"(?:api|sdk|library|package|module|framework)",
101
+ r"(?:documentation|docs|reference|guide)",
102
+ ]
93
103
 
94
- # Payments
95
- "paypal", "square", "braintree", "adyen",
104
+ # Exclusion patterns - things that DON'T need research
105
+ EXCLUDE_PATTERNS = [
106
+ r"^(?:hi|hello|hey|thanks|thank you|ok|okay|yes|no|sure)[\s!?.]*$",
107
+ r"^(?:good morning|good afternoon|good evening|goodbye|bye)[\s!?.]*$",
108
+ r"^(?:please|sorry|excuse me)[\s!?.]*$",
109
+ r"^(?:\d+[\s+\-*/]\d+|calculate|math).*$", # Simple math
96
110
  ]
97
111
 
98
112
  # ============================================================================
99
113
  # DETECTION LOGIC
100
114
  # ============================================================================
101
115
 
102
- def detect_external_api_question(prompt: str) -> dict:
116
+ def is_excluded(prompt: str) -> bool:
117
+ """Check if prompt is a simple greeting or non-technical."""
118
+ prompt_clean = prompt.strip().lower()
119
+
120
+ # Very short prompts that are just greetings
121
+ if len(prompt_clean) < 20:
122
+ for pattern in EXCLUDE_PATTERNS:
123
+ if re.match(pattern, prompt_clean, re.IGNORECASE):
124
+ return True
125
+
126
+ return False
127
+
128
+
129
+ def detect_technical_question(prompt: str) -> dict:
103
130
  """
104
- Detect if the prompt is asking about external APIs/SDKs.
131
+ Aggressively detect if the prompt is technical and requires research.
105
132
 
106
133
  Returns:
107
134
  {
108
135
  "detected": bool,
109
136
  "terms": list of detected terms,
110
- "patterns_matched": list of pattern types matched,
111
- "confidence": "high" | "medium" | "low"
137
+ "patterns_matched": list of pattern types,
138
+ "confidence": "critical" | "high" | "medium" | "low" | "none"
112
139
  }
113
140
  """
141
+ if is_excluded(prompt):
142
+ return {
143
+ "detected": False,
144
+ "terms": [],
145
+ "patterns_matched": [],
146
+ "confidence": "none",
147
+ }
148
+
114
149
  prompt_lower = prompt.lower()
115
150
  detected_terms = []
116
151
  patterns_matched = []
117
152
 
118
- # Check for known services
119
- for service in KNOWN_SERVICES:
120
- if service in prompt_lower:
121
- detected_terms.append(service)
122
- patterns_matched.append("known_service")
123
-
124
- # Check external service patterns
125
- for pattern in EXTERNAL_SERVICE_PATTERNS:
153
+ # Check for ALWAYS_RESEARCH_PHRASES first (highest priority)
154
+ for pattern in ALWAYS_RESEARCH_PHRASES:
155
+ if re.search(pattern, prompt_lower, re.IGNORECASE):
156
+ patterns_matched.append("always_research")
157
+ # Extract the matched phrase
158
+ match = re.search(pattern, prompt_lower, re.IGNORECASE)
159
+ if match:
160
+ detected_terms.append(match.group(0)[:50])
161
+
162
+ # Check technical terms
163
+ for pattern in TECHNICAL_TERMS:
126
164
  matches = re.findall(pattern, prompt_lower, re.IGNORECASE)
127
165
  if matches:
128
- detected_terms.extend(matches)
129
- patterns_matched.append("external_service_pattern")
166
+ detected_terms.extend(matches[:3]) # Limit per pattern
167
+ patterns_matched.append("technical_term")
130
168
 
131
- # Check capability question patterns
132
- for pattern in CAPABILITY_QUESTION_PATTERNS:
169
+ # Check question patterns
170
+ for pattern in QUESTION_PATTERNS:
133
171
  if re.search(pattern, prompt_lower, re.IGNORECASE):
134
- patterns_matched.append("capability_question")
172
+ patterns_matched.append("question_pattern")
135
173
  break
136
174
 
137
175
  # Deduplicate
138
- detected_terms = list(set(detected_terms))
176
+ detected_terms = list(dict.fromkeys(detected_terms))[:10]
139
177
  patterns_matched = list(set(patterns_matched))
140
178
 
141
- # Determine confidence
142
- if "known_service" in patterns_matched and "capability_question" in patterns_matched:
179
+ # Determine confidence - MUCH more aggressive
180
+ if "always_research" in patterns_matched:
181
+ confidence = "critical"
182
+ elif "technical_term" in patterns_matched and "question_pattern" in patterns_matched:
143
183
  confidence = "high"
144
- elif "known_service" in patterns_matched or len(detected_terms) >= 2:
145
- confidence = "medium"
146
- elif patterns_matched:
147
- confidence = "low"
184
+ elif "technical_term" in patterns_matched:
185
+ confidence = "high" # Technical terms alone = high
186
+ elif "question_pattern" in patterns_matched and len(prompt) > 30:
187
+ confidence = "medium" # Questions longer than 30 chars
188
+ elif len(prompt) > 50:
189
+ confidence = "low" # Longer prompts default to low (still triggers)
148
190
  else:
149
191
  confidence = "none"
150
192
 
193
+ # AGGRESSIVE: Trigger on anything except "none"
194
+ detected = confidence != "none"
195
+
151
196
  return {
152
- "detected": confidence in ["high", "medium"],
153
- "terms": detected_terms[:10], # Limit to 10 terms
197
+ "detected": detected,
198
+ "terms": detected_terms,
154
199
  "patterns_matched": patterns_matched,
155
200
  "confidence": confidence,
156
201
  }
@@ -165,11 +210,11 @@ def check_active_workflow() -> bool:
165
210
  state = json.loads(STATE_FILE.read_text())
166
211
  phases = state.get("phases", {})
167
212
 
168
- # Check if any phase is in progress
169
213
  for phase_key, phase_data in phases.items():
170
214
  if isinstance(phase_data, dict):
171
215
  status = phase_data.get("status", "")
172
- if status in ["in_progress", "pending"]:
216
+ if status in ["in_progress", "pending", "complete"]:
217
+ # If ANY phase has been touched, we're in a workflow
173
218
  return True
174
219
 
175
220
  return False
@@ -177,50 +222,13 @@ def check_active_workflow() -> bool:
177
222
  return False
178
223
 
179
224
 
180
- def check_already_researched(terms: list) -> list:
181
- """Check which terms have already been researched."""
182
- if not STATE_FILE.exists():
183
- return []
184
-
185
- try:
186
- state = json.loads(STATE_FILE.read_text())
187
- research_queries = state.get("research_queries", [])
188
-
189
- # Also check sources in phases
190
- phases = state.get("phases", {})
191
- all_sources = []
192
- for phase_data in phases.values():
193
- if isinstance(phase_data, dict):
194
- sources = phase_data.get("sources", [])
195
- all_sources.extend(sources)
196
-
197
- # Combine all research text
198
- all_research_text = " ".join(str(s) for s in all_sources)
199
- all_research_text += " ".join(
200
- str(q.get("query", "")) + " " + str(q.get("term", ""))
201
- for q in research_queries
202
- if isinstance(q, dict)
203
- )
204
- all_research_text = all_research_text.lower()
205
-
206
- # Find which terms were already researched
207
- already_researched = []
208
- for term in terms:
209
- if term.lower() in all_research_text:
210
- already_researched.append(term)
211
-
212
- return already_researched
213
- except (json.JSONDecodeError, Exception):
214
- return []
215
-
216
-
217
- def log_detection(prompt: str, detection: dict) -> None:
225
+ def log_detection(prompt: str, detection: dict, injected: bool) -> None:
218
226
  """Log this detection for debugging/auditing."""
219
- if not STATE_FILE.exists():
220
- return
221
-
222
227
  try:
223
- state = json.loads(STATE_FILE.read_text())
228
+ if STATE_FILE.exists():
229
+ state = json.loads(STATE_FILE.read_text())
230
+ else:
231
+ state = {"prompt_detections": []}
224
232
 
225
233
  if "prompt_detections" not in state:
226
234
  state["prompt_detections"] = []
@@ -229,11 +237,13 @@ def log_detection(prompt: str, detection: dict) -> None:
229
237
  "timestamp": datetime.now().isoformat(),
230
238
  "prompt_preview": prompt[:100] + "..." if len(prompt) > 100 else prompt,
231
239
  "detection": detection,
240
+ "injected": injected,
232
241
  })
233
242
 
234
- # Keep only last 20 detections
235
- state["prompt_detections"] = state["prompt_detections"][-20:]
243
+ # Keep only last 50 detections
244
+ state["prompt_detections"] = state["prompt_detections"][-50:]
236
245
 
246
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
237
247
  STATE_FILE.write_text(json.dumps(state, indent=2))
238
248
  except Exception:
239
249
  pass # Don't fail the hook on logging errors
@@ -248,69 +258,69 @@ def main():
248
258
  try:
249
259
  input_data = json.load(sys.stdin)
250
260
  except json.JSONDecodeError:
251
- # If we can't parse input, allow without injection
252
261
  sys.exit(0)
253
262
 
254
263
  prompt = input_data.get("prompt", "")
255
264
 
256
- if not prompt:
265
+ if not prompt or len(prompt.strip()) < 5:
257
266
  sys.exit(0)
258
267
 
259
- # Check if in active workflow mode (stricter enforcement)
268
+ # Check if in active workflow mode
260
269
  active_workflow = check_active_workflow()
261
270
 
262
- # Detect external API questions
263
- detection = detect_external_api_question(prompt)
264
-
265
- # Log for debugging
266
- if detection["detected"] or active_workflow:
267
- log_detection(prompt, detection)
271
+ # Detect technical questions
272
+ detection = detect_technical_question(prompt)
268
273
 
269
- # Determine if we should inject research requirement
270
- should_inject = False
271
- inject_reason = ""
274
+ # In active workflow, ALWAYS inject (even for low confidence)
275
+ if active_workflow and detection["confidence"] != "none":
276
+ detection["detected"] = True
272
277
 
273
- if active_workflow:
274
- # In active workflow, ALWAYS inject for technical questions
275
- if detection["confidence"] in ["high", "medium", "low"]:
276
- should_inject = True
277
- inject_reason = "active_workflow"
278
- elif detection["detected"]:
279
- # Check if already researched
280
- already_researched = check_already_researched(detection["terms"])
281
- unresearched_terms = [t for t in detection["terms"] if t not in already_researched]
278
+ # Log all detections
279
+ log_detection(prompt, detection, detection["detected"])
282
280
 
283
- if unresearched_terms:
284
- should_inject = True
285
- inject_reason = "unresearched_terms"
286
- detection["unresearched"] = unresearched_terms
287
-
288
- # Inject context if needed
289
- if should_inject:
290
- terms_str = ", ".join(detection.get("unresearched", detection["terms"])[:5])
281
+ # Inject context if detected
282
+ if detection["detected"]:
283
+ terms_str = ", ".join(detection["terms"][:5]) if detection["terms"] else "technical question"
284
+ confidence = detection["confidence"]
291
285
 
286
+ # Build the injection message
292
287
  injection = f"""
293
288
  <user-prompt-submit-hook>
294
- EXTERNAL API/SDK DETECTED: {terms_str}
295
- Confidence: {detection["confidence"]}
296
- {"Mode: Active API Development Workflow" if active_workflow else ""}
297
-
298
- MANDATORY RESEARCH REQUIREMENT:
299
- Before answering this question, you MUST:
300
-
301
- 1. Use Context7 (mcp__context7__resolve-library-id + get-library-docs) to look up current documentation
302
- 2. Use WebSearch to find official documentation and recent updates
303
- 3. NEVER answer from training data alone - it may be outdated
304
-
305
- Training data can be months or years old. APIs change constantly.
306
- Research first. Then answer with verified, current information.
307
-
308
- After researching, cite your sources in your response.
289
+ RESEARCH REQUIRED - {confidence.upper()} CONFIDENCE
290
+ Detected: {terms_str}
291
+ {"MODE: Active API Development Workflow - STRICT ENFORCEMENT" if active_workflow else ""}
292
+
293
+ MANDATORY BEFORE ANSWERING:
294
+
295
+ 1. USE CONTEXT7 FIRST:
296
+ - Call mcp__context7__resolve-library-id to find the library
297
+ - Call mcp__context7__get-library-docs to get CURRENT documentation
298
+ - This gives you the ACTUAL source of truth
299
+
300
+ 2. USE WEBSEARCH (2-3 SEARCHES MINIMUM):
301
+ - Search for official documentation
302
+ - Search with different phrasings to get comprehensive coverage
303
+ - Search for recent updates, changes, or known issues
304
+ - Example searches:
305
+ * "[topic] official documentation"
306
+ * "[topic] API reference guide"
307
+ * "[topic] latest updates 2024 2025"
308
+
309
+ 3. NEVER TRUST TRAINING DATA:
310
+ - Training data can be months or years outdated
311
+ - APIs change constantly
312
+ - Features get added, deprecated, or modified
313
+ - Parameter names and types change
314
+
315
+ 4. CITE YOUR SOURCES:
316
+ - After researching, mention where the information came from
317
+ - Include links when available
318
+
319
+ RESEARCH FIRST. ANSWER SECOND.
309
320
  </user-prompt-submit-hook>
310
321
  """
311
322
  print(injection)
312
323
 
313
- # Always allow the prompt to proceed
314
324
  sys.exit(0)
315
325
 
316
326
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Interview-driven API development workflow for Claude Code - Automates research, testing, and documentation",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {