@hustle-together/api-dev-tools 3.11.1 ā 3.12.2
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/agents/code-reviewer.md +170 -0
- package/.claude/agents/docs-generator.md +80 -0
- package/.claude/agents/implementation-reviewer.md +119 -0
- package/.claude/agents/parallel-researcher.md +52 -0
- package/.claude/agents/research-validator.md +116 -0
- package/.claude/agents/schema-generator.md +70 -0
- package/.claude/agents/test-writer.md +104 -0
- package/.claude/api-dev-state.json +305 -56
- package/.claude/commands/README.md +21 -10
- package/.claude/commands/add-command.md +8 -5
- package/.claude/commands/api-create.md +36 -25
- package/.claude/commands/api-env.md +1 -0
- package/.claude/commands/api-interview.md +32 -19
- package/.claude/commands/api-research.md +47 -21
- package/.claude/commands/api-status.md +21 -1
- package/.claude/commands/api-verify.md +14 -13
- package/.claude/commands/beepboop.md +4 -5
- package/.claude/commands/busycommit.md +2 -3
- package/.claude/commands/commit.md +2 -3
- package/.claude/commands/cycle.md +2 -7
- package/.claude/commands/gap.md +2 -3
- package/.claude/commands/green.md +2 -7
- package/.claude/commands/issue.md +3 -8
- package/.claude/commands/ntfy-setup.md +91 -0
- package/.claude/commands/ntfy-test.md +74 -0
- package/.claude/commands/plan.md +2 -3
- package/.claude/commands/pr.md +2 -3
- package/.claude/commands/publish.md +40 -0
- package/.claude/commands/red.md +2 -7
- package/.claude/commands/refactor.md +2 -7
- package/.claude/commands/spike.md +2 -7
- package/.claude/commands/summarize.md +2 -3
- package/.claude/commands/tdd.md +2 -7
- package/.claude/commands/worktree-add.md +208 -216
- package/.claude/commands/worktree-cleanup.md +172 -178
- package/.claude/settings.json +63 -12
- package/.claude/settings.local.json +2 -1
- package/.claude-plugin/marketplace.json +2 -11
- package/.skills/README.md +55 -53
- package/.skills/_shared/settings.json +1 -1
- package/.skills/add-command/SKILL.md +10 -5
- package/.skills/api-create/SKILL.md +146 -35
- package/.skills/api-env/SKILL.md +1 -0
- package/.skills/api-interview/SKILL.md +32 -19
- package/.skills/api-research/SKILL.md +47 -21
- package/.skills/api-status/SKILL.md +21 -1
- package/.skills/api-verify/SKILL.md +14 -13
- package/.skills/beepboop/SKILL.md +6 -5
- package/.skills/busycommit/SKILL.md +4 -3
- package/.skills/commit/SKILL.md +4 -3
- package/.skills/cycle/SKILL.md +4 -7
- package/.skills/gap/SKILL.md +4 -3
- package/.skills/green/SKILL.md +4 -7
- package/.skills/issue/SKILL.md +5 -8
- package/.skills/plan/SKILL.md +4 -3
- package/.skills/pr/SKILL.md +4 -3
- package/.skills/publish/SKILL.md +160 -0
- package/.skills/red/SKILL.md +4 -7
- package/.skills/refactor/SKILL.md +4 -7
- package/.skills/spike/SKILL.md +4 -7
- package/.skills/summarize/SKILL.md +4 -3
- package/.skills/tdd/SKILL.md +4 -7
- package/.skills/update-todos/SKILL.md +22 -0
- package/.skills/worktree-add/SKILL.md +210 -216
- package/.skills/worktree-cleanup/SKILL.md +183 -187
- package/CHANGELOG.md +97 -79
- package/README.md +161 -7142
- package/bin/cli.js +448 -805
- package/commands/README.md +66 -31
- package/commands/add-command.md +8 -5
- package/commands/beepboop.md +4 -5
- package/commands/busycommit.md +2 -3
- package/commands/commit.md +2 -3
- package/commands/cycle.md +2 -7
- package/commands/gap.md +2 -3
- package/commands/green.md +2 -7
- package/commands/hustle-api-continue.md +8 -5
- package/commands/hustle-api-create.md +70 -29
- package/commands/hustle-api-env.md +1 -0
- package/commands/hustle-api-interview.md +32 -19
- package/commands/hustle-api-research.md +47 -21
- package/commands/hustle-api-sessions.md +8 -7
- package/commands/hustle-api-status.md +21 -1
- package/commands/hustle-api-verify.md +14 -13
- package/commands/hustle-combine.md +488 -241
- package/commands/hustle-ui-create-page.md +113 -50
- package/commands/hustle-ui-create.md +179 -26
- package/commands/issue.md +3 -8
- package/commands/plan.md +2 -3
- package/commands/pr.md +2 -3
- package/commands/red.md +2 -7
- package/commands/refactor.md +2 -7
- package/commands/spike.md +2 -7
- package/commands/summarize.md +2 -3
- package/commands/tdd.md +2 -7
- package/commands/worktree-add.md +208 -216
- package/commands/worktree-cleanup.md +172 -178
- package/hooks/api-workflow-check.py +5 -3
- package/hooks/enforce-component-type-confirm.py +97 -0
- package/hooks/lib/__init__.py +1 -0
- package/hooks/lib/greptile.py +355 -0
- package/hooks/lib/ntfy.py +209 -0
- package/hooks/notify-input-needed.py +73 -0
- package/hooks/notify-phase-complete.py +90 -0
- package/hooks/run-code-review.py +246 -0
- package/hooks/track-token-usage.py +121 -0
- package/package.json +13 -3
- package/scripts/collect-test-results.ts +102 -77
- package/scripts/extract-parameters.ts +112 -70
- package/scripts/generate-test-manifest.ts +118 -77
- package/templates/.env.example +57 -0
- package/templates/BRAND_GUIDE.md +92 -52
- package/templates/CLAUDE-SECTION.md +40 -37
- package/templates/SPEC.json +186 -38
- package/templates/api-dev-state.json +33 -4
- package/templates/api-showcase/_components/APICard.tsx +22 -18
- package/templates/api-showcase/_components/APIModal.tsx +110 -64
- package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
- package/templates/api-showcase/_components/APITester.tsx +128 -67
- package/templates/api-showcase/page.tsx +4 -4
- package/templates/api-test/page.tsx +51 -30
- package/templates/api-test/test-structure/route.ts +43 -34
- package/templates/component/Component.stories.tsx +41 -39
- package/templates/component/Component.test.tsx +96 -78
- package/templates/component/Component.tsx +63 -52
- package/templates/component/Component.types.ts +10 -6
- package/templates/component/Component.visual.spec.ts +170 -0
- package/templates/component/index.ts +2 -2
- package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
- package/templates/dev-tools/page.tsx +4 -3
- package/templates/mcp-servers.json +30 -2
- package/templates/page/page.e2e.test.ts +56 -48
- package/templates/page/page.tsx +3 -3
- package/templates/shared/HeroHeader.tsx +16 -15
- package/templates/shared/index.ts +1 -1
- package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
- package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
- package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
- package/templates/ui-showcase/page.tsx +4 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PostToolUse
|
|
4
|
+
Purpose: Send NTFY notification when a phase completes
|
|
5
|
+
|
|
6
|
+
Triggers after state file is updated with phase completion.
|
|
7
|
+
Includes token usage in notification.
|
|
8
|
+
|
|
9
|
+
Version: 3.10.0
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Add lib to path
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
17
|
+
from lib.ntfy import send_phase_update
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
# Read hook input from stdin
|
|
22
|
+
try:
|
|
23
|
+
input_data = json.load(sys.stdin)
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
|
|
27
|
+
tool_name = input_data.get("tool_name", "")
|
|
28
|
+
tool_input = input_data.get("tool_input", {})
|
|
29
|
+
|
|
30
|
+
# Only trigger on Write/Edit to state file
|
|
31
|
+
if tool_name not in ["Write", "Edit"]:
|
|
32
|
+
sys.exit(0)
|
|
33
|
+
|
|
34
|
+
file_path = tool_input.get("file_path", "")
|
|
35
|
+
if "api-dev-state.json" not in file_path:
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
|
|
38
|
+
# Read the updated state
|
|
39
|
+
cwd = Path.cwd()
|
|
40
|
+
state_file = cwd / ".claude" / "api-dev-state.json"
|
|
41
|
+
|
|
42
|
+
if not state_file.exists():
|
|
43
|
+
sys.exit(0)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
state = json.loads(state_file.read_text())
|
|
47
|
+
except (json.JSONDecodeError, IOError):
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
# Check for recently completed phases
|
|
51
|
+
phases = state.get("phases", {})
|
|
52
|
+
workflow = state.get("workflow", "unknown")
|
|
53
|
+
element = state.get("element_name", state.get("endpoint", "unknown"))
|
|
54
|
+
|
|
55
|
+
phase_names = {
|
|
56
|
+
"disambiguation": "Disambiguation",
|
|
57
|
+
"scope": "Scope",
|
|
58
|
+
"research_initial": "Initial Research",
|
|
59
|
+
"interview": "Interview",
|
|
60
|
+
"research_deep": "Deep Research",
|
|
61
|
+
"schema_creation": "Schema Creation",
|
|
62
|
+
"environment_check": "Environment Check",
|
|
63
|
+
"tdd_red": "TDD Red",
|
|
64
|
+
"tdd_green": "TDD Green",
|
|
65
|
+
"verify": "Verification",
|
|
66
|
+
"tdd_refactor": "Refactor",
|
|
67
|
+
"documentation": "Documentation",
|
|
68
|
+
"completion": "Completion",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for phase_key, phase_data in phases.items():
|
|
72
|
+
if isinstance(phase_data, dict):
|
|
73
|
+
status = phase_data.get("status", "")
|
|
74
|
+
notified = phase_data.get("ntfy_notified", False)
|
|
75
|
+
|
|
76
|
+
if status == "complete" and not notified:
|
|
77
|
+
phase_name = phase_names.get(phase_key, phase_key.title())
|
|
78
|
+
send_phase_update(
|
|
79
|
+
phase_name=phase_name,
|
|
80
|
+
status="complete",
|
|
81
|
+
details=f"Element: {element}",
|
|
82
|
+
workflow=workflow
|
|
83
|
+
)
|
|
84
|
+
# Mark as notified (would need to update state, but we avoid writes in hooks)
|
|
85
|
+
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Phase 11: AI Code Review Hook
|
|
4
|
+
|
|
5
|
+
Triggers Greptile AI code review after Phase 10 (Verify) and BEFORE Phase 12 (Refactor).
|
|
6
|
+
This ensures issues are caught early and can be fixed during the refactor phase,
|
|
7
|
+
rather than after PR creation when it's too late.
|
|
8
|
+
|
|
9
|
+
Hook Type: PostToolUse (triggers after tests pass in Phase 9/10)
|
|
10
|
+
|
|
11
|
+
Greptile API:
|
|
12
|
+
POST https://api.greptile.com/v2/query
|
|
13
|
+
- Analyzes code changes against entire codebase context
|
|
14
|
+
- Returns issues with file:line references
|
|
15
|
+
- Provides actionable fix suggestions
|
|
16
|
+
|
|
17
|
+
Environment Variables:
|
|
18
|
+
GREPTILE_API_KEY: Your Greptile API key (get from https://app.greptile.com)
|
|
19
|
+
GITHUB_TOKEN: GitHub Personal Access Token with repo access
|
|
20
|
+
CODE_REVIEW_ENABLED: Set to 'true' to enable (default: true)
|
|
21
|
+
|
|
22
|
+
Version: 1.1.0
|
|
23
|
+
"""
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import json
|
|
27
|
+
import subprocess
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# Add lib directory to path for imports
|
|
31
|
+
HOOK_DIR = Path(__file__).parent
|
|
32
|
+
LIB_DIR = HOOK_DIR / "lib"
|
|
33
|
+
sys.path.insert(0, str(LIB_DIR))
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from greptile import (
|
|
37
|
+
is_configured,
|
|
38
|
+
review_changes,
|
|
39
|
+
get_review_summary,
|
|
40
|
+
format_review_for_display,
|
|
41
|
+
get_status
|
|
42
|
+
)
|
|
43
|
+
GREPTILE_AVAILABLE = True
|
|
44
|
+
except ImportError:
|
|
45
|
+
GREPTILE_AVAILABLE = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_git_diff() -> tuple:
|
|
49
|
+
"""Get the current git diff and changed files."""
|
|
50
|
+
try:
|
|
51
|
+
# Get list of changed files
|
|
52
|
+
files_result = subprocess.run(
|
|
53
|
+
["git", "diff", "--name-only", "HEAD~1"],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=30
|
|
57
|
+
)
|
|
58
|
+
files_changed = files_result.stdout.strip().split("\n") if files_result.stdout else []
|
|
59
|
+
|
|
60
|
+
# Get full diff
|
|
61
|
+
diff_result = subprocess.run(
|
|
62
|
+
["git", "diff", "HEAD~1"],
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
timeout=30
|
|
66
|
+
)
|
|
67
|
+
diff_content = diff_result.stdout
|
|
68
|
+
|
|
69
|
+
return files_changed, diff_content
|
|
70
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
71
|
+
return [], ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_repo_info() -> tuple:
|
|
75
|
+
"""Get repository owner and name from git remote."""
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
["git", "remote", "get-url", "origin"],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
timeout=10
|
|
82
|
+
)
|
|
83
|
+
if result.returncode == 0:
|
|
84
|
+
url = result.stdout.strip()
|
|
85
|
+
# Parse GitHub URL (handles both HTTPS and SSH)
|
|
86
|
+
if "github.com" in url:
|
|
87
|
+
if url.startswith("git@"):
|
|
88
|
+
# SSH format: git@github.com:owner/repo.git
|
|
89
|
+
parts = url.split(":")[-1].replace(".git", "").split("/")
|
|
90
|
+
else:
|
|
91
|
+
# HTTPS format: https://github.com/owner/repo.git
|
|
92
|
+
parts = url.replace(".git", "").split("/")[-2:]
|
|
93
|
+
|
|
94
|
+
if len(parts) >= 2:
|
|
95
|
+
return parts[-2], parts[-1]
|
|
96
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
97
|
+
pass
|
|
98
|
+
return None, None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_state() -> dict:
|
|
102
|
+
"""Load current workflow state."""
|
|
103
|
+
state_file = Path.cwd() / ".claude" / "api-dev-state.json"
|
|
104
|
+
if state_file.exists():
|
|
105
|
+
try:
|
|
106
|
+
return json.loads(state_file.read_text())
|
|
107
|
+
except (json.JSONDecodeError, IOError):
|
|
108
|
+
pass
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def update_state_with_review(review_summary: dict):
|
|
113
|
+
"""Update state file with code review results."""
|
|
114
|
+
state_file = Path.cwd() / ".claude" / "api-dev-state.json"
|
|
115
|
+
state = load_state()
|
|
116
|
+
|
|
117
|
+
# Add or update code_review phase
|
|
118
|
+
if "phases" not in state:
|
|
119
|
+
state["phases"] = {}
|
|
120
|
+
|
|
121
|
+
state["phases"]["code_review"] = {
|
|
122
|
+
"status": "complete",
|
|
123
|
+
"score": review_summary.get("score", 0),
|
|
124
|
+
"issues_found": review_summary.get("issue_count", 0),
|
|
125
|
+
"suggestions": review_summary.get("suggestion_count", 0),
|
|
126
|
+
"reviewed_at": __import__("datetime").datetime.now().isoformat()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
131
|
+
except IOError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def should_run_review(hook_input: dict) -> bool:
|
|
136
|
+
"""Determine if code review should run based on hook context."""
|
|
137
|
+
# Check if code review is enabled
|
|
138
|
+
if os.environ.get("CODE_REVIEW_ENABLED", "true").lower() == "false":
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
tool_name = hook_input.get("tool_name", "")
|
|
142
|
+
|
|
143
|
+
# Run after tests pass (Phase 9/10) - triggers before refactoring
|
|
144
|
+
if tool_name == "Bash":
|
|
145
|
+
tool_input = hook_input.get("tool_input", {})
|
|
146
|
+
command = tool_input.get("command", "")
|
|
147
|
+
tool_result = hook_input.get("tool_result", {})
|
|
148
|
+
stdout = tool_result.get("stdout", "")
|
|
149
|
+
|
|
150
|
+
# Check if tests just passed
|
|
151
|
+
if ("pnpm test" in command or "npm test" in command or "vitest" in command):
|
|
152
|
+
# Only run if tests passed (look for success indicators)
|
|
153
|
+
if "pass" in stdout.lower() or "ā" in stdout or "PASS" in stdout:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
# Also run if verify-after-green hook triggered
|
|
157
|
+
if "verify" in tool_name.lower():
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
"""Main hook entry point."""
|
|
165
|
+
# Read hook input
|
|
166
|
+
try:
|
|
167
|
+
hook_input = json.loads(sys.stdin.read())
|
|
168
|
+
except json.JSONDecodeError:
|
|
169
|
+
hook_input = {}
|
|
170
|
+
|
|
171
|
+
# Check if we should run
|
|
172
|
+
if not should_run_review(hook_input):
|
|
173
|
+
# Pass through - no review needed
|
|
174
|
+
print(json.dumps({"continue": True}))
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Check if Greptile is available and configured
|
|
178
|
+
if not GREPTILE_AVAILABLE:
|
|
179
|
+
print(json.dumps({
|
|
180
|
+
"continue": True,
|
|
181
|
+
"message": "Greptile library not found - skipping code review"
|
|
182
|
+
}))
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if not is_configured():
|
|
186
|
+
status = get_status()
|
|
187
|
+
print(json.dumps({
|
|
188
|
+
"continue": True,
|
|
189
|
+
"message": f"Phase 11 Code Review skipped: {status['message']}"
|
|
190
|
+
}))
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Get repository info
|
|
194
|
+
repo_owner, repo_name = get_repo_info()
|
|
195
|
+
if not repo_owner or not repo_name:
|
|
196
|
+
print(json.dumps({
|
|
197
|
+
"continue": True,
|
|
198
|
+
"message": "Could not determine repository info - skipping code review"
|
|
199
|
+
}))
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Get changes to review
|
|
203
|
+
files_changed, diff_content = get_git_diff()
|
|
204
|
+
if not diff_content:
|
|
205
|
+
print(json.dumps({
|
|
206
|
+
"continue": True,
|
|
207
|
+
"message": "No changes detected - skipping code review"
|
|
208
|
+
}))
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Run the review
|
|
212
|
+
result = review_changes(
|
|
213
|
+
repo_owner=repo_owner,
|
|
214
|
+
repo_name=repo_name,
|
|
215
|
+
files_changed=files_changed,
|
|
216
|
+
diff_content=diff_content
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not result:
|
|
220
|
+
print(json.dumps({
|
|
221
|
+
"continue": True,
|
|
222
|
+
"message": "Greptile review failed - check API credentials"
|
|
223
|
+
}))
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Parse and display results
|
|
227
|
+
summary = get_review_summary(result)
|
|
228
|
+
update_state_with_review(summary)
|
|
229
|
+
|
|
230
|
+
# Format for display
|
|
231
|
+
display_output = format_review_for_display(summary)
|
|
232
|
+
|
|
233
|
+
# Determine if we should block based on critical issues
|
|
234
|
+
has_critical = summary.get("score", 10) < 5
|
|
235
|
+
|
|
236
|
+
print(json.dumps({
|
|
237
|
+
"continue": not has_critical,
|
|
238
|
+
"message": display_output,
|
|
239
|
+
"review_score": summary.get("score", 0),
|
|
240
|
+
"issues_count": summary.get("issue_count", 0),
|
|
241
|
+
"action_required": has_critical
|
|
242
|
+
}))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
main()
|
|
@@ -0,0 +1,121 @@
|
|
|
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()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hustle-together/api-dev-tools",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Interview-driven, research-first API development toolkit with
|
|
3
|
+
"version": "3.12.2",
|
|
4
|
+
"description": "Interview-driven, research-first API development toolkit with 14-phase TDD workflow, enforcement hooks, and 23 Agent Skills for cross-platform AI agents",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"api-dev-tools": "./bin/cli.js"
|
|
@@ -20,7 +20,17 @@
|
|
|
20
20
|
"CHANGELOG.md"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node bin/cli.js --scope=project"
|
|
23
|
+
"test": "node bin/cli.js --scope=project",
|
|
24
|
+
"usage": "ccusage",
|
|
25
|
+
"format": "prettier --write .",
|
|
26
|
+
"lint": "eslint . --fix"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"prettier": "^3.0.0",
|
|
30
|
+
"eslint": "^8.0.0"
|
|
31
|
+
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"ccusage": "^1.0.0"
|
|
24
34
|
},
|
|
25
35
|
"keywords": [
|
|
26
36
|
"claude",
|