@hustle-together/api-dev-tools 3.12.3 → 3.12.16

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.
Files changed (96) hide show
  1. package/.claude/commands/hustle-build.md +259 -0
  2. package/.claude/commands/hustle-combine.md +1089 -0
  3. package/.claude/commands/hustle-ui-create-page.md +1078 -0
  4. package/.claude/commands/hustle-ui-create.md +1058 -0
  5. package/.claude/hooks/auto-answer.py +305 -0
  6. package/.claude/hooks/cache-research.py +337 -0
  7. package/.claude/hooks/check-api-routes.py +168 -0
  8. package/.claude/hooks/check-playwright-setup.py +103 -0
  9. package/.claude/hooks/check-storybook-setup.py +81 -0
  10. package/.claude/hooks/check-update.py +132 -0
  11. package/.claude/hooks/completion-promise-detector.py +293 -0
  12. package/.claude/hooks/context-capacity-warning.py +171 -0
  13. package/.claude/hooks/detect-interruption.py +165 -0
  14. package/.claude/hooks/docs-update-check.py +120 -0
  15. package/.claude/hooks/enforce-a11y-audit.py +202 -0
  16. package/.claude/hooks/enforce-brand-guide.py +241 -0
  17. package/.claude/hooks/enforce-component-type-confirm.py +97 -0
  18. package/.claude/hooks/enforce-dry-run.py +134 -0
  19. package/.claude/hooks/enforce-freshness.py +184 -0
  20. package/.claude/hooks/enforce-page-components.py +186 -0
  21. package/.claude/hooks/enforce-page-data-schema.py +155 -0
  22. package/.claude/hooks/enforce-questions-sourced.py +146 -0
  23. package/.claude/hooks/enforce-schema-from-interview.py +248 -0
  24. package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/.claude/hooks/enforce-ui-interview.py +130 -0
  26. package/.claude/hooks/generate-adr-options.py +282 -0
  27. package/.claude/hooks/generate-manifest-entry.py +1161 -0
  28. package/.claude/hooks/hook_utils.py +609 -0
  29. package/.claude/hooks/lib/__init__.py +1 -0
  30. package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  32. package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  33. package/.claude/hooks/lib/greptile.py +355 -0
  34. package/.claude/hooks/lib/ntfy.py +209 -0
  35. package/.claude/hooks/notify-input-needed.py +73 -0
  36. package/.claude/hooks/notify-phase-complete.py +90 -0
  37. package/.claude/hooks/ntfy-on-question.py +240 -0
  38. package/.claude/hooks/orchestrator-completion.py +313 -0
  39. package/.claude/hooks/orchestrator-handoff.py +267 -0
  40. package/.claude/hooks/orchestrator-session-startup.py +146 -0
  41. package/.claude/hooks/parallel-orchestrator.py +451 -0
  42. package/.claude/hooks/project-document-prompt.py +302 -0
  43. package/.claude/hooks/remote-question-proxy.py +284 -0
  44. package/.claude/hooks/remote-question-server.py +1224 -0
  45. package/.claude/hooks/run-code-review.py +393 -0
  46. package/.claude/hooks/run-visual-qa.py +338 -0
  47. package/.claude/hooks/session-logger.py +323 -0
  48. package/.claude/hooks/test-orchestrator-reground.py +248 -0
  49. package/.claude/hooks/track-scope-coverage.py +220 -0
  50. package/.claude/hooks/track-token-usage.py +121 -0
  51. package/.claude/hooks/update-adr-decision.py +236 -0
  52. package/.claude/hooks/update-api-showcase.py +161 -0
  53. package/.claude/hooks/update-registry.py +352 -0
  54. package/.claude/hooks/update-testing-checklist.py +195 -0
  55. package/.claude/hooks/update-ui-showcase.py +224 -0
  56. package/.claude/settings.local.json +7 -1
  57. package/.claude/test-auto-answer-bot.py +183 -0
  58. package/.claude/test-completion-detector.py +263 -0
  59. package/.claude/test-orchestrator-state.json +20 -0
  60. package/.claude/test-orchestrator.sh +271 -0
  61. package/.skills/api-create/SKILL.md +88 -3
  62. package/.skills/docs-sync/SKILL.md +260 -0
  63. package/.skills/hustle-build/SKILL.md +459 -0
  64. package/.skills/hustle-build-review/SKILL.md +518 -0
  65. package/CHANGELOG.md +87 -0
  66. package/README.md +86 -9
  67. package/bin/cli.js +1302 -88
  68. package/commands/hustle-api-create.md +22 -0
  69. package/commands/hustle-combine.md +81 -2
  70. package/commands/hustle-ui-create-page.md +84 -2
  71. package/commands/hustle-ui-create.md +82 -2
  72. package/hooks/auto-answer.py +228 -0
  73. package/hooks/check-update.py +132 -0
  74. package/hooks/ntfy-on-question.py +227 -0
  75. package/hooks/orchestrator-completion.py +313 -0
  76. package/hooks/orchestrator-handoff.py +189 -0
  77. package/hooks/orchestrator-session-startup.py +146 -0
  78. package/hooks/periodic-reground.py +230 -67
  79. package/hooks/update-api-showcase.py +13 -1
  80. package/hooks/update-ui-showcase.py +13 -1
  81. package/package.json +7 -3
  82. package/scripts/extract-schema-docs.cjs +322 -0
  83. package/templates/CLAUDE-SECTION.md +89 -64
  84. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  85. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  86. package/templates/api-showcase/_components/APITester.tsx +367 -58
  87. package/templates/docs/page.tsx +230 -0
  88. package/templates/hustle-build-defaults.json +84 -0
  89. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  90. package/templates/playwright-report/page.tsx +258 -0
  91. package/templates/settings.json +88 -7
  92. package/templates/test-results/page.tsx +237 -0
  93. package/templates/typedoc.json +19 -0
  94. package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
  95. package/templates/ui-showcase/page.tsx +1 -1
  96. package/.claude/api-dev-state.json +0 -466
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NTFY notification hook for AskUserQuestion.
4
+
5
+ Sends a push notification via NTFY when a question is asked,
6
+ allowing the user to be notified on their phone/desktop.
7
+
8
+ Hook Type: PostToolUse (matcher: AskUserQuestion)
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ try:
17
+ import requests
18
+ HAS_REQUESTS = True
19
+ except ImportError:
20
+ HAS_REQUESTS = False
21
+
22
+
23
+ def load_state():
24
+ """Load workflow state for context"""
25
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
26
+
27
+ # Check hustle-build state first
28
+ build_state = Path(project_dir) / ".claude" / "hustle-build-state.json"
29
+ if build_state.exists():
30
+ try:
31
+ return json.loads(build_state.read_text())
32
+ except Exception:
33
+ pass
34
+
35
+ # Check api-dev state
36
+ api_state = Path(project_dir) / ".claude" / "api-dev-state.json"
37
+ if api_state.exists():
38
+ try:
39
+ return json.loads(api_state.read_text())
40
+ except Exception:
41
+ pass
42
+
43
+ return {}
44
+
45
+
46
+ def get_ntfy_config():
47
+ """Get NTFY configuration from environment or .env file"""
48
+ topic = os.environ.get("NTFY_TOPIC")
49
+ server = os.environ.get("NTFY_SERVER", "https://ntfy.sh")
50
+
51
+ if not topic:
52
+ # Try loading from .env
53
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
54
+ env_file = Path(project_dir) / ".env"
55
+
56
+ if env_file.exists():
57
+ try:
58
+ for line in env_file.read_text().splitlines():
59
+ if line.startswith("NTFY_TOPIC="):
60
+ topic = line.split("=", 1)[1].strip().strip('"\'')
61
+ elif line.startswith("NTFY_SERVER="):
62
+ server = line.split("=", 1)[1].strip().strip('"\'')
63
+ except Exception:
64
+ pass
65
+
66
+ return topic, server
67
+
68
+
69
+ def send_notification(topic, server, title, message, priority="default", tags=None):
70
+ """Send notification via NTFY"""
71
+ if not HAS_REQUESTS:
72
+ # Fallback to curl
73
+ import subprocess
74
+ try:
75
+ headers = ["-H", f"Title: {title}", "-H", f"Priority: {priority}"]
76
+ if tags:
77
+ headers.extend(["-H", f"Tags: {','.join(tags)}"])
78
+
79
+ subprocess.run(
80
+ ["curl", "-s", "-d", message, *headers, f"{server}/{topic}"],
81
+ capture_output=True,
82
+ timeout=5
83
+ )
84
+ return True
85
+ except Exception:
86
+ return False
87
+
88
+ try:
89
+ headers = {
90
+ "Title": title,
91
+ "Priority": priority,
92
+ }
93
+ if tags:
94
+ headers["Tags"] = ",".join(tags)
95
+
96
+ response = requests.post(
97
+ f"{server}/{topic}",
98
+ data=message.encode("utf-8"),
99
+ headers=headers,
100
+ timeout=5
101
+ )
102
+ return response.status_code == 200
103
+ except Exception:
104
+ return False
105
+
106
+
107
+ def extract_question_summary(tool_input):
108
+ """Extract question summary from tool input"""
109
+ try:
110
+ data = json.loads(tool_input)
111
+ questions = data.get("questions", [])
112
+
113
+ if not questions:
114
+ return None, None
115
+
116
+ # Get first question
117
+ q = questions[0]
118
+ header = q.get("header", "Question")
119
+ question = q.get("question", "")
120
+ options = q.get("options", [])
121
+
122
+ # Truncate question for notification
123
+ if len(question) > 100:
124
+ question = question[:97] + "..."
125
+
126
+ # Add option count
127
+ option_text = f" ({len(options)} options)"
128
+
129
+ return header, question + option_text
130
+ except Exception:
131
+ return None, None
132
+
133
+
134
+ def main():
135
+ tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
136
+
137
+ # Check if in auto mode (skip notification in auto mode)
138
+ state = load_state()
139
+ if state.get("mode") == "auto":
140
+ # In auto mode, questions are auto-answered
141
+ # Only notify on errors, not questions
142
+ print(json.dumps({"continue": True}))
143
+ return
144
+
145
+ # Get NTFY config
146
+ topic, server = get_ntfy_config()
147
+
148
+ if not topic:
149
+ # NTFY not configured, skip
150
+ print(json.dumps({"continue": True}))
151
+ return
152
+
153
+ # Extract question info
154
+ header, message = extract_question_summary(tool_input)
155
+
156
+ if not header or not message:
157
+ print(json.dumps({"continue": True}))
158
+ return
159
+
160
+ # Build context for notification
161
+ workflow = state.get("workflow", "")
162
+ endpoint = state.get("active_endpoint") or state.get("active_element") or ""
163
+ phase = ""
164
+
165
+ # Find current phase
166
+ phases = state.get("phases", {})
167
+ for phase_name, phase_data in phases.items():
168
+ if phase_data.get("status") == "in_progress":
169
+ phase = phase_name.replace("_", " ").title()
170
+ break
171
+
172
+ # Build title
173
+ title_parts = ["Hustle Dev"]
174
+ if workflow:
175
+ title_parts.append(workflow)
176
+ if endpoint:
177
+ title_parts.append(endpoint)
178
+ title = " - ".join(title_parts)
179
+
180
+ # Build message
181
+ full_message = f"{header}: {message}"
182
+ if phase:
183
+ full_message = f"[{phase}] {full_message}"
184
+
185
+ # Determine priority
186
+ priority = "default"
187
+ tags = ["question", "hustle"]
188
+
189
+ # Higher priority for blocking questions
190
+ if "required" in message.lower() or "must" in message.lower():
191
+ priority = "high"
192
+ tags.append("warning")
193
+
194
+ # Send notification
195
+ success = send_notification(topic, server, title, full_message, priority, tags)
196
+
197
+ # Log notification
198
+ if success:
199
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
200
+ logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
201
+ logs_dir.mkdir(parents=True, exist_ok=True)
202
+
203
+ log_file = logs_dir / "ntfy-log.json"
204
+ try:
205
+ if log_file.exists():
206
+ log = json.loads(log_file.read_text())
207
+ else:
208
+ log = {"notifications": []}
209
+
210
+ from datetime import datetime
211
+ log["notifications"].append({
212
+ "timestamp": datetime.now().isoformat(),
213
+ "title": title,
214
+ "message": full_message,
215
+ "workflow": workflow,
216
+ "endpoint": endpoint
217
+ })
218
+
219
+ log_file.write_text(json.dumps(log, indent=2))
220
+ except Exception:
221
+ pass
222
+
223
+ print(json.dumps({"continue": True}))
224
+
225
+
226
+ if __name__ == "__main__":
227
+ main()
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Orchestrator completion hook.
4
+
5
+ After a Skill completes, this hook updates the orchestration state
6
+ and determines the next workflow to execute.
7
+
8
+ Hook Type: PostToolUse (matcher: Skill)
9
+ """
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+
16
+ try:
17
+ import requests
18
+ HAS_REQUESTS = True
19
+ except ImportError:
20
+ HAS_REQUESTS = False
21
+
22
+
23
+ def load_build_state():
24
+ """Load hustle-build orchestration state"""
25
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
26
+ state_file = Path(project_dir) / ".claude" / "hustle-build-state.json"
27
+
28
+ if state_file.exists():
29
+ try:
30
+ return json.loads(state_file.read_text())
31
+ except Exception:
32
+ pass
33
+ return None
34
+
35
+
36
+ def save_build_state(state):
37
+ """Save hustle-build orchestration state"""
38
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
39
+ state_file = Path(project_dir) / ".claude" / "hustle-build-state.json"
40
+
41
+ try:
42
+ state_file.write_text(json.dumps(state, indent=2))
43
+ return True
44
+ except Exception:
45
+ return False
46
+
47
+
48
+ def load_api_state():
49
+ """Load api-dev state to check completion"""
50
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
51
+ state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
52
+
53
+ if state_file.exists():
54
+ try:
55
+ return json.loads(state_file.read_text())
56
+ except Exception:
57
+ pass
58
+ return {}
59
+
60
+
61
+ def get_skill_name(tool_input):
62
+ """Extract skill name from tool input"""
63
+ try:
64
+ data = json.loads(tool_input)
65
+ return data.get("skill", "")
66
+ except Exception:
67
+ return ""
68
+
69
+
70
+ def check_workflow_complete(api_state):
71
+ """Check if the current workflow completed successfully"""
72
+ phases = api_state.get("phases", {})
73
+
74
+ # Check if documentation phase is complete (last phase)
75
+ doc_phase = phases.get("documentation", {})
76
+ if doc_phase.get("status") == "complete":
77
+ return True
78
+
79
+ # Alternative: check verification
80
+ verify_phase = phases.get("verify", {})
81
+ if verify_phase.get("status") == "complete":
82
+ return True
83
+
84
+ return False
85
+
86
+
87
+ def find_next_workflow(build_state):
88
+ """Find the next pending workflow based on dependencies"""
89
+ decomposition = build_state.get("decomposition", {})
90
+ completed_names = set()
91
+
92
+ # Collect completed workflow names
93
+ for wf_type in ["apis", "components", "combined_apis", "pages"]:
94
+ workflows = decomposition.get(wf_type, [])
95
+ for wf in workflows:
96
+ if wf.get("status") == "complete":
97
+ completed_names.add(wf.get("name"))
98
+
99
+ # Find first pending workflow with satisfied dependencies
100
+ for wf_type in ["apis", "components", "combined_apis", "pages"]:
101
+ workflows = decomposition.get(wf_type, [])
102
+ for wf in workflows:
103
+ if wf.get("status") != "pending":
104
+ continue
105
+
106
+ # Check dependencies
107
+ deps = wf.get("depends_on", [])
108
+ if all(dep in completed_names for dep in deps):
109
+ return wf, wf_type
110
+
111
+ return None, None
112
+
113
+
114
+ def send_ntfy(title, message, priority="default"):
115
+ """Send NTFY notification"""
116
+ topic = os.environ.get("NTFY_TOPIC")
117
+ server = os.environ.get("NTFY_SERVER", "https://ntfy.sh")
118
+
119
+ if not topic:
120
+ return
121
+
122
+ try:
123
+ if HAS_REQUESTS:
124
+ requests.post(
125
+ f"{server}/{topic}",
126
+ data=message.encode("utf-8"),
127
+ headers={"Title": title, "Priority": priority, "Tags": "hustle,check"},
128
+ timeout=5
129
+ )
130
+ else:
131
+ import subprocess
132
+ subprocess.run(
133
+ ["curl", "-s", "-d", message, "-H", f"Title: {title}", f"{server}/{topic}"],
134
+ capture_output=True,
135
+ timeout=5
136
+ )
137
+ except Exception:
138
+ pass
139
+
140
+
141
+ def main():
142
+ tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
143
+ tool_result = os.environ.get("CLAUDE_TOOL_RESULT", "")
144
+
145
+ # Get skill that completed
146
+ skill_name = get_skill_name(tool_input)
147
+
148
+ # Check if this is a workflow skill
149
+ workflow_skills = [
150
+ "api-create", "hustle-ui-create", "hustle-ui-create-page",
151
+ "hustle-combine", "cycle"
152
+ ]
153
+
154
+ if skill_name not in workflow_skills:
155
+ print(json.dumps({"continue": True}))
156
+ return
157
+
158
+ # Check if we're in an orchestrated build
159
+ build_state = load_build_state()
160
+
161
+ if not build_state or build_state.get("status") != "in_progress":
162
+ print(json.dumps({"continue": True}))
163
+ return
164
+
165
+ # Check if the workflow completed successfully
166
+ api_state = load_api_state()
167
+ workflow_complete = check_workflow_complete(api_state)
168
+
169
+ # Get active workflow info
170
+ active = build_state.get("active_sub_workflow", {})
171
+ active_name = active.get("name")
172
+ active_type = active.get("type")
173
+
174
+ # Update workflow status
175
+ decomposition = build_state.get("decomposition", {})
176
+
177
+ if workflow_complete and active_name:
178
+ # Mark workflow as complete
179
+ for wf_type in ["apis", "components", "combined_apis", "pages"]:
180
+ workflows = decomposition.get(wf_type, [])
181
+ for wf in workflows:
182
+ if wf.get("name") == active_name:
183
+ wf["status"] = "complete"
184
+ wf["completed_at"] = datetime.now().isoformat()
185
+
186
+ # Add to completed list
187
+ if "completed_sub_workflows" not in build_state:
188
+ build_state["completed_sub_workflows"] = []
189
+
190
+ build_state["completed_sub_workflows"].append({
191
+ "type": active_type,
192
+ "name": active_name,
193
+ "completed_at": datetime.now().isoformat()
194
+ })
195
+
196
+ # Find next workflow
197
+ next_wf, next_type = find_next_workflow(build_state)
198
+
199
+ if next_wf:
200
+ # Set next as active
201
+ build_state["active_sub_workflow"] = {
202
+ "type": next_type.rstrip("s"),
203
+ "name": next_wf.get("name"),
204
+ "workflow_id": f"wf-{len(build_state.get('completed_sub_workflows', []))+1:03d}"
205
+ }
206
+
207
+ # Mark as in progress
208
+ next_wf["status"] = "in_progress"
209
+ next_wf["started_at"] = datetime.now().isoformat()
210
+
211
+ save_build_state(build_state)
212
+
213
+ # Determine which skill to run
214
+ skill_mapping = {
215
+ "api": "/api-create",
216
+ "component": "/hustle-ui-create",
217
+ "combined_api": "/hustle-combine api",
218
+ "page": "/hustle-ui-create-page"
219
+ }
220
+
221
+ next_skill = skill_mapping.get(next_type.rstrip("s"), "/api-create")
222
+ next_name = next_wf.get("name")
223
+
224
+ context = f"""
225
+ ## Workflow Complete: {active_name}
226
+
227
+ The [{active_type}] **{active_name}** workflow has completed.
228
+
229
+ ### Next Workflow: [{next_type}] {next_name}
230
+
231
+ Run: `{next_skill} {next_name}`
232
+
233
+ Progress: {len(build_state.get('completed_sub_workflows', []))}/{sum(len(decomposition.get(t, [])) for t in decomposition)} complete
234
+ """
235
+
236
+ result = {
237
+ "continue": True,
238
+ "additionalContext": context
239
+ }
240
+
241
+ else:
242
+ # All workflows complete!
243
+ build_state["status"] = "complete"
244
+ build_state["completed_at"] = datetime.now().isoformat()
245
+ build_state["active_sub_workflow"] = None
246
+
247
+ save_build_state(build_state)
248
+
249
+ # Send completion notification
250
+ completed_count = len(build_state.get("completed_sub_workflows", []))
251
+ build_id = build_state.get("build_id", "unknown")
252
+
253
+ send_ntfy(
254
+ "Hustle Build Complete!",
255
+ f"All {completed_count} workflows finished.\nReview: /hustle-build-review {build_id}",
256
+ "high"
257
+ )
258
+
259
+ context = f"""
260
+ ## BUILD COMPLETE
261
+
262
+ All workflows have finished successfully!
263
+
264
+ **Build ID:** {build_id}
265
+ **Total Workflows:** {completed_count}
266
+ **Duration:** {calculate_duration(build_state)}
267
+
268
+ ### Created Elements:
269
+ {format_created_elements(build_state)}
270
+
271
+ ### Next Steps:
272
+ - `/hustle-build-review {build_id}` - Review all decisions and results
273
+ - `/commit` - Commit all changes
274
+ - `/pr` - Create pull request
275
+
276
+ Visit `/hustle-dev-dashboard` to see all created elements.
277
+ """
278
+
279
+ result = {
280
+ "continue": True,
281
+ "additionalContext": context
282
+ }
283
+
284
+ print(json.dumps(result))
285
+
286
+
287
+ def calculate_duration(build_state):
288
+ """Calculate build duration"""
289
+ try:
290
+ started = datetime.fromisoformat(build_state.get("created_at", ""))
291
+ ended = datetime.fromisoformat(build_state.get("completed_at", datetime.now().isoformat()))
292
+ duration = ended - started
293
+ minutes = int(duration.total_seconds() / 60)
294
+ return f"{minutes} minutes"
295
+ except Exception:
296
+ return "Unknown"
297
+
298
+
299
+ def format_created_elements(build_state):
300
+ """Format list of created elements"""
301
+ completed = build_state.get("completed_sub_workflows", [])
302
+ lines = []
303
+
304
+ for wf in completed:
305
+ wf_type = wf.get("type", "unknown")
306
+ name = wf.get("name", "unnamed")
307
+ lines.append(f" - [{wf_type}] {name}")
308
+
309
+ return "\n".join(lines) if lines else " None"
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()