claude-code-tracker 1.1.5 → 1.1.7

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/README.md CHANGED
@@ -35,8 +35,11 @@ Homebrew only knows about packages in its default registry. `brew tap` adds a th
35
35
  ```bash
36
36
  brew tap kelsi-andrewss/claude-code-tracker
37
37
  brew install claude-code-tracker
38
+ claude-tracker-setup
38
39
  ```
39
40
 
41
+ The `claude-tracker-setup` command registers the Stop hook in `~/.claude/settings.json`. This must be run separately because Homebrew's install sandbox blocks writes to user directories.
42
+
40
43
  ### Option 3 — git clone
41
44
 
42
45
  ```bash
package/install.sh CHANGED
@@ -7,31 +7,29 @@ SETTINGS="$HOME/.claude/settings.json"
7
7
 
8
8
  echo "Installing claude-code-tracker..."
9
9
 
10
- # Install scripts
11
- mkdir -p "$INSTALL_DIR"
12
-
13
10
  # Detect Homebrew install (SCRIPT_DIR is inside a Cellar path)
14
11
  if [[ "$SCRIPT_DIR" == */Cellar/* ]]; then
15
- # Symlink scriptsavoids macOS provenance xattr issues on upgrade.
16
- # ln -sf fails if the target has com.apple.provenance (SIP-protected),
17
- # so create a temp symlink and mv -f (rename syscall bypasses SIP).
18
- for f in "$SCRIPT_DIR/src/"*.sh "$SCRIPT_DIR/src/"*.py; do
19
- dest="$INSTALL_DIR/$(basename "$f")"
20
- tmplink=$(mktemp -u "$INSTALL_DIR/.tmp.XXXXXX")
21
- ln -s "$f" "$tmplink"
22
- mv -f "$tmplink" "$dest"
23
- done
12
+ # Resolve stable opt path survives brew upgrade (Homebrew maintains the symlink)
13
+ FORMULA_NAME="$(echo "$SCRIPT_DIR" | sed -n 's|.*/Cellar/\([^/]*\)/.*|\1|p')"
14
+ OPT_PREFIX="$(brew --prefix "$FORMULA_NAME" 2>/dev/null)" || OPT_PREFIX=""
15
+ if [[ -z "$OPT_PREFIX" ]]; then
16
+ echo "Error: could not resolve brew --prefix for $FORMULA_NAME" >&2
17
+ exit 1
18
+ fi
19
+ HOOK_CMD="$OPT_PREFIX/libexec/src/stop-hook.sh"
20
+ echo "Homebrew install detected — hook will point to $HOOK_CMD"
24
21
  else
25
22
  # Direct copy for npm / git-clone installs
23
+ mkdir -p "$INSTALL_DIR"
26
24
  rm -f "$INSTALL_DIR/"*.sh "$INSTALL_DIR/"*.py 2>/dev/null || true
27
25
  cp "$SCRIPT_DIR/src/"*.sh "$SCRIPT_DIR/src/"*.py "$INSTALL_DIR/"
28
26
  chmod +x "$INSTALL_DIR/"*.sh "$INSTALL_DIR/"*.py
27
+ HOOK_CMD="$INSTALL_DIR/stop-hook.sh"
28
+ echo "Scripts installed to $INSTALL_DIR"
29
29
  fi
30
30
 
31
- echo "Scripts installed to $INSTALL_DIR"
32
-
33
31
  # Patch settings.json — add Stop hook if not already present
34
- python3 - "$SETTINGS" "$INSTALL_DIR/stop-hook.sh" <<'PYEOF'
32
+ python3 - "$SETTINGS" "$HOOK_CMD" <<'PYEOF'
35
33
  import sys, json, os
36
34
 
37
35
  settings_file = sys.argv[1]
@@ -77,15 +75,17 @@ MDEOF
77
75
  echo "Tracking instruction added to $CLAUDE_MD"
78
76
  fi
79
77
 
80
- # Backfill historical sessions for the current project
81
- PROJECT_ROOT="$PWD"
82
- while [[ "$PROJECT_ROOT" != "/" ]]; do
83
- [[ -d "$PROJECT_ROOT/.git" ]] && break
84
- PROJECT_ROOT="$(dirname "$PROJECT_ROOT")"
85
- done
86
- if [[ "$PROJECT_ROOT" != "/" ]]; then
87
- echo "Backfilling historical sessions..."
88
- python3 "$INSTALL_DIR/backfill.py" "$PROJECT_ROOT"
78
+ # Backfill historical sessions for the current project (skip for Homebrew installs)
79
+ if [[ "$SCRIPT_DIR" != */Cellar/* ]]; then
80
+ PROJECT_ROOT="$PWD"
81
+ while [[ "$PROJECT_ROOT" != "/" ]]; do
82
+ [[ -d "$PROJECT_ROOT/.git" ]] && break
83
+ PROJECT_ROOT="$(dirname "$PROJECT_ROOT")"
84
+ done
85
+ if [[ "$PROJECT_ROOT" != "/" ]]; then
86
+ echo "Backfilling historical sessions..."
87
+ python3 "$INSTALL_DIR/backfill.py" "$PROJECT_ROOT"
88
+ fi
89
89
  fi
90
90
 
91
91
  echo "claude-code-tracker installed. Restart Claude Code to activate."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-tracker",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Automatic token, cost, and prompt tracking for Claude Code sessions",
5
5
  "keywords": [
6
6
  "claude",
package/src/backfill.py CHANGED
@@ -50,7 +50,7 @@ for jf in jsonl_files:
50
50
  inp = out = cache_create = cache_read = 0
51
51
  model = "unknown"
52
52
  first_ts = None
53
- last_ts = None
53
+ msgs = []
54
54
 
55
55
  try:
56
56
  with open(jf) as f:
@@ -58,10 +58,13 @@ for jf in jsonl_files:
58
58
  try:
59
59
  obj = json.loads(line)
60
60
  ts = obj.get("timestamp")
61
- if ts:
62
- if first_ts is None:
63
- first_ts = ts
64
- last_ts = ts
61
+ if ts and first_ts is None:
62
+ first_ts = ts
63
+ t = obj.get("type")
64
+ if t == "user" and not obj.get("isSidechain") and ts:
65
+ msgs.append(("user", ts))
66
+ elif t == "assistant" and ts:
67
+ msgs.append(("assistant", ts))
65
68
  msg = obj.get("message", {})
66
69
  if isinstance(msg, dict) and msg.get("role") == "assistant":
67
70
  usage = msg.get("usage", {})
@@ -94,15 +97,22 @@ for jf in jsonl_files:
94
97
  if not session_date:
95
98
  session_date = datetime.fromtimestamp(os.path.getmtime(jf)).strftime("%Y-%m-%d")
96
99
 
97
- # Duration
100
+ # Duration: sum of per-turn active thinking time (user -> first assistant reply)
98
101
  duration = 0
99
- if first_ts and last_ts:
100
- try:
101
- t0 = datetime.fromisoformat(first_ts.replace("Z", "+00:00"))
102
- t1 = datetime.fromisoformat(last_ts.replace("Z", "+00:00"))
103
- duration = max(0, int((t1 - t0).total_seconds()))
104
- except Exception:
105
- pass
102
+ i = 0
103
+ while i < len(msgs):
104
+ if msgs[i][0] == "user":
105
+ j = i + 1
106
+ while j < len(msgs) and msgs[j][0] != "assistant":
107
+ j += 1
108
+ if j < len(msgs):
109
+ try:
110
+ t0 = datetime.fromisoformat(msgs[i][1].replace("Z", "+00:00"))
111
+ t1 = datetime.fromisoformat(msgs[j][1].replace("Z", "+00:00"))
112
+ duration += max(0, int((t1 - t0).total_seconds()))
113
+ except Exception:
114
+ pass
115
+ i += 1
106
116
 
107
117
  # Cost
108
118
  if "opus" in model:
package/uninstall.sh CHANGED
@@ -1,22 +1,31 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
5
  INSTALL_DIR="$HOME/.claude/tracking"
5
6
  SETTINGS="$HOME/.claude/settings.json"
6
- HOOK_CMD="$INSTALL_DIR/stop-hook.sh"
7
7
 
8
8
  echo "Uninstalling claude-code-tracker..."
9
9
 
10
- # Remove scripts
11
- if [[ -d "$INSTALL_DIR" ]]; then
12
- rm -f "$INSTALL_DIR/"*.sh "$INSTALL_DIR/"*.py
13
- echo "Scripts removed from $INSTALL_DIR"
10
+ # Detect Homebrew install
11
+ if [[ "$SCRIPT_DIR" == */Cellar/* ]]; then
12
+ FORMULA_NAME="$(echo "$SCRIPT_DIR" | sed -n 's|.*/Cellar/\([^/]*\)/.*|\1|p')"
13
+ OPT_PREFIX="$(brew --prefix "$FORMULA_NAME" 2>/dev/null)" || OPT_PREFIX=""
14
+ HOOK_CMD="${OPT_PREFIX:+$OPT_PREFIX/libexec/src/stop-hook.sh}"
15
+ echo "Homebrew install detected — skipping script removal from $INSTALL_DIR"
14
16
  else
15
- echo "Nothing to remove at $INSTALL_DIR"
17
+ HOOK_CMD="$INSTALL_DIR/stop-hook.sh"
18
+ # Remove scripts
19
+ if [[ -d "$INSTALL_DIR" ]]; then
20
+ rm -f "$INSTALL_DIR/"*.sh "$INSTALL_DIR/"*.py
21
+ echo "Scripts removed from $INSTALL_DIR"
22
+ else
23
+ echo "Nothing to remove at $INSTALL_DIR"
24
+ fi
16
25
  fi
17
26
 
18
27
  # Remove hook entry from settings.json
19
- if [[ -f "$SETTINGS" ]]; then
28
+ if [[ -f "$SETTINGS" ]] && [[ -n "$HOOK_CMD" ]]; then
20
29
  python3 - "$SETTINGS" "$HOOK_CMD" <<'PYEOF'
21
30
  import sys, json, os
22
31