@hustle-together/api-dev-tools 3.12.16 → 4.5.3
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/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +10 -0
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/settings.local.json +1 -7
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +34 -20
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +365 -38
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +488 -0
- package/README.md +346 -53
- package/bin/cli.js +359 -123
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +97 -20
- package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
- package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
- package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
- package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
- package/{.claude/hooks → hooks}/hook_utils.py +0 -0
- package/hooks/ntfy-on-question.py +15 -2
- package/hooks/orchestrator-handoff.py +81 -3
- package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
- package/hooks/periodic-reground.py +40 -0
- package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
- package/hooks/run-code-review.py +176 -29
- package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
- package/package.json +1 -1
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +53 -1
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +74 -7
- package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/.claude/commands/hustle-combine.md +0 -1089
- package/.claude/commands/hustle-ui-create-page.md +0 -1078
- package/.claude/commands/hustle-ui-create.md +0 -1058
- package/.claude/hooks/auto-answer.py +0 -305
- package/.claude/hooks/cache-research.py +0 -337
- package/.claude/hooks/check-api-routes.py +0 -168
- package/.claude/hooks/check-playwright-setup.py +0 -103
- package/.claude/hooks/check-storybook-setup.py +0 -81
- package/.claude/hooks/check-update.py +0 -132
- package/.claude/hooks/detect-interruption.py +0 -165
- package/.claude/hooks/enforce-a11y-audit.py +0 -202
- package/.claude/hooks/enforce-brand-guide.py +0 -241
- package/.claude/hooks/enforce-component-type-confirm.py +0 -97
- package/.claude/hooks/enforce-freshness.py +0 -184
- package/.claude/hooks/enforce-page-components.py +0 -186
- package/.claude/hooks/enforce-page-data-schema.py +0 -155
- package/.claude/hooks/enforce-questions-sourced.py +0 -146
- package/.claude/hooks/enforce-schema-from-interview.py +0 -248
- package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
- package/.claude/hooks/enforce-ui-interview.py +0 -130
- package/.claude/hooks/generate-manifest-entry.py +0 -1161
- package/.claude/hooks/lib/__init__.py +0 -1
- package/.claude/hooks/lib/greptile.py +0 -355
- package/.claude/hooks/lib/ntfy.py +0 -209
- package/.claude/hooks/notify-input-needed.py +0 -73
- package/.claude/hooks/notify-phase-complete.py +0 -90
- package/.claude/hooks/ntfy-on-question.py +0 -240
- package/.claude/hooks/orchestrator-completion.py +0 -313
- package/.claude/hooks/orchestrator-handoff.py +0 -267
- package/.claude/hooks/orchestrator-session-startup.py +0 -146
- package/.claude/hooks/run-code-review.py +0 -393
- package/.claude/hooks/session-logger.py +0 -323
- package/.claude/hooks/test-orchestrator-reground.py +0 -248
- package/.claude/hooks/track-scope-coverage.py +0 -220
- package/.claude/hooks/track-token-usage.py +0 -121
- package/.claude/hooks/update-api-showcase.py +0 -161
- package/.claude/hooks/update-registry.py +0 -352
- package/.claude/hooks/update-ui-showcase.py +0 -224
- package/.claude/test-auto-answer-bot.py +0 -183
- package/.claude/test-completion-detector.py +0 -263
- package/.claude/test-orchestrator-state.json +0 -20
- package/.claude/test-orchestrator.sh +0 -271
- /package/{.claude/commands → commands}/hustle-build.md +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
- /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
- /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: PostToolUse for AskUserQuestion
|
|
4
|
-
Purpose: Track implemented vs deferred features for scope coverage
|
|
5
|
-
|
|
6
|
-
This hook tracks which features discovered during research are:
|
|
7
|
-
- Implemented (user chose to include)
|
|
8
|
-
- Deferred (user chose to skip for later)
|
|
9
|
-
- Discovered (found in docs but not yet decided)
|
|
10
|
-
|
|
11
|
-
Added in v3.6.7 for feature scope tracking.
|
|
12
|
-
|
|
13
|
-
Returns:
|
|
14
|
-
- JSON with scope coverage update info
|
|
15
|
-
"""
|
|
16
|
-
import json
|
|
17
|
-
import sys
|
|
18
|
-
from datetime import datetime
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_active_endpoint(state):
|
|
25
|
-
"""Get active endpoint - supports both old and new state formats."""
|
|
26
|
-
if "endpoints" in state and "active_endpoint" in state:
|
|
27
|
-
active = state.get("active_endpoint")
|
|
28
|
-
if active and active in state["endpoints"]:
|
|
29
|
-
return active, state["endpoints"][active]
|
|
30
|
-
return None, None
|
|
31
|
-
|
|
32
|
-
endpoint = state.get("endpoint")
|
|
33
|
-
if endpoint:
|
|
34
|
-
return endpoint, state
|
|
35
|
-
|
|
36
|
-
return None, None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def extract_feature_from_question(question, options):
|
|
40
|
-
"""Try to extract a feature name from the question."""
|
|
41
|
-
# Look for common patterns
|
|
42
|
-
patterns = [
|
|
43
|
-
"implement",
|
|
44
|
-
"include",
|
|
45
|
-
"support",
|
|
46
|
-
"enable",
|
|
47
|
-
"add"
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
question_lower = question.lower()
|
|
51
|
-
for pattern in patterns:
|
|
52
|
-
if pattern in question_lower:
|
|
53
|
-
# Extract the words after the pattern
|
|
54
|
-
idx = question_lower.find(pattern)
|
|
55
|
-
after = question_lower[idx:].split("?")[0]
|
|
56
|
-
# Clean up
|
|
57
|
-
words = after.split()[1:4] # Get 1-3 words after pattern
|
|
58
|
-
if words:
|
|
59
|
-
return " ".join(words).strip(",.?")
|
|
60
|
-
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def is_feature_decision(question, answer, options):
|
|
65
|
-
"""Determine if this was a feature implementation decision."""
|
|
66
|
-
question_lower = question.lower()
|
|
67
|
-
|
|
68
|
-
# Keywords suggesting feature decision
|
|
69
|
-
feature_keywords = [
|
|
70
|
-
"implement", "include", "support", "enable", "add",
|
|
71
|
-
"feature", "functionality", "capability"
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
has_keyword = any(k in question_lower for k in feature_keywords)
|
|
75
|
-
|
|
76
|
-
# Check if answer indicates yes/no/defer decision
|
|
77
|
-
answer_lower = str(answer).lower() if answer else ""
|
|
78
|
-
is_decision = any(word in answer_lower for word in [
|
|
79
|
-
"yes", "no", "skip", "defer", "later", "include", "exclude",
|
|
80
|
-
"implement", "confirm", "reject"
|
|
81
|
-
])
|
|
82
|
-
|
|
83
|
-
return has_keyword and is_decision
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def categorize_decision(answer):
|
|
87
|
-
"""Categorize the decision as implement/defer/skip."""
|
|
88
|
-
answer_lower = str(answer).lower() if answer else ""
|
|
89
|
-
|
|
90
|
-
if any(word in answer_lower for word in ["yes", "include", "implement", "confirm"]):
|
|
91
|
-
return "implement"
|
|
92
|
-
elif any(word in answer_lower for word in ["defer", "later", "phase 2", "future"]):
|
|
93
|
-
return "defer"
|
|
94
|
-
elif any(word in answer_lower for word in ["no", "skip", "exclude", "reject"]):
|
|
95
|
-
return "skip"
|
|
96
|
-
|
|
97
|
-
return "unknown"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def main():
|
|
101
|
-
try:
|
|
102
|
-
input_data = json.load(sys.stdin)
|
|
103
|
-
except json.JSONDecodeError:
|
|
104
|
-
print(json.dumps({"continue": True}))
|
|
105
|
-
sys.exit(0)
|
|
106
|
-
|
|
107
|
-
tool_name = input_data.get("tool_name", "")
|
|
108
|
-
tool_input = input_data.get("tool_input", {})
|
|
109
|
-
tool_result = input_data.get("tool_result", {})
|
|
110
|
-
|
|
111
|
-
if tool_name != "AskUserQuestion":
|
|
112
|
-
print(json.dumps({"continue": True}))
|
|
113
|
-
sys.exit(0)
|
|
114
|
-
|
|
115
|
-
if not STATE_FILE.exists():
|
|
116
|
-
print(json.dumps({"continue": True}))
|
|
117
|
-
sys.exit(0)
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
state = json.loads(STATE_FILE.read_text())
|
|
121
|
-
except json.JSONDecodeError:
|
|
122
|
-
print(json.dumps({"continue": True}))
|
|
123
|
-
sys.exit(0)
|
|
124
|
-
|
|
125
|
-
endpoint, endpoint_data = get_active_endpoint(state)
|
|
126
|
-
if not endpoint or not endpoint_data:
|
|
127
|
-
print(json.dumps({"continue": True}))
|
|
128
|
-
sys.exit(0)
|
|
129
|
-
|
|
130
|
-
# Get question and answer
|
|
131
|
-
question = tool_input.get("question", "")
|
|
132
|
-
options = tool_input.get("options", [])
|
|
133
|
-
|
|
134
|
-
# Get user's answer from result
|
|
135
|
-
answer = None
|
|
136
|
-
if isinstance(tool_result, dict):
|
|
137
|
-
answer = tool_result.get("answer", tool_result.get("value", ""))
|
|
138
|
-
elif isinstance(tool_result, str):
|
|
139
|
-
answer = tool_result
|
|
140
|
-
|
|
141
|
-
# Check if this is a feature decision
|
|
142
|
-
if not is_feature_decision(question, answer, options):
|
|
143
|
-
print(json.dumps({"continue": True}))
|
|
144
|
-
sys.exit(0)
|
|
145
|
-
|
|
146
|
-
# Extract feature name
|
|
147
|
-
feature = extract_feature_from_question(question, options)
|
|
148
|
-
if not feature:
|
|
149
|
-
feature = f"feature_{datetime.now().strftime('%H%M%S')}"
|
|
150
|
-
|
|
151
|
-
# Categorize decision
|
|
152
|
-
category = categorize_decision(answer)
|
|
153
|
-
|
|
154
|
-
# Ensure scope object exists
|
|
155
|
-
if "endpoints" in state:
|
|
156
|
-
if "scope" not in state["endpoints"][endpoint]:
|
|
157
|
-
state["endpoints"][endpoint]["scope"] = {
|
|
158
|
-
"discovered_features": [],
|
|
159
|
-
"implemented_features": [],
|
|
160
|
-
"deferred_features": [],
|
|
161
|
-
"coverage_percent": 0
|
|
162
|
-
}
|
|
163
|
-
scope = state["endpoints"][endpoint]["scope"]
|
|
164
|
-
else:
|
|
165
|
-
if "scope" not in state:
|
|
166
|
-
state["scope"] = {
|
|
167
|
-
"discovered_features": [],
|
|
168
|
-
"implemented_features": [],
|
|
169
|
-
"deferred_features": [],
|
|
170
|
-
"coverage_percent": 0
|
|
171
|
-
}
|
|
172
|
-
scope = state["scope"]
|
|
173
|
-
|
|
174
|
-
# Add to discovered if not already there
|
|
175
|
-
feature_entry = {
|
|
176
|
-
"name": feature,
|
|
177
|
-
"discovered_at": datetime.now().isoformat(),
|
|
178
|
-
"question": question[:100],
|
|
179
|
-
"decision": category
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if feature not in [f.get("name") if isinstance(f, dict) else f for f in scope["discovered_features"]]:
|
|
183
|
-
scope["discovered_features"].append(feature_entry)
|
|
184
|
-
|
|
185
|
-
# Add to appropriate category
|
|
186
|
-
if category == "implement":
|
|
187
|
-
if feature not in scope["implemented_features"]:
|
|
188
|
-
scope["implemented_features"].append(feature)
|
|
189
|
-
elif category == "defer":
|
|
190
|
-
defer_entry = {
|
|
191
|
-
"name": feature,
|
|
192
|
-
"reason": f"User chose to defer: {str(answer)[:50]}",
|
|
193
|
-
"deferred_at": datetime.now().isoformat()
|
|
194
|
-
}
|
|
195
|
-
if feature not in [f.get("name") if isinstance(f, dict) else f for f in scope["deferred_features"]]:
|
|
196
|
-
scope["deferred_features"].append(defer_entry)
|
|
197
|
-
|
|
198
|
-
# Calculate coverage
|
|
199
|
-
total = len(scope["discovered_features"])
|
|
200
|
-
implemented = len(scope["implemented_features"])
|
|
201
|
-
if total > 0:
|
|
202
|
-
scope["coverage_percent"] = round((implemented / total) * 100, 1)
|
|
203
|
-
|
|
204
|
-
# Save state
|
|
205
|
-
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
206
|
-
|
|
207
|
-
output = {
|
|
208
|
-
"hookSpecificOutput": {
|
|
209
|
-
"featureTracked": feature,
|
|
210
|
-
"decision": category,
|
|
211
|
-
"coveragePercent": scope["coverage_percent"]
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
print(json.dumps(output))
|
|
216
|
-
sys.exit(0)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if __name__ == "__main__":
|
|
220
|
-
main()
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: PostToolUse
|
|
4
|
-
Purpose: Track token usage per phase and display after phase completion
|
|
5
|
-
|
|
6
|
-
Logs token usage to state file and outputs summary after each phase.
|
|
7
|
-
Integrates with ccusage if available.
|
|
8
|
-
|
|
9
|
-
Version: 3.10.0
|
|
10
|
-
"""
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
import subprocess
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from datetime import datetime
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_token_usage() -> dict:
|
|
19
|
-
"""Get current token usage from ccusage."""
|
|
20
|
-
try:
|
|
21
|
-
result = subprocess.run(
|
|
22
|
-
["ccusage", "--json"],
|
|
23
|
-
capture_output=True,
|
|
24
|
-
text=True,
|
|
25
|
-
timeout=5
|
|
26
|
-
)
|
|
27
|
-
if result.returncode == 0:
|
|
28
|
-
return json.loads(result.stdout)
|
|
29
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
|
|
30
|
-
pass
|
|
31
|
-
return {}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def main():
|
|
35
|
-
# Read hook input from stdin
|
|
36
|
-
try:
|
|
37
|
-
input_data = json.load(sys.stdin)
|
|
38
|
-
except json.JSONDecodeError:
|
|
39
|
-
sys.exit(0)
|
|
40
|
-
|
|
41
|
-
tool_name = input_data.get("tool_name", "")
|
|
42
|
-
tool_input = input_data.get("tool_input", {})
|
|
43
|
-
|
|
44
|
-
# Only trigger on Write/Edit to state file
|
|
45
|
-
if tool_name not in ["Write", "Edit"]:
|
|
46
|
-
sys.exit(0)
|
|
47
|
-
|
|
48
|
-
file_path = tool_input.get("file_path", "")
|
|
49
|
-
if "api-dev-state.json" not in file_path:
|
|
50
|
-
sys.exit(0)
|
|
51
|
-
|
|
52
|
-
# Get current token usage
|
|
53
|
-
usage = get_token_usage()
|
|
54
|
-
if not usage:
|
|
55
|
-
sys.exit(0)
|
|
56
|
-
|
|
57
|
-
# Read state file
|
|
58
|
-
cwd = Path.cwd()
|
|
59
|
-
state_file = cwd / ".claude" / "api-dev-state.json"
|
|
60
|
-
|
|
61
|
-
if not state_file.exists():
|
|
62
|
-
sys.exit(0)
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
state = json.loads(state_file.read_text())
|
|
66
|
-
except (json.JSONDecodeError, IOError):
|
|
67
|
-
sys.exit(0)
|
|
68
|
-
|
|
69
|
-
# Check for phase completion and log usage
|
|
70
|
-
phases = state.get("phases", {})
|
|
71
|
-
current_phase = None
|
|
72
|
-
|
|
73
|
-
for phase_key, phase_data in phases.items():
|
|
74
|
-
if isinstance(phase_data, dict):
|
|
75
|
-
status = phase_data.get("status", "")
|
|
76
|
-
if status == "complete":
|
|
77
|
-
current_phase = phase_key
|
|
78
|
-
|
|
79
|
-
if current_phase:
|
|
80
|
-
# Initialize token tracking in state if needed
|
|
81
|
-
if "token_usage" not in state:
|
|
82
|
-
state["token_usage"] = {
|
|
83
|
-
"by_phase": {},
|
|
84
|
-
"total_at_start": usage.get("total_tokens", 0),
|
|
85
|
-
"started_at": datetime.now().isoformat()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
# Record phase completion tokens
|
|
89
|
-
state["token_usage"]["by_phase"][current_phase] = {
|
|
90
|
-
"total_tokens": usage.get("total_tokens", 0),
|
|
91
|
-
"total_cost": usage.get("total_cost", 0),
|
|
92
|
-
"timestamp": datetime.now().isoformat()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
# Calculate phase delta if we have previous data
|
|
96
|
-
by_phase = state["token_usage"]["by_phase"]
|
|
97
|
-
phase_keys = list(by_phase.keys())
|
|
98
|
-
|
|
99
|
-
if len(phase_keys) >= 2:
|
|
100
|
-
prev_phase = phase_keys[-2]
|
|
101
|
-
prev_tokens = by_phase[prev_phase].get("total_tokens", 0)
|
|
102
|
-
current_tokens = usage.get("total_tokens", 0)
|
|
103
|
-
delta = current_tokens - prev_tokens
|
|
104
|
-
|
|
105
|
-
# Output phase token summary
|
|
106
|
-
print(f"\n📊 Phase '{current_phase}' Token Usage:", file=sys.stderr)
|
|
107
|
-
print(f" Phase tokens: {delta:,}", file=sys.stderr)
|
|
108
|
-
print(f" Total tokens: {current_tokens:,}", file=sys.stderr)
|
|
109
|
-
print(f" Total cost: ${usage.get('total_cost', 0):.2f}", file=sys.stderr)
|
|
110
|
-
|
|
111
|
-
# Update state file with token tracking
|
|
112
|
-
try:
|
|
113
|
-
state_file.write_text(json.dumps(state, indent=2))
|
|
114
|
-
except IOError:
|
|
115
|
-
pass
|
|
116
|
-
|
|
117
|
-
sys.exit(0)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if __name__ == "__main__":
|
|
121
|
-
main()
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: PostToolUse for Write/Edit
|
|
4
|
-
Purpose: Auto-create API Showcase page when first API is created
|
|
5
|
-
|
|
6
|
-
This hook monitors for new API registrations. When the first API is added
|
|
7
|
-
to registry.json, it creates the API Showcase page at src/app/api-showcase/
|
|
8
|
-
if it doesn't exist.
|
|
9
|
-
|
|
10
|
-
Version: 3.9.0
|
|
11
|
-
|
|
12
|
-
Returns:
|
|
13
|
-
- {"continue": true} - Always continues
|
|
14
|
-
- May include "notify" about showcase creation
|
|
15
|
-
"""
|
|
16
|
-
import json
|
|
17
|
-
import sys
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
import shutil
|
|
20
|
-
|
|
21
|
-
# State and registry files in .claude/ directory
|
|
22
|
-
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
23
|
-
REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def copy_showcase_templates(cwd):
|
|
27
|
-
"""Copy API showcase templates to src/app/api-showcase/."""
|
|
28
|
-
# Source templates (installed by CLI)
|
|
29
|
-
templates_dir = Path(__file__).parent.parent / "templates" / "api-showcase"
|
|
30
|
-
shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
|
|
31
|
-
|
|
32
|
-
# Destination
|
|
33
|
-
showcase_dir = cwd / "src" / "app" / "api-showcase"
|
|
34
|
-
shared_dir = cwd / "src" / "app" / "shared"
|
|
35
|
-
|
|
36
|
-
# Create directories if needed
|
|
37
|
-
showcase_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
shared_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
|
|
40
|
-
# Copy template files
|
|
41
|
-
templates_to_copy = [
|
|
42
|
-
("page.tsx", "page.tsx"),
|
|
43
|
-
("APIShowcase.tsx", "_components/APIShowcase.tsx"),
|
|
44
|
-
("APICard.tsx", "_components/APICard.tsx"),
|
|
45
|
-
("APIModal.tsx", "_components/APIModal.tsx"),
|
|
46
|
-
("APITester.tsx", "_components/APITester.tsx"),
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
created_files = []
|
|
50
|
-
for src_name, dest_name in templates_to_copy:
|
|
51
|
-
src_path = templates_dir / src_name
|
|
52
|
-
dest_path = showcase_dir / dest_name
|
|
53
|
-
|
|
54
|
-
# Create subdirectories if needed
|
|
55
|
-
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
if src_path.exists() and not dest_path.exists():
|
|
58
|
-
shutil.copy2(src_path, dest_path)
|
|
59
|
-
created_files.append(str(dest_path.relative_to(cwd)))
|
|
60
|
-
|
|
61
|
-
# Also copy shared components (HeroHeader, etc.)
|
|
62
|
-
if shared_templates_dir.exists():
|
|
63
|
-
for src_file in shared_templates_dir.iterdir():
|
|
64
|
-
if src_file.is_file():
|
|
65
|
-
dest_path = shared_dir / src_file.name
|
|
66
|
-
if not dest_path.exists():
|
|
67
|
-
shutil.copy2(src_file, dest_path)
|
|
68
|
-
created_files.append(str(dest_path.relative_to(cwd)))
|
|
69
|
-
|
|
70
|
-
return created_files
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def main():
|
|
74
|
-
# Read hook input from stdin
|
|
75
|
-
try:
|
|
76
|
-
input_data = json.load(sys.stdin)
|
|
77
|
-
except json.JSONDecodeError:
|
|
78
|
-
print(json.dumps({"continue": True}))
|
|
79
|
-
sys.exit(0)
|
|
80
|
-
|
|
81
|
-
tool_name = input_data.get("tool_name", "")
|
|
82
|
-
|
|
83
|
-
# Only process Write/Edit operations
|
|
84
|
-
if tool_name not in ["Write", "Edit"]:
|
|
85
|
-
print(json.dumps({"continue": True}))
|
|
86
|
-
sys.exit(0)
|
|
87
|
-
|
|
88
|
-
# Check if state file exists
|
|
89
|
-
if not STATE_FILE.exists():
|
|
90
|
-
print(json.dumps({"continue": True}))
|
|
91
|
-
sys.exit(0)
|
|
92
|
-
|
|
93
|
-
# Load state
|
|
94
|
-
try:
|
|
95
|
-
state = json.loads(STATE_FILE.read_text())
|
|
96
|
-
except json.JSONDecodeError:
|
|
97
|
-
print(json.dumps({"continue": True}))
|
|
98
|
-
sys.exit(0)
|
|
99
|
-
|
|
100
|
-
workflow = state.get("workflow", "")
|
|
101
|
-
|
|
102
|
-
# Only apply for API workflows
|
|
103
|
-
if workflow not in ["api-create", "combine-api"]:
|
|
104
|
-
print(json.dumps({"continue": True}))
|
|
105
|
-
sys.exit(0)
|
|
106
|
-
|
|
107
|
-
# Check if completion phase is complete
|
|
108
|
-
active_endpoint = state.get("active_endpoint", "")
|
|
109
|
-
endpoints = state.get("endpoints", {})
|
|
110
|
-
|
|
111
|
-
if active_endpoint and active_endpoint in endpoints:
|
|
112
|
-
phases = endpoints[active_endpoint].get("phases", {})
|
|
113
|
-
else:
|
|
114
|
-
phases = state.get("phases", {})
|
|
115
|
-
|
|
116
|
-
completion = phases.get("completion", {})
|
|
117
|
-
if completion.get("status") != "complete":
|
|
118
|
-
print(json.dumps({"continue": True}))
|
|
119
|
-
sys.exit(0)
|
|
120
|
-
|
|
121
|
-
# Check if showcase already exists
|
|
122
|
-
cwd = Path.cwd()
|
|
123
|
-
showcase_page = cwd / "src" / "app" / "api-showcase" / "page.tsx"
|
|
124
|
-
|
|
125
|
-
if showcase_page.exists():
|
|
126
|
-
print(json.dumps({"continue": True}))
|
|
127
|
-
sys.exit(0)
|
|
128
|
-
|
|
129
|
-
# Check if we have APIs in registry
|
|
130
|
-
if not REGISTRY_FILE.exists():
|
|
131
|
-
print(json.dumps({"continue": True}))
|
|
132
|
-
sys.exit(0)
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
registry = json.loads(REGISTRY_FILE.read_text())
|
|
136
|
-
except json.JSONDecodeError:
|
|
137
|
-
print(json.dumps({"continue": True}))
|
|
138
|
-
sys.exit(0)
|
|
139
|
-
|
|
140
|
-
apis = registry.get("apis", {})
|
|
141
|
-
combined = registry.get("combined", {})
|
|
142
|
-
|
|
143
|
-
# Create showcase if we have at least one API
|
|
144
|
-
if apis or combined:
|
|
145
|
-
created_files = copy_showcase_templates(cwd)
|
|
146
|
-
|
|
147
|
-
if created_files:
|
|
148
|
-
print(json.dumps({
|
|
149
|
-
"continue": True,
|
|
150
|
-
"notify": f"Created API Showcase at /api-showcase ({len(created_files)} files)"
|
|
151
|
-
}))
|
|
152
|
-
else:
|
|
153
|
-
print(json.dumps({"continue": True}))
|
|
154
|
-
else:
|
|
155
|
-
print(json.dumps({"continue": True}))
|
|
156
|
-
|
|
157
|
-
sys.exit(0)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if __name__ == "__main__":
|
|
161
|
-
main()
|