fantasy-claude 1.0.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/VERSION +1 -0
  4. package/cli.sh +7 -0
  5. package/config.json +135 -0
  6. package/configure.py +1922 -0
  7. package/configure.sh +5 -0
  8. package/hooks/notify-auth.sh +17 -0
  9. package/hooks/notify-elicitation.sh +17 -0
  10. package/hooks/notify-idle.sh +9 -0
  11. package/hooks/notify-permission.sh +17 -0
  12. package/hooks/notify.sh +38 -0
  13. package/hooks/sounds.sh +55 -0
  14. package/install.sh +67 -0
  15. package/package.json +25 -0
  16. package/sounds/error/.gitkeep +0 -0
  17. package/sounds/error/FFAAAAH.mp3 +0 -0
  18. package/sounds/error/lego-break.mp3 +0 -0
  19. package/sounds/notification/.gitkeep +0 -0
  20. package/statusline/elements/battery.sh +12 -0
  21. package/statusline/elements/burn-rate.sh +6 -0
  22. package/statusline/elements/context-pct.sh +40 -0
  23. package/statusline/elements/cwd.sh +21 -0
  24. package/statusline/elements/datetime.sh +2 -0
  25. package/statusline/elements/file-entropy.sh +31 -0
  26. package/statusline/elements/git-branch.sh +3 -0
  27. package/statusline/elements/github-repo.sh +6 -0
  28. package/statusline/elements/haiku.sh +77 -0
  29. package/statusline/elements/model.sh +33 -0
  30. package/statusline/elements/mood.sh +27 -0
  31. package/statusline/elements/moon-phase.sh +31 -0
  32. package/statusline/elements/pomodoro.sh +39 -0
  33. package/statusline/elements/reset-time.sh +15 -0
  34. package/statusline/elements/session-cost.sh +42 -0
  35. package/statusline/elements/session-duration.sh +43 -0
  36. package/statusline/elements/streak.sh +47 -0
  37. package/statusline/elements/usage-5h.sh +6 -0
  38. package/statusline/statusline.sh +223 -0
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # Estimated session cost from token counts in the active JSONL
3
+ # Uses Sonnet 4.6 pricing: $3/MTok in, $15/MTok out, $3.75/MTok cache-write, $0.30/MTok cache-read
4
+ python3 - << 'PYEOF'
5
+ import glob, json, os, sys
6
+
7
+ CLAUDE_DIR = os.path.expanduser("~/.claude/projects")
8
+ files = glob.glob(f"{CLAUDE_DIR}/**/*.jsonl", recursive=True)
9
+ if not files:
10
+ print("--")
11
+ sys.exit()
12
+
13
+ latest = max(files, key=os.path.getmtime)
14
+
15
+ # Pricing per token (in USD)
16
+ PRICE = {
17
+ "input": 3.00 / 1_000_000,
18
+ "output": 15.00 / 1_000_000,
19
+ "cache_write": 3.75 / 1_000_000,
20
+ "cache_read": 0.30 / 1_000_000,
21
+ }
22
+
23
+ totals = {k: 0 for k in PRICE}
24
+ with open(latest, errors="replace") as f:
25
+ for line in f:
26
+ try:
27
+ d = json.loads(line)
28
+ if d.get("type") == "assistant":
29
+ u = d.get("message", {}).get("usage", {})
30
+ totals["input"] += u.get("input_tokens", 0)
31
+ totals["output"] += u.get("output_tokens", 0)
32
+ totals["cache_write"] += u.get("cache_creation_input_tokens", 0)
33
+ totals["cache_read"] += u.get("cache_read_input_tokens", 0)
34
+ except Exception:
35
+ pass
36
+
37
+ cost = sum(totals[k] * PRICE[k] for k in PRICE)
38
+ if cost < 0.01:
39
+ print(f"~${cost:.4f}")
40
+ else:
41
+ print(f"~${cost:.2f}")
42
+ PYEOF
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # Duration of the current Claude Code session from its JSONL
3
+ python3 - << 'PYEOF'
4
+ import glob, json, os, sys
5
+ from datetime import datetime, timezone
6
+
7
+ CLAUDE_DIR = os.path.expanduser("~/.claude/projects")
8
+ files = glob.glob(f"{CLAUDE_DIR}/**/*.jsonl", recursive=True)
9
+ if not files:
10
+ print("--")
11
+ sys.exit()
12
+
13
+ latest = max(files, key=os.path.getmtime)
14
+
15
+ first_ts = None
16
+ with open(latest, errors="replace") as f:
17
+ for line in f:
18
+ try:
19
+ d = json.loads(line)
20
+ ts = d.get("timestamp")
21
+ if ts and d.get("type") in ("user", "assistant"):
22
+ first_ts = ts
23
+ break
24
+ except Exception:
25
+ pass
26
+
27
+ if not first_ts:
28
+ print("--")
29
+ sys.exit()
30
+
31
+ try:
32
+ start = datetime.fromisoformat(first_ts.replace("Z", "+00:00"))
33
+ now = datetime.now(timezone.utc)
34
+ delta = int((now - start).total_seconds())
35
+ h, rem = divmod(delta, 3600)
36
+ m, s = divmod(rem, 60)
37
+ if h > 0:
38
+ print(f"{h}h{m:02d}m")
39
+ else:
40
+ print(f"{m}m{s:02d}s")
41
+ except Exception:
42
+ print("--")
43
+ PYEOF
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
3
+ python3 - "$REPO_DIR/config.json" << 'PYEOF'
4
+ import glob, os, json, sys
5
+ from datetime import date, timedelta
6
+
7
+ show_unit = True
8
+ try:
9
+ with open(sys.argv[1]) as f:
10
+ show_unit = json.load(f).get('statusline', {}).get('element_settings', {}).get('streak', {}).get('show_unit', True)
11
+ except Exception:
12
+ pass
13
+
14
+ claude_dir = os.path.expanduser('~/.claude/projects')
15
+ files = glob.glob(f'{claude_dir}/**/*.jsonl', recursive=True)
16
+ days = set()
17
+ for f in files:
18
+ try:
19
+ with open(f, errors='replace') as fh:
20
+ for line in fh:
21
+ try:
22
+ d = json.loads(line)
23
+ ts = d.get('timestamp')
24
+ if ts:
25
+ days.add(ts[:10])
26
+ except Exception:
27
+ pass
28
+ except Exception:
29
+ pass
30
+
31
+ if not days:
32
+ print("0 days" if show_unit else "0d")
33
+ exit()
34
+
35
+ today = date.today()
36
+ streak = 0
37
+ current = today
38
+ while current.isoformat() in days:
39
+ streak += 1
40
+ current -= timedelta(days=1)
41
+ if streak == 0:
42
+ current = today - timedelta(days=1)
43
+ while current.isoformat() in days:
44
+ streak += 1
45
+ current -= timedelta(days=1)
46
+ print(f"{streak} days" if show_unit else f"{streak}d")
47
+ PYEOF
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ # 5-hour rate limit utilization, read from cache written by statusline-command.sh
3
+ CACHE="/tmp/claude_usage_cache_$(id -u)"
4
+ [ -f "$CACHE" ] || { echo "--"; exit 0; }
5
+ read -r pct _ < "$CACHE"
6
+ [ -n "$pct" ] && echo "${pct%.*}%" || echo "--"
@@ -0,0 +1,223 @@
1
+ #!/bin/bash
2
+ # Main statusline script — reads config.json and composes the status line
3
+
4
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ CONFIG="$REPO_DIR/config.json"
6
+ ELEMENTS_DIR="$REPO_DIR/statusline/elements"
7
+
8
+ BAR_WIDTH=8
9
+
10
+ make_bar() {
11
+ local pct=$1
12
+ local filled=$(( pct * BAR_WIDTH / 100 ))
13
+ (( filled > BAR_WIDTH )) && filled=$BAR_WIDTH
14
+ local empty=$(( BAR_WIDTH - filled ))
15
+ local bar="" i
16
+ for ((i=0; i<filled; i++)); do bar+="█"; done
17
+ for ((i=0; i<empty; i++)); do bar+="░"; done
18
+ printf '%s' "$bar"
19
+ }
20
+
21
+ get_multi_color() {
22
+ local rule=$1 pct=$2
23
+ case "$rule" in
24
+ usage)
25
+ if (( pct <= 60 )); then echo "32"
26
+ elif (( pct <= 80 )); then echo "38;5;208"
27
+ else echo "31"
28
+ fi ;;
29
+ battery)
30
+ if (( pct < 20 )); then echo "31"
31
+ else echo "32"
32
+ fi ;;
33
+ esac
34
+ }
35
+
36
+ SEPARATOR_ANSI=""
37
+
38
+ flush_line() {
39
+ if [ ${#current_parts[@]} -gt 0 ]; then
40
+ local sep=" | "
41
+ [ -n "$SEPARATOR_ANSI" ] && sep=$'\033'"[${SEPARATOR_ANSI}m | "$'\033'"[0m"
42
+ local result="" i
43
+ for ((i=0; i<${#current_parts[@]}; i++)); do
44
+ [ $i -gt 0 ] && result+="$sep"
45
+ result+="${current_parts[$i]}"
46
+ done
47
+ echo "$result"
48
+ fi
49
+ }
50
+
51
+ current_parts=()
52
+ while IFS= read -r raw_line; do
53
+ if [ -z "$raw_line" ]; then
54
+ flush_line
55
+ current_parts=()
56
+ continue
57
+ fi
58
+
59
+ element=$(printf '%s' "$raw_line" | cut -f1)
60
+ prefix=$(printf '%s' "$raw_line" | cut -f2)
61
+ ansi_code=$(printf '%s' "$raw_line" | cut -f3)
62
+ bar_mode=$(printf '%s' "$raw_line" | cut -f4)
63
+ bar_rule=$(printf '%s' "$raw_line" | cut -f5)
64
+
65
+ if [ "$element" = "__sep__" ]; then SEPARATOR_ANSI="$ansi_code"; continue; fi
66
+
67
+ script="$ELEMENTS_DIR/$element.sh"
68
+ if [ -x "$script" ]; then
69
+ out=$("$script" 2>/dev/null)
70
+ if [ -n "$out" ]; then
71
+ formatted=""
72
+
73
+ if [ -n "$bar_mode" ] && [ "$bar_mode" != "off" ] && [ -n "$bar_rule" ]; then
74
+ pct_num=$(printf '%s' "$out" | grep -o '[0-9]*' | head -1)
75
+ if [ -n "$pct_num" ] && [[ "$pct_num" =~ ^[0-9]+$ ]]; then
76
+ bar=$(make_bar "$pct_num")
77
+ if [ "$bar_mode" = "multi" ]; then
78
+ bar_color=$(get_multi_color "$bar_rule" "$pct_num")
79
+ colored_bar=$'\033'"[${bar_color}m${bar}"$'\033'"[0m"
80
+ if [ -n "$ansi_code" ]; then
81
+ formatted=$'\033'"[${ansi_code}m${prefix}"$'\033'"[0m${colored_bar}"$'\033'"[${ansi_code}m ${out}"$'\033'"[0m"
82
+ else
83
+ formatted="${prefix}${colored_bar} ${out}"
84
+ fi
85
+ else # mono
86
+ if [ -n "$ansi_code" ]; then
87
+ formatted=$'\033'"[${ansi_code}m${prefix}${bar} ${out}"$'\033'"[0m"
88
+ else
89
+ formatted="${prefix}${bar} ${out}"
90
+ fi
91
+ fi
92
+ fi
93
+ fi
94
+
95
+ if [ -z "$formatted" ]; then
96
+ formatted="${prefix}${out}"
97
+ if [ -n "$ansi_code" ]; then
98
+ formatted=$'\033'"[${ansi_code}m${formatted}"$'\033'"[0m"
99
+ fi
100
+ fi
101
+
102
+ current_parts+=("$formatted")
103
+ fi
104
+ fi
105
+ done < <(python3 -c "
106
+ import json, sys
107
+ EMOJIS = {
108
+ 'battery': '\U0001f50b',
109
+ 'cwd': '\U0001f4c1',
110
+ 'datetime': '\U0001f552',
111
+ 'git-branch': '\U0001f33f',
112
+ 'usage-5h': '\U0001f4ca',
113
+ 'burn-rate': '\U0001f525',
114
+ 'reset-time': '\u23f0',
115
+ 'context-pct': '\U0001f4cb',
116
+ 'session-duration': '\u23f1',
117
+ 'session-cost': '\U0001f4b0',
118
+ 'github-repo': '\U0001f4cd',
119
+ 'mood': '\U0001f3ad',
120
+ 'streak': '\U0001f4c8',
121
+ 'pomodoro': '\U0001f345',
122
+ 'file-entropy': '\U0001f500',
123
+ 'moon-phase': '\U0001f319',
124
+ 'haiku': '\U0001f4dc',
125
+ }
126
+ LABELS = {
127
+ 'battery': 'bat',
128
+ 'cwd': 'dir',
129
+ 'datetime': 'time',
130
+ 'git-branch': 'branch',
131
+ 'usage-5h': '5h',
132
+ 'burn-rate': 'burn',
133
+ 'reset-time': 'reset',
134
+ 'context-pct': 'ctx',
135
+ 'session-duration': 'dur',
136
+ 'session-cost': 'cost',
137
+ 'github-repo': 'repo',
138
+ 'mood': 'mood',
139
+ 'streak': 'streak',
140
+ 'pomodoro': 'pomo',
141
+ 'file-entropy': 'files',
142
+ 'moon-phase': 'phase',
143
+ 'haiku': 'haiku',
144
+ }
145
+ COLORS = {
146
+ 'green': '32', 'cyan': '36', 'red': '31', 'yellow': '33',
147
+ 'orange': '38;5;208', 'light gray': '37', 'dark gray': '90',
148
+ }
149
+ BAR_ELEMENTS = {
150
+ 'context-pct': 'usage',
151
+ 'usage-5h': 'usage',
152
+ 'battery': 'battery',
153
+ }
154
+ MODEL_EMOJI_SETS = {
155
+ 1: {'haiku': '\U0001f6eb', 'sonnet': '\U0001f6f0', 'opus': '\U0001f6f8'},
156
+ 2: {'haiku': '\u26b1\ufe0f', 'sonnet': '\U0001f3fa', 'opus': '\U0001f52e'},
157
+ }
158
+ def get_model_emoji(set_idx=1):
159
+ import glob, os
160
+ claude_dir = os.path.expanduser('~/.claude/projects')
161
+ files = glob.glob(f'{claude_dir}/**/*.jsonl', recursive=True)
162
+ if not files:
163
+ return ''
164
+ latest = max(files, key=os.path.getmtime)
165
+ model = None
166
+ try:
167
+ with open(latest, errors='replace') as fj:
168
+ for line in fj:
169
+ try:
170
+ d = json.loads(line)
171
+ if d.get('type') == 'assistant':
172
+ m = d.get('message', {}).get('model')
173
+ if m: model = m
174
+ except Exception:
175
+ pass
176
+ except Exception:
177
+ pass
178
+ if model:
179
+ ml = model.lower()
180
+ emoji_map = MODEL_EMOJI_SETS.get(set_idx, MODEL_EMOJI_SETS[1])
181
+ for kw, em in emoji_map.items():
182
+ if kw in ml:
183
+ return em
184
+ return ''
185
+ with open('$CONFIG') as f:
186
+ d = json.load(f)
187
+ sl = d.get('statusline', {})
188
+ lines = sl.get('lines')
189
+ if lines is None:
190
+ elems = sl.get('elements', [])
191
+ lines = [elems] if elems else []
192
+ settings = sl.get('element_settings', {})
193
+ default_color = COLORS.get(sl.get('default_color') or '', '')
194
+ sep_color = COLORS.get(sl.get('separator_color') or '', '')
195
+ if sep_color:
196
+ print(f'__sep__\t\t{sep_color}\t\t')
197
+ for i, line in enumerate(lines):
198
+ if i > 0:
199
+ print('') # blank separator
200
+ for elem in line:
201
+ s = settings.get(elem, {})
202
+ parts = []
203
+ if s.get('emoji', False):
204
+ if elem == 'model':
205
+ raw_set = s.get('emoji_set', 1)
206
+ set_idx = raw_set if isinstance(raw_set, int) and raw_set in (1, 2) else 1
207
+ em = get_model_emoji(set_idx)
208
+ if em: parts.append(em)
209
+ else:
210
+ e = EMOJIS.get(elem, '')
211
+ if e: parts.append(e)
212
+ if s.get('label', False):
213
+ l = LABELS.get(elem, '')
214
+ if l: parts.append(l)
215
+ prefix = ' '.join(parts) + ' ' if parts else ''
216
+ color = COLORS.get(s.get('color') or '', '') or default_color
217
+ bar_mode = s.get('bar', 'off') or 'off'
218
+ bar_rule = BAR_ELEMENTS.get(elem, '')
219
+ print(f'{elem}\t{prefix}\t{color}\t{bar_mode}\t{bar_rule}')
220
+ " 2>/dev/null)
221
+
222
+ # Flush last line
223
+ flush_line