@hustle-together/api-dev-tools 3.12.3 → 4.5.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.
Files changed (159) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +7 -463
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. package/templates/ui-showcase/page.tsx +1 -1
@@ -17,6 +17,10 @@ v3.6.7 Enhancement:
17
17
  - Research cache location
18
18
  - Summary statistics
19
19
 
20
+ v3.12.13 Fix:
21
+ - Skip enforcement when running in source repository (developing the package)
22
+ - Detect via package.json name = @hustle-together/api-dev-tools
23
+
20
24
  Returns:
21
25
  - {"decision": "approve"} - Allow stopping
22
26
  - {"decision": "block", "reason": "..."} - Prevent stopping with explanation
@@ -28,6 +32,36 @@ import re
28
32
  from datetime import datetime
29
33
  from pathlib import Path
30
34
 
35
+
36
+ def is_source_repository() -> bool:
37
+ """
38
+ Check if we're running in the api-dev-tools source repository.
39
+ If so, hooks should NOT enforce workflow - we're developing, not using.
40
+ """
41
+ try:
42
+ # Use parent of hooks dir (project root), not cwd which may be hooks/
43
+ project_root = Path(__file__).parent.parent
44
+ package_json = project_root / "package.json"
45
+ if package_json.exists():
46
+ data = json.loads(package_json.read_text())
47
+ # If this is the source repo, skip enforcement
48
+ if data.get("name") == "@hustle-together/api-dev-tools":
49
+ return True
50
+
51
+ # Also check for templates/ folder (only exists in source repo)
52
+ if (project_root / "templates").is_dir():
53
+ return True
54
+
55
+ except Exception:
56
+ pass
57
+ return False
58
+
59
+
60
+ # Skip enforcement in source repository
61
+ if is_source_repository():
62
+ print(json.dumps({"decision": "approve"}))
63
+ sys.exit(0)
64
+
31
65
  # State file is in .claude/ directory (sibling to hooks/)
32
66
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
33
67
  RESEARCH_DIR = Path(__file__).parent.parent / "research"
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-answer hook for --auto mode.
4
+
5
+ This hook intercepts AskUserQuestion calls when running in auto-mode
6
+ and either:
7
+ 1. Uses pre-configured defaults from hustle-build-defaults.json
8
+ 2. Spawns a Haiku sub-agent to pick the most comprehensive option
9
+
10
+ Hook Type: PreToolUse (matcher: AskUserQuestion)
11
+
12
+ Updated in v4.5.0:
13
+ - Use shared hook_utils for logging
14
+ - Log all auto-answered questions to workflow logs
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # Import shared utilities
23
+ try:
24
+ from hook_utils import log_workflow_event
25
+ UTILS_AVAILABLE = True
26
+ except ImportError:
27
+ UTILS_AVAILABLE = False
28
+
29
+
30
+ def load_state():
31
+ """Load workflow state to check if in auto mode"""
32
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
33
+
34
+ # Check hustle-build state first
35
+ build_state = Path(project_dir) / ".claude" / "hustle-build-state.json"
36
+ if build_state.exists():
37
+ try:
38
+ state = json.loads(build_state.read_text())
39
+ if state.get("mode") == "auto":
40
+ return state, "build"
41
+ except Exception:
42
+ pass
43
+
44
+ # Check api-dev state
45
+ api_state = Path(project_dir) / ".claude" / "api-dev-state.json"
46
+ if api_state.exists():
47
+ try:
48
+ state = json.loads(api_state.read_text())
49
+ if state.get("mode") == "auto":
50
+ return state, "workflow"
51
+ except Exception:
52
+ pass
53
+
54
+ return None, None
55
+
56
+
57
+ def load_defaults():
58
+ """Load pre-configured default answers"""
59
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
60
+
61
+ # Check project-specific defaults first
62
+ defaults_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
63
+ if defaults_file.exists():
64
+ try:
65
+ return json.loads(defaults_file.read_text())
66
+ except Exception:
67
+ pass
68
+
69
+ # Fall back to template defaults
70
+ template_defaults = Path(project_dir) / "templates" / "hustle-build-defaults.json"
71
+ if template_defaults.exists():
72
+ try:
73
+ return json.loads(template_defaults.read_text())
74
+ except Exception:
75
+ pass
76
+
77
+ return {}
78
+
79
+
80
+ def is_autonomous_enabled():
81
+ """Check if autonomous mode is enabled by default in settings"""
82
+ defaults = load_defaults()
83
+ autonomous = defaults.get("autonomous", {})
84
+ return autonomous.get("enabled", False) and autonomous.get("skip_interviews", False)
85
+
86
+
87
+ def find_comprehensive_option(options):
88
+ """
89
+ Find the most comprehensive option based on keywords.
90
+
91
+ Comprehensive options typically include words like:
92
+ - "all", "full", "complete", "comprehensive"
93
+ - Higher numbers (e.g., "100%" vs "50%")
94
+ - More features listed
95
+
96
+ Also prioritizes affirmative options for phase exits:
97
+ - "yes", "proceed", "continue", "approve", "confirm"
98
+ """
99
+ if not options:
100
+ return None
101
+
102
+ comprehensive_keywords = [
103
+ "all", "full", "complete", "comprehensive", "everything",
104
+ "maximum", "extensive", "detailed", "thorough", "wcag-aa"
105
+ ]
106
+
107
+ # Affirmative keywords for phase exit questions
108
+ affirmative_keywords = [
109
+ "yes", "proceed", "continue", "approve", "confirm",
110
+ "accept", "ready", "go ahead", "move forward",
111
+ "auto", "defaults", "use auto", "use defaults"
112
+ ]
113
+
114
+ # Negative keywords to avoid
115
+ negative_keywords = [
116
+ "no", "skip", "cancel", "stop", "more research", "not ready"
117
+ ]
118
+
119
+ # Score each option
120
+ scored = []
121
+ for i, opt in enumerate(options):
122
+ label = opt.get("label", "").lower()
123
+ description = opt.get("description", "").lower()
124
+ text = f"{label} {description}"
125
+
126
+ score = 0
127
+
128
+ # Check for negative keywords first (penalize heavily)
129
+ for keyword in negative_keywords:
130
+ if keyword in text:
131
+ score -= 50
132
+
133
+ # Check for comprehensive keywords
134
+ for keyword in comprehensive_keywords:
135
+ if keyword in text:
136
+ score += 10
137
+
138
+ # Check for affirmative keywords (high priority for phase exits)
139
+ for keyword in affirmative_keywords:
140
+ if keyword in text:
141
+ score += 25
142
+
143
+ # Check for "(Recommended)" suffix
144
+ if "recommended" in label.lower():
145
+ score += 20
146
+
147
+ # Prefer options with more content (longer descriptions = more features)
148
+ score += len(description) / 50
149
+
150
+ scored.append((i, score, opt))
151
+
152
+ # Sort by score descending
153
+ scored.sort(key=lambda x: x[1], reverse=True)
154
+
155
+ # Return the index of the best option (0-based)
156
+ if scored:
157
+ return scored[0][0]
158
+
159
+ return 0 # Default to first option
160
+
161
+
162
+ def get_question_key(questions):
163
+ """Extract a key from the question for lookup in defaults"""
164
+ if not questions or len(questions) == 0:
165
+ return None
166
+
167
+ q = questions[0]
168
+ header = q.get("header", "").lower().replace(" ", "_")
169
+ return header
170
+
171
+
172
+ def main():
173
+ # Get tool input from environment
174
+ tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
175
+
176
+ try:
177
+ input_data = json.loads(tool_input)
178
+ except Exception:
179
+ print(json.dumps({"continue": True}))
180
+ return
181
+
182
+ # Check if in auto mode (explicit flag OR defaults enabled)
183
+ state, state_type = load_state()
184
+ autonomous_by_default = is_autonomous_enabled()
185
+
186
+ if not state and not autonomous_by_default:
187
+ # Not in auto mode and autonomous not enabled, continue normally
188
+ print(json.dumps({"continue": True}))
189
+ return
190
+
191
+ # If no state but autonomous is enabled, create a minimal state
192
+ if not state and autonomous_by_default:
193
+ state = {"mode": "auto", "source": "defaults"}
194
+
195
+ # Load defaults
196
+ defaults = load_defaults()
197
+
198
+ questions = input_data.get("questions", [])
199
+ if not questions:
200
+ print(json.dumps({"continue": True}))
201
+ return
202
+
203
+ # Try to find pre-configured answer
204
+ question_key = get_question_key(questions)
205
+ answers = {}
206
+
207
+ for q in questions:
208
+ header = q.get("header", "")
209
+ options = q.get("options", [])
210
+ question_text = q.get("question", "")
211
+
212
+ # Check defaults first
213
+ default_answer = None
214
+ if question_key and question_key in defaults:
215
+ default_answer = defaults[question_key]
216
+ elif header.lower().replace(" ", "_") in defaults:
217
+ default_answer = defaults[header.lower().replace(" ", "_")]
218
+
219
+ if default_answer is not None:
220
+ # Use pre-configured default
221
+ answers[question_text] = default_answer
222
+ else:
223
+ # Auto-select comprehensive option
224
+ best_idx = find_comprehensive_option(options)
225
+ if best_idx is not None and options:
226
+ answers[question_text] = options[best_idx].get("label", "")
227
+
228
+ if answers:
229
+ # Log the auto-answer
230
+ log_auto_answer(state, questions, answers)
231
+
232
+ # Get the first question and answer for display
233
+ first_question = questions[0] if questions else {}
234
+ header = first_question.get("header", "Question")
235
+ question_text = first_question.get("question", "")
236
+ answer = list(answers.values())[0] if answers else "Unknown"
237
+
238
+ # BLOCK the tool and provide the answer in the reason
239
+ # This prevents the question UI from showing and tells the AI to use this answer
240
+ result = {
241
+ "continue": False,
242
+ "reason": f"""## 🤖 Auto-Selected
243
+
244
+ **{header}:** {answer}
245
+
246
+ _Question: {question_text}_
247
+
248
+ ---
249
+
250
+ Autonomous mode is active. The workflow will proceed with this answer.
251
+
252
+ To review auto-selected answers: `.claude/workflow-logs/`
253
+ To disable: Set `autonomous.enabled: false` in `.claude/hustle-build-defaults.json`
254
+ """
255
+ }
256
+ print(json.dumps(result))
257
+ else:
258
+ print(json.dumps({"continue": True}))
259
+
260
+
261
+ def log_auto_answer(state, questions, answers):
262
+ """Log auto-answered questions to workflow log using shared utility (v4.5.0)"""
263
+ # Use shared utility if available
264
+ if UTILS_AVAILABLE:
265
+ try:
266
+ log_workflow_event("auto_answer", {
267
+ "questions": [q.get("question") for q in questions],
268
+ "headers": [q.get("header") for q in questions],
269
+ "answers": answers,
270
+ "reason": "auto-comprehensive",
271
+ "mode": state.get("mode", "auto") if state else "auto"
272
+ })
273
+ return
274
+ except Exception:
275
+ pass
276
+
277
+ # Fallback to legacy logging
278
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
279
+ logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
280
+ logs_dir.mkdir(parents=True, exist_ok=True)
281
+
282
+ build_id = state.get("build_id", state.get("workflow_id", "unknown")) if state else "unknown"
283
+ log_file = logs_dir / f"{build_id}.json"
284
+
285
+ try:
286
+ if log_file.exists():
287
+ log = json.loads(log_file.read_text())
288
+ else:
289
+ log = {"auto_answers": [], "events": []}
290
+
291
+ from datetime import datetime
292
+ log["auto_answers"].append({
293
+ "timestamp": datetime.now().isoformat(),
294
+ "questions": [q.get("question") for q in questions],
295
+ "answers": answers,
296
+ "reason": "auto-comprehensive"
297
+ })
298
+
299
+ log_file.write_text(json.dumps(log, indent=2))
300
+ except Exception:
301
+ pass
302
+
303
+
304
+ if __name__ == "__main__":
305
+ main()
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Check for api-dev-tools updates at session start.
4
+
5
+ This hook runs at SessionStart and checks npm registry for newer versions.
6
+ Non-blocking - only injects a message if update available.
7
+
8
+ Hook Type: SessionStart
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+
18
+ def get_installed_version():
19
+ """Get currently installed version from package.json"""
20
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
21
+ package_json = Path(project_dir) / "package.json"
22
+
23
+ if package_json.exists():
24
+ try:
25
+ data = json.loads(package_json.read_text())
26
+ # Check if this project uses api-dev-tools
27
+ deps = data.get("devDependencies", {})
28
+ deps.update(data.get("dependencies", {}))
29
+
30
+ if "@hustle-together/api-dev-tools" in deps:
31
+ version = deps["@hustle-together/api-dev-tools"]
32
+ # Remove ^ or ~ prefix
33
+ return version.lstrip("^~")
34
+ except Exception:
35
+ pass
36
+
37
+ # Check for version in state file
38
+ state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
39
+ if state_file.exists():
40
+ try:
41
+ state = json.loads(state_file.read_text())
42
+ return state.get("version", "0.0.0")
43
+ except Exception:
44
+ pass
45
+
46
+ return None
47
+
48
+
49
+ def get_latest_version():
50
+ """Check npm registry for latest version"""
51
+ try:
52
+ result = subprocess.run(
53
+ ["npm", "view", "@hustle-together/api-dev-tools", "version"],
54
+ capture_output=True,
55
+ text=True,
56
+ timeout=5
57
+ )
58
+ if result.returncode == 0:
59
+ return result.stdout.strip()
60
+ except Exception:
61
+ pass
62
+ return None
63
+
64
+
65
+ def version_tuple(v):
66
+ """Convert version string to tuple for comparison"""
67
+ try:
68
+ return tuple(map(int, v.split(".")))
69
+ except Exception:
70
+ return (0, 0, 0)
71
+
72
+
73
+ def main():
74
+ # Check if we should skip (already checked recently)
75
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
76
+ state_file = Path(project_dir) / ".claude" / "api-dev-state.json"
77
+
78
+ try:
79
+ if state_file.exists():
80
+ state = json.loads(state_file.read_text())
81
+ last_check = state.get("last_update_check")
82
+
83
+ if last_check:
84
+ from datetime import datetime, timedelta
85
+ last_check_dt = datetime.fromisoformat(last_check)
86
+ if datetime.now() - last_check_dt < timedelta(hours=24):
87
+ # Already checked today, skip
88
+ print(json.dumps({"continue": True}))
89
+ return
90
+ except Exception:
91
+ pass
92
+
93
+ installed = get_installed_version()
94
+ latest = get_latest_version()
95
+
96
+ result = {"continue": True}
97
+
98
+ if installed and latest:
99
+ if version_tuple(latest) > version_tuple(installed):
100
+ result["additionalContext"] = f"""
101
+ ## Update Available
102
+
103
+ A new version of api-dev-tools is available:
104
+ - **Current**: {installed}
105
+ - **Latest**: {latest}
106
+
107
+ To update, run:
108
+ ```bash
109
+ npx @hustle-together/api-dev-tools@latest
110
+ ```
111
+
112
+ This update may include new features, bug fixes, and improved workflows.
113
+ """
114
+ # Update state with last check time
115
+ try:
116
+ if state_file.exists():
117
+ state = json.loads(state_file.read_text())
118
+ else:
119
+ state = {}
120
+
121
+ from datetime import datetime
122
+ state["last_update_check"] = datetime.now().isoformat()
123
+ state["available_update"] = latest
124
+ state_file.write_text(json.dumps(state, indent=2))
125
+ except Exception:
126
+ pass
127
+
128
+ print(json.dumps(result))
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()