mirror of
https://github.com/luongnv89/claude-howto.git
synced 2026-08-08 00:08:36 +02:00
Internal accuracy pass against v2.1.220 — no missing upstream features, but broken example code, disagreeing counts, and metadata drift.
Functional fixes: pre-commit.sh now exits 2 so it actually blocks; dependency-check.sh reads file_path from stdin JSON instead of $1; database-mcp.json uses ${DATABASE_URL}; broken fences repaired; three command templates had invalid skill names.
Factual corrections: /fork and /subtask unswapped and /subtask added; /fewer-permission-prompts; permissions.defaultMode; dontAsk/auto unreversed; 31 hook events verified name-by-name; subagent depth 3; skill precedence enterprise > project > personal; /output-style removed not deprecated; permissionDecision gained defer.
Follow-up review fixed defects the pass left behind: zh/vi headers claiming 31 events above 25-name lists, a surviving hardcoded DB credential in the MCP README examples, an unbalanced fence swallowing a metadata footer, and non-canonical tool names. All four translated CATALOG summary tables were recounted so their arithmetic holds.
Full detail in CHANGELOG.md under v2.1.220-r2.
127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Context Usage Tracker - Tracks token consumption per request.
|
|
|
|
Uses UserPromptSubmit as "pre-message" hook and Stop as "post-response" hook
|
|
to calculate the delta in token usage for each request.
|
|
|
|
This version uses character-based estimation (no dependencies).
|
|
For better accuracy, see context-tracker-tiktoken.py.
|
|
|
|
Usage:
|
|
Configure both hooks to use the same script:
|
|
- UserPromptSubmit: saves current token count
|
|
- Stop: calculates delta and reports usage
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
# Configuration
|
|
CONTEXT_LIMIT = 1000000 # 1M on Opus 5, Sonnet 5, Opus 4.8, Sonnet 4.6; 200000 on Haiku 4.5 (adjust for your model)
|
|
|
|
|
|
def get_state_file(session_id: str) -> str:
|
|
"""Get temp file path for storing pre-message token count, isolated by session."""
|
|
return os.path.join(tempfile.gettempdir(), f"claude-context-{session_id}.json")
|
|
|
|
|
|
def count_tokens_estimate(text: str) -> int:
|
|
"""
|
|
Estimate token count using character-based approximation.
|
|
|
|
Uses ~4 characters per token ratio, which provides ~80-90% accuracy
|
|
for English text. Less accurate for code and non-English text.
|
|
"""
|
|
return len(text) // 4
|
|
|
|
|
|
def read_transcript(transcript_path: str) -> str:
|
|
"""Read and concatenate all content from transcript file."""
|
|
if not transcript_path or not os.path.exists(transcript_path):
|
|
return ""
|
|
|
|
content = []
|
|
with open(transcript_path, "r") as f:
|
|
for line in f:
|
|
try:
|
|
entry = json.loads(line.strip())
|
|
# Extract text content from various message formats
|
|
if "message" in entry:
|
|
msg = entry["message"]
|
|
if isinstance(msg.get("content"), str):
|
|
content.append(msg["content"])
|
|
elif isinstance(msg.get("content"), list):
|
|
for block in msg["content"]:
|
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
content.append(block.get("text", ""))
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
return "\n".join(content)
|
|
|
|
|
|
def handle_user_prompt_submit(data: dict) -> None:
|
|
"""Pre-message hook: Save current token count before request."""
|
|
session_id = data.get("session_id", "unknown")
|
|
transcript_path = data.get("transcript_path", "")
|
|
|
|
transcript_content = read_transcript(transcript_path)
|
|
current_tokens = count_tokens_estimate(transcript_content)
|
|
|
|
# Save to temp file for later comparison
|
|
state_file = get_state_file(session_id)
|
|
with open(state_file, "w") as f:
|
|
json.dump({"pre_tokens": current_tokens}, f)
|
|
|
|
|
|
def handle_stop(data: dict) -> None:
|
|
"""Post-response hook: Calculate and report token delta."""
|
|
session_id = data.get("session_id", "unknown")
|
|
transcript_path = data.get("transcript_path", "")
|
|
|
|
transcript_content = read_transcript(transcript_path)
|
|
current_tokens = count_tokens_estimate(transcript_content)
|
|
|
|
# Load pre-message count
|
|
state_file = get_state_file(session_id)
|
|
pre_tokens = 0
|
|
if os.path.exists(state_file):
|
|
try:
|
|
with open(state_file, "r") as f:
|
|
state = json.load(f)
|
|
pre_tokens = state.get("pre_tokens", 0)
|
|
except (json.JSONDecodeError, IOError):
|
|
pass
|
|
|
|
# Calculate delta
|
|
delta_tokens = current_tokens - pre_tokens
|
|
remaining = CONTEXT_LIMIT - current_tokens
|
|
percentage = (current_tokens / CONTEXT_LIMIT) * 100
|
|
|
|
# Report usage (stderr so it doesn't interfere with hook output)
|
|
print(
|
|
f"Context (estimated): ~{current_tokens:,} tokens "
|
|
f"({percentage:.1f}% used, ~{remaining:,} remaining)",
|
|
file=sys.stderr,
|
|
)
|
|
if delta_tokens > 0:
|
|
print(f"This request: ~{delta_tokens:,} tokens", file=sys.stderr)
|
|
|
|
|
|
def main():
|
|
data = json.load(sys.stdin)
|
|
event = data.get("hook_event_name", "")
|
|
|
|
if event == "UserPromptSubmit":
|
|
handle_user_prompt_submit(data)
|
|
elif event == "Stop":
|
|
handle_stop(data)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|