@ramarivera/coding-buddy 0.4.0-alpha.8 → 0.6.0
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/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
- package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
- package/LICENSE +1 -1
- package/README.md +102 -72
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +59 -44
- package/cli/biomes.ts +309 -0
- package/cli/buddy-shell.ts +818 -0
- package/{adapters/claude/install → cli}/disable.ts +14 -32
- package/{adapters/claude/install → cli}/doctor.ts +55 -57
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +36 -25
- package/{adapters/claude/install → cli}/install.ts +100 -145
- package/{adapters/claude/install → cli}/pick.ts +111 -50
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
- package/cli/tui.tsx +2333 -0
- package/{adapters/claude/install → cli}/uninstall.ts +23 -38
- package/cli/upgrade.ts +213 -0
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
- package/hooks/file-type-react.sh +401 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +208 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
- package/hooks/react.sh +1368 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +113 -59
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +56 -0
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -0
- package/server/art.ts +450 -0
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/server/index.ts +1340 -0
- package/server/mcp-launcher.sh +26 -0
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +82 -0
- package/server/shift-mood.ts +43 -0
- package/{adapters/claude/storage → server}/state.ts +200 -7
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
- package/statusline/buddy-status.sh +397 -0
- package/statusline/combined-status.sh +134 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -204
- package/adapters/claude/popup/buddy-popup.sh +0 -92
- package/adapters/claude/popup/buddy-render.sh +0 -540
- package/adapters/claude/popup/popup-manager.sh +0 -355
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/index.ts +0 -752
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -389
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -24
- package/adapters/claude/storage/settings.ts +0 -41
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# coding-buddy status line — animated, right-aligned multi-line companion
|
|
3
|
+
#
|
|
4
|
+
# Art rendering: the server (writeStatusState in server/state.ts) pre-bakes
|
|
5
|
+
# every frame with eye, hat overlay, and blink resolved, and writes them into
|
|
6
|
+
# status.json along with the frame-index sequence. This script is a dumb
|
|
7
|
+
# cycler — one jq call per tick picks the current frame body.
|
|
8
|
+
#
|
|
9
|
+
# BUDDY_FAKE_NOW env var: override wall clock for snapshot tests.
|
|
10
|
+
#
|
|
11
|
+
# Uses Braille Blank (U+2800) for padding — survives JS .trim()
|
|
12
|
+
#
|
|
13
|
+
# When running inside buddy-shell (the PTY wrapper), skip status line rendering
|
|
14
|
+
# so the buddy doesn't show up twice (once in status line, once in wrapper panel).
|
|
15
|
+
[ "$BUDDY_SHELL" = "1" ] && exit 0
|
|
16
|
+
|
|
17
|
+
# shellcheck source=../scripts/paths.sh
|
|
18
|
+
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
|
|
19
|
+
|
|
20
|
+
STATE="$BUDDY_STATE_DIR/status.json"
|
|
21
|
+
CONFIG_FILE="$BUDDY_STATE_DIR/config.json"
|
|
22
|
+
# Session ID: sanitized tmux pane number, or "default" outside tmux
|
|
23
|
+
SID="${TMUX_PANE#%}"
|
|
24
|
+
SID="${SID:-default}"
|
|
25
|
+
|
|
26
|
+
[ -f "$STATE" ] || exit 0
|
|
27
|
+
|
|
28
|
+
MUTED=$(jq -r '.muted // false' "$STATE" 2>/dev/null)
|
|
29
|
+
[ "$MUTED" = "true" ] && exit 0
|
|
30
|
+
|
|
31
|
+
NAME=$(jq -r '.name // ""' "$STATE" 2>/dev/null)
|
|
32
|
+
[ -z "$NAME" ] && exit 0
|
|
33
|
+
|
|
34
|
+
RARITY=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
|
|
35
|
+
SHINY=$(jq -r '.shiny // false' "$STATE" 2>/dev/null)
|
|
36
|
+
REACTION=$(jq -r '.reaction // ""' "$STATE" 2>/dev/null)
|
|
37
|
+
ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
|
|
38
|
+
LEVEL=$(jq -r '.level // 1' "$STATE" 2>/dev/null)
|
|
39
|
+
MOOD=$(jq -r '.mood // "focused"' "$STATE" 2>/dev/null)
|
|
40
|
+
|
|
41
|
+
cat > /dev/null # drain stdin
|
|
42
|
+
|
|
43
|
+
# ─── Animation: pick current frame from server-rendered frames ──────────────
|
|
44
|
+
NOW=${BUDDY_FAKE_NOW:-$(date +%s)}
|
|
45
|
+
FRAME_BODY=$(jq -r --argjson now "$NOW" '
|
|
46
|
+
.frameSequence[$now % (.frameSequence | length)] as $idx
|
|
47
|
+
| .frames[$idx] // ""
|
|
48
|
+
' "$STATE" 2>/dev/null)
|
|
49
|
+
|
|
50
|
+
# Fallback when status.json lacks .frames — e.g. server/bash version skew
|
|
51
|
+
# during install or while the MCP server hasn't rewritten the file yet. Keep
|
|
52
|
+
# the buddy visible in a degraded form instead of emitting an empty block.
|
|
53
|
+
if [ -z "$FRAME_BODY" ]; then
|
|
54
|
+
FRAME_BODY=$' \n (°°) \n ( ) \n \n '
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
ART_LINES=()
|
|
58
|
+
while IFS= read -r line; do
|
|
59
|
+
ART_LINES+=("$line")
|
|
60
|
+
done <<< "$FRAME_BODY"
|
|
61
|
+
|
|
62
|
+
# ─── Rarity color (theme-aware) ─────────────────────────────────────────────
|
|
63
|
+
_THEME="dark"
|
|
64
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
65
|
+
_cfg_theme=$(jq -r '.theme // "auto"' "$CONFIG_FILE" 2>/dev/null)
|
|
66
|
+
[ "$_cfg_theme" = "light" ] && _THEME="light"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
NC=$'\033[0m'
|
|
70
|
+
case "$RARITY" in
|
|
71
|
+
common)
|
|
72
|
+
[ "$_THEME" = "light" ] && C=$'\033[38;2;90;90;90m' || C=$'\033[38;2;153;153;153m' ;;
|
|
73
|
+
uncommon)
|
|
74
|
+
[ "$_THEME" = "light" ] && C=$'\033[38;2;22;115;55m' || C=$'\033[38;2;78;186;101m' ;;
|
|
75
|
+
rare)
|
|
76
|
+
[ "$_THEME" = "light" ] && C=$'\033[38;2;55;85;210m' || C=$'\033[38;2;177;185;249m' ;;
|
|
77
|
+
epic)
|
|
78
|
+
[ "$_THEME" = "light" ] && C=$'\033[38;2;110;55;200m' || C=$'\033[38;2;175;135;255m' ;;
|
|
79
|
+
legendary)
|
|
80
|
+
[ "$_THEME" = "light" ] && C=$'\033[38;2;180;120;0m' || C=$'\033[38;2;255;193;7m' ;;
|
|
81
|
+
*) C=$'\033[0m' ;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
B=$'\xe2\xa0\x80' # Braille Blank U+2800
|
|
85
|
+
|
|
86
|
+
# ─── Rainbow colors for shiny buddies ────────────────────────────────────────
|
|
87
|
+
# Default ROYGBIV palette; overridden by rainbowColors in config.json
|
|
88
|
+
_hex_to_ansi() {
|
|
89
|
+
local hex="${1#\#}"
|
|
90
|
+
printf '\033[38;2;%d;%d;%dm' "$(( 16#${hex:0:2} ))" "$(( 16#${hex:2:2} ))" "$(( 16#${hex:4:2} ))"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
RAINBOW=(
|
|
94
|
+
$'\033[38;2;255;50;50m'
|
|
95
|
+
$'\033[38;2;255;140;0m'
|
|
96
|
+
$'\033[38;2;255;220;0m'
|
|
97
|
+
$'\033[38;2;50;210;50m'
|
|
98
|
+
$'\033[38;2;50;120;255m'
|
|
99
|
+
$'\033[38;2;100;50;220m'
|
|
100
|
+
$'\033[38;2;180;50;220m'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
104
|
+
_custom=$(jq -r '(.rainbowColors // []) | @tsv' "$CONFIG_FILE" 2>/dev/null)
|
|
105
|
+
if [ -n "$_custom" ]; then
|
|
106
|
+
RAINBOW=()
|
|
107
|
+
for _hex in $_custom; do
|
|
108
|
+
RAINBOW+=("$(_hex_to_ansi "$_hex")")
|
|
109
|
+
done
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
RAINBOW_LEN=${#RAINBOW[@]}
|
|
114
|
+
RAINBOW_OFFSET=$(( NOW % RAINBOW_LEN ))
|
|
115
|
+
|
|
116
|
+
# ─── Terminal width ──────────────────────────────────────────────────────────
|
|
117
|
+
COLS=0
|
|
118
|
+
PID=$$
|
|
119
|
+
for _ in 1 2 3 4 5; do
|
|
120
|
+
PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
121
|
+
[ -z "$PID" ] || [ "$PID" = "1" ] && break
|
|
122
|
+
|
|
123
|
+
# Linux: read PTY device from /proc
|
|
124
|
+
PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
|
|
125
|
+
if [ -c "$PTY" ] 2>/dev/null; then
|
|
126
|
+
COLS=$(stty size < "$PTY" 2>/dev/null | awk '{print $2}')
|
|
127
|
+
[ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# macOS: /proc doesn't exist — get TTY name from process table
|
|
131
|
+
TTY_NAME=$(ps -o tty= -p "$PID" 2>/dev/null | tr -d ' ')
|
|
132
|
+
if [ -n "$TTY_NAME" ] && [ "$TTY_NAME" != "??" ] && [ "$TTY_NAME" != "?" ]; then
|
|
133
|
+
TTY_DEV="/dev/$TTY_NAME"
|
|
134
|
+
if [ -c "$TTY_DEV" ] 2>/dev/null; then
|
|
135
|
+
COLS=$(stty size < "$TTY_DEV" 2>/dev/null | awk '{print $2}')
|
|
136
|
+
[ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
done
|
|
140
|
+
[ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=${COLUMNS:-0}
|
|
141
|
+
# Windows: /proc and TTY device detection don't exist; use PowerShell as fallback
|
|
142
|
+
if [ "${COLS:-0}" -lt 40 ] 2>/dev/null; then
|
|
143
|
+
_ps_cols=$(powershell.exe -NoProfile -Command "(Get-Host).UI.RawUI.WindowSize.Width" 2>/dev/null | tr -d '\r\n')
|
|
144
|
+
case "$_ps_cols" in ''|*[!0-9]*) ;; *) [ "$_ps_cols" -gt 40 ] 2>/dev/null && COLS=$_ps_cols ;; esac
|
|
145
|
+
fi
|
|
146
|
+
[ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=125
|
|
147
|
+
|
|
148
|
+
# ─── Reaction bubble (with TTL check) ────────────────────────────────────────
|
|
149
|
+
BUBBLE=""
|
|
150
|
+
if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
|
|
151
|
+
BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
|
|
152
|
+
fi
|
|
153
|
+
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
154
|
+
REACTION_TTL=0
|
|
155
|
+
INNER_W=44
|
|
156
|
+
MARGIN=8
|
|
157
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
158
|
+
_ttl=$(jq -r '.reactionTTL // 0' "$CONFIG_FILE" 2>/dev/null || echo 0)
|
|
159
|
+
case "$_ttl" in ''|*[!0-9]*) ;; *) REACTION_TTL="$_ttl" ;; esac
|
|
160
|
+
_bw=$(jq -r '.bubbleWidth // 44' "$CONFIG_FILE" 2>/dev/null || echo 44)
|
|
161
|
+
case "$_bw" in ''|*[!0-9]*) ;; *) INNER_W="$_bw" ;; esac
|
|
162
|
+
_bm=$(jq -r '.bubbleMargin // 8' "$CONFIG_FILE" 2>/dev/null || echo 8)
|
|
163
|
+
case "$_bm" in ''|*[!0-9]*) ;; *) MARGIN="$_bm" ;; esac
|
|
164
|
+
fi
|
|
165
|
+
if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; then
|
|
166
|
+
FRESH=0
|
|
167
|
+
if [ "$REACTION_TTL" -eq 0 ]; then
|
|
168
|
+
FRESH=1
|
|
169
|
+
elif [ -f "$REACTION_FILE" ]; then
|
|
170
|
+
TS=$(jq -r '.timestamp // 0' "$REACTION_FILE" 2>/dev/null || echo 0)
|
|
171
|
+
if [ "$TS" != "0" ]; then
|
|
172
|
+
NOW=$(date +%s)
|
|
173
|
+
AGE=$(( NOW - TS / 1000 ))
|
|
174
|
+
[ "$AGE" -lt "$REACTION_TTL" ] && FRESH=1
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
if [ "$FRESH" -eq 1 ]; then
|
|
178
|
+
if [ -n "$BUBBLE" ]; then
|
|
179
|
+
BUBBLE="$BUBBLE | \"${REACTION}\""
|
|
180
|
+
else
|
|
181
|
+
BUBBLE="\"${REACTION}\""
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# ─── Build all art lines ──────────────────────────────────────────────────────
|
|
187
|
+
# ART_LINES comes from the pre-rendered frame (already includes hat + blink).
|
|
188
|
+
# Center the name under the art. Frames are 12 cols wide (see server/art.ts),
|
|
189
|
+
# so the geometric center sits at col 6.
|
|
190
|
+
NAME_WITH_LEVEL="$NAME"
|
|
191
|
+
[ "$LEVEL" -gt 1 ] 2>/dev/null && NAME_WITH_LEVEL="${NAME} [L${LEVEL}]"
|
|
192
|
+
case "$MOOD" in
|
|
193
|
+
happy) MOOD_EMOJI="" ;;
|
|
194
|
+
focused) MOOD_EMOJI="" ;;
|
|
195
|
+
excited) MOOD_EMOJI="" ;;
|
|
196
|
+
tired) MOOD_EMOJI="" ;;
|
|
197
|
+
melancholy) MOOD_EMOJI="" ;;
|
|
198
|
+
chaotic) MOOD_EMOJI="" ;;
|
|
199
|
+
*) MOOD_EMOJI="" ;;
|
|
200
|
+
esac
|
|
201
|
+
NAME_WITH_LEVEL="${NAME_WITH_LEVEL}${MOOD_EMOJI}"
|
|
202
|
+
NAME_LEN=${#NAME_WITH_LEVEL}
|
|
203
|
+
ART_CENTER=6
|
|
204
|
+
NAME_PAD=$(( ART_CENTER - NAME_LEN / 2 ))
|
|
205
|
+
[ "$NAME_PAD" -lt 0 ] && NAME_PAD=0
|
|
206
|
+
NAME_LINE="$(printf '%*s%s' "$NAME_PAD" '' "$NAME_WITH_LEVEL")"
|
|
207
|
+
|
|
208
|
+
DIM=$'\033[2;3m'
|
|
209
|
+
|
|
210
|
+
ALL_LINES=()
|
|
211
|
+
ALL_COLORS=()
|
|
212
|
+
_arc=0
|
|
213
|
+
for line in "${ART_LINES[@]}"; do
|
|
214
|
+
ALL_LINES+=("$line")
|
|
215
|
+
if [ "$SHINY" = "true" ]; then
|
|
216
|
+
ALL_COLORS+=("${RAINBOW[$(( (_arc + RAINBOW_OFFSET) % RAINBOW_LEN ))]}")
|
|
217
|
+
else
|
|
218
|
+
ALL_COLORS+=("$C")
|
|
219
|
+
fi
|
|
220
|
+
_arc=$(( _arc + 1 ))
|
|
221
|
+
done
|
|
222
|
+
ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$DIM")
|
|
223
|
+
|
|
224
|
+
ART_W=14
|
|
225
|
+
ART_COUNT=${#ALL_LINES[@]}
|
|
226
|
+
|
|
227
|
+
# ─── Speech bubble (left of art, word-wrapped) ──────────────────────────────
|
|
228
|
+
# Strip the quotes we added earlier
|
|
229
|
+
BUBBLE_TEXT=""
|
|
230
|
+
if [ -n "$BUBBLE" ]; then
|
|
231
|
+
BUBBLE_TEXT="${BUBBLE%\"}"
|
|
232
|
+
BUBBLE_TEXT="${BUBBLE_TEXT#\"}"
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# ─── Display width (emojis count as 2 cols) ──────────────────────────────────
|
|
236
|
+
# iconv turns the string into a stream of UTF-32LE codepoints, then awk sums
|
|
237
|
+
# widths. Rules mirror server/art.ts:displayWidth — the U+2600-U+27BF range
|
|
238
|
+
# is split by Emoji_Presentation (2) vs text-presentation (1), and VS16
|
|
239
|
+
# (U+FE0F) upgrades the previous narrow symbol to 2 cols (e.g. ❤ + VS16).
|
|
240
|
+
# The ambiguous codepoint list comes from emoji-widths.data, generated by
|
|
241
|
+
# scripts/gen-emoji-widths.ts from the Unicode Emoji_Presentation property.
|
|
242
|
+
EMOJI_WIDTHS_DATA="$(dirname "${BASH_SOURCE[0]}")/emoji-widths.data"
|
|
243
|
+
EMOJI_PRES_2600="$(grep -v '^#' "$EMOJI_WIDTHS_DATA" 2>/dev/null | tr -d '\n')"
|
|
244
|
+
|
|
245
|
+
dwidth() {
|
|
246
|
+
printf '%s' "$1" | iconv -f UTF-8 -t UTF-32LE 2>/dev/null | od -An -tu4 | awk -v pres="$EMOJI_PRES_2600" '
|
|
247
|
+
BEGIN {
|
|
248
|
+
n = split(pres, arr)
|
|
249
|
+
for (k = 1; k <= n; k++) wide[arr[k]] = 1
|
|
250
|
+
}
|
|
251
|
+
# Precondition: cp is neither a variation selector (65024-65039) nor ZWJ
|
|
252
|
+
# (8205); the main loop filters those before calling in.
|
|
253
|
+
function char_width(cp) {
|
|
254
|
+
if (cp >= 126976) return 2
|
|
255
|
+
if (cp >= 9728 && cp <= 10175) return (cp in wide) ? 2 : 1
|
|
256
|
+
if (cp >= 9472 && cp <= 9631) return 1
|
|
257
|
+
if (cp >= 12288 && cp <= 40959) return 2
|
|
258
|
+
if (cp >= 65281 && cp <= 65376) return 2
|
|
259
|
+
return 1
|
|
260
|
+
}
|
|
261
|
+
{ for (i = 1; i <= NF; i++) {
|
|
262
|
+
cp = $i + 0
|
|
263
|
+
if (cp == 65039) {
|
|
264
|
+
if (upgradable) { w += 1; upgradable = 0 }
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
if ((cp >= 65024 && cp <= 65038) || cp == 8205) { upgradable = 0; continue }
|
|
268
|
+
cw = char_width(cp)
|
|
269
|
+
w += cw
|
|
270
|
+
upgradable = (cw == 1 && cp >= 9728 && cp <= 10175) ? 1 : 0
|
|
271
|
+
} }
|
|
272
|
+
END { print w+0 }'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# ─── Word-wrap bubble text ────────────────────────────────────────────────────
|
|
276
|
+
TEXT_LINES=()
|
|
277
|
+
if [ -n "$BUBBLE_TEXT" ]; then
|
|
278
|
+
WORDS=($BUBBLE_TEXT)
|
|
279
|
+
CUR_LINE=""
|
|
280
|
+
CUR_W=0
|
|
281
|
+
for word in "${WORDS[@]}"; do
|
|
282
|
+
word_w=$(dwidth "$word")
|
|
283
|
+
if [ -z "$CUR_LINE" ]; then
|
|
284
|
+
CUR_LINE="$word"; CUR_W=$word_w
|
|
285
|
+
elif [ $(( CUR_W + 1 + word_w )) -le $INNER_W ]; then
|
|
286
|
+
CUR_LINE="$CUR_LINE $word"; CUR_W=$(( CUR_W + 1 + word_w ))
|
|
287
|
+
else
|
|
288
|
+
TEXT_LINES+=("$CUR_LINE")
|
|
289
|
+
CUR_LINE="$word"; CUR_W=$word_w
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
[ -n "$CUR_LINE" ] && TEXT_LINES+=("$CUR_LINE")
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
TEXT_COUNT=${#TEXT_LINES[@]}
|
|
296
|
+
|
|
297
|
+
# Build box as plain strings (no ANSI). Color applied at output time.
|
|
298
|
+
# Box display width = INNER_W + 4: "| " + text(INNER_W) + " |"
|
|
299
|
+
BOX_W=$(( INNER_W + 4 ))
|
|
300
|
+
BUBBLE_LINES=()
|
|
301
|
+
BUBBLE_TYPES=() # "border" or "text" — determines coloring
|
|
302
|
+
if [ $TEXT_COUNT -gt 0 ]; then
|
|
303
|
+
# Top border
|
|
304
|
+
BORDER=$(printf '%*s' "$(( BOX_W - 2 ))" '' | tr ' ' '-')
|
|
305
|
+
BUBBLE_LINES+=(".${BORDER}.")
|
|
306
|
+
BUBBLE_TYPES+=("border")
|
|
307
|
+
# Text rows: "| text padded |"
|
|
308
|
+
for tl in "${TEXT_LINES[@]}"; do
|
|
309
|
+
tpad=$(( INNER_W - $(dwidth "$tl") ))
|
|
310
|
+
[ "$tpad" -lt 0 ] && tpad=0
|
|
311
|
+
padding=$(printf '%*s' "$tpad" '')
|
|
312
|
+
BUBBLE_LINES+=("| ${tl}${padding} |")
|
|
313
|
+
BUBBLE_TYPES+=("text")
|
|
314
|
+
done
|
|
315
|
+
# Bottom border
|
|
316
|
+
BUBBLE_LINES+=("\`${BORDER}'")
|
|
317
|
+
BUBBLE_TYPES+=("border")
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
BUBBLE_COUNT=${#BUBBLE_LINES[@]}
|
|
321
|
+
|
|
322
|
+
# ─── Right-align with bubble box to the left ─────────────────────────────────
|
|
323
|
+
GAP=2
|
|
324
|
+
if [ $BUBBLE_COUNT -gt 0 ]; then
|
|
325
|
+
TOTAL_W=$(( BOX_W + GAP + ART_W ))
|
|
326
|
+
else
|
|
327
|
+
TOTAL_W=$ART_W
|
|
328
|
+
fi
|
|
329
|
+
PAD=$(( COLS - TOTAL_W - MARGIN ))
|
|
330
|
+
[ "$PAD" -lt 0 ] && PAD=0
|
|
331
|
+
|
|
332
|
+
# On Windows (Git Bash / MSYS2), Braille Blank (U+2800) renders as double-width,
|
|
333
|
+
# which doubles the spacer and pushes content off-screen. Use regular spaces instead.
|
|
334
|
+
case "$(uname -s)" in
|
|
335
|
+
MINGW*|CYGWIN*|MSYS*) SPACER=$(printf '%*s' "$PAD" '') ;;
|
|
336
|
+
*) SPACER=$(printf "${B}%${PAD}s" "") ;;
|
|
337
|
+
esac
|
|
338
|
+
GAP_STR=$(printf '%*s' "$GAP" '')
|
|
339
|
+
|
|
340
|
+
# Vertically center bubble box on the art
|
|
341
|
+
BUBBLE_START=0
|
|
342
|
+
if [ $BUBBLE_COUNT -gt 0 ] && [ $BUBBLE_COUNT -lt $ART_COUNT ]; then
|
|
343
|
+
BUBBLE_START=$(( (ART_COUNT - BUBBLE_COUNT) / 2 ))
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# ─── Find the connector line (middle text line → points to buddy's mouth) ─────
|
|
347
|
+
# The connector goes on the middle text row of the bubble
|
|
348
|
+
CONNECTOR_BI=-1
|
|
349
|
+
if [ $BUBBLE_COUNT -gt 2 ]; then
|
|
350
|
+
# text rows are indices 1..(BUBBLE_COUNT-2), pick the middle one
|
|
351
|
+
FIRST_TEXT=1
|
|
352
|
+
LAST_TEXT=$(( BUBBLE_COUNT - 2 ))
|
|
353
|
+
CONNECTOR_BI=$(( (FIRST_TEXT + LAST_TEXT) / 2 ))
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# ─── Output: merged bubble box + connector + art per line ─────────────────────
|
|
357
|
+
TOTAL_BUBBLE=$(( BUBBLE_START + BUBBLE_COUNT ))
|
|
358
|
+
MAX_LINES=$(( ART_COUNT > TOTAL_BUBBLE ? ART_COUNT : TOTAL_BUBBLE ))
|
|
359
|
+
for (( i=0; i<MAX_LINES; i++ )); do
|
|
360
|
+
# Art part: actual art line or blank filler
|
|
361
|
+
if [ $i -lt $ART_COUNT ]; then
|
|
362
|
+
art_part="${ALL_COLORS[$i]}${ALL_LINES[$i]}${NC}"
|
|
363
|
+
else
|
|
364
|
+
art_part=$(printf '%*s' "$ART_W" '')
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [ $BUBBLE_COUNT -gt 0 ]; then
|
|
368
|
+
bi=$(( i - BUBBLE_START ))
|
|
369
|
+
if [ $bi -ge 0 ] && [ $bi -lt $BUBBLE_COUNT ]; then
|
|
370
|
+
bline="${BUBBLE_LINES[$bi]}"
|
|
371
|
+
btype="${BUBBLE_TYPES[$bi]}"
|
|
372
|
+
|
|
373
|
+
# Connector: "-- " on the middle text line, spaces otherwise
|
|
374
|
+
if [ $bi -eq $CONNECTOR_BI ]; then
|
|
375
|
+
gap="${C}--${NC} "
|
|
376
|
+
else
|
|
377
|
+
gap=" "
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
if [ "$btype" = "border" ]; then
|
|
381
|
+
echo "${SPACER}${C}${bline}${NC}${gap}${art_part}"
|
|
382
|
+
else
|
|
383
|
+
pipe_l="${bline:0:1}"
|
|
384
|
+
pipe_r="${bline: -1}"
|
|
385
|
+
inner="${bline:1:$(( ${#bline} - 2 ))}"
|
|
386
|
+
echo "${SPACER}${C}${pipe_l}${NC}${DIM}${inner}${NC}${C}${pipe_r}${NC}${gap}${art_part}"
|
|
387
|
+
fi
|
|
388
|
+
else
|
|
389
|
+
empty=$(printf '%*s' "$BOX_W" '')
|
|
390
|
+
echo "${SPACER}${empty} ${art_part}"
|
|
391
|
+
fi
|
|
392
|
+
else
|
|
393
|
+
echo "${SPACER}${art_part}"
|
|
394
|
+
fi
|
|
395
|
+
done
|
|
396
|
+
|
|
397
|
+
exit 0
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# statusline/combined-status.sh
|
|
3
|
+
# Two-panel status line: rate-limit stats left, buddy art right.
|
|
4
|
+
# buddy-status.sh is intentionally untouched (kept clean for upstream PR).
|
|
5
|
+
|
|
6
|
+
[ "$BUDDY_SHELL" = "1" ] && exit 0
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
9
|
+
BUDDY_SCRIPT="$SCRIPT_DIR/buddy-status.sh"
|
|
10
|
+
|
|
11
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
12
|
+
exec "$BUDDY_SCRIPT"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# ── Capture stdin from Claude Code ──────────────────────────────────────────
|
|
16
|
+
STDIN_DATA=$(cat)
|
|
17
|
+
|
|
18
|
+
# ── Parse rate-limit fields ──────────────────────────────────────────────────
|
|
19
|
+
STATS_JSON=$(printf '%s\n' "$STDIN_DATA" | python3 -c "
|
|
20
|
+
import json, sys, datetime
|
|
21
|
+
|
|
22
|
+
def fmt_session_reset(ts):
|
|
23
|
+
if not ts: return '--'
|
|
24
|
+
diff = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc) - datetime.datetime.now(datetime.timezone.utc)
|
|
25
|
+
mins = max(0, int(diff.total_seconds() / 60))
|
|
26
|
+
h, m = mins // 60, mins % 60
|
|
27
|
+
return f'{h}h{m:02d}m' if h else f'{m}m'
|
|
28
|
+
|
|
29
|
+
def fmt_weekly_reset(ts):
|
|
30
|
+
if not ts: return '--'
|
|
31
|
+
diff = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc) - datetime.datetime.now(datetime.timezone.utc)
|
|
32
|
+
secs = max(0, int(diff.total_seconds()))
|
|
33
|
+
d = secs // 86400
|
|
34
|
+
h = (secs % 86400) // 3600
|
|
35
|
+
m = (secs % 3600) // 60
|
|
36
|
+
return f'{d}d{h:02d}h' if d else (f'{h}h{m:02d}m' if h else f'{m}m')
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
data = json.load(sys.stdin)
|
|
40
|
+
rl = data.get('rate_limits', {})
|
|
41
|
+
fh = rl.get('five_hour', {})
|
|
42
|
+
sd = rl.get('seven_day', {})
|
|
43
|
+
sess_pct = fh.get('used_percentage')
|
|
44
|
+
week_pct = sd.get('used_percentage')
|
|
45
|
+
print(json.dumps({
|
|
46
|
+
'sess_pct': sess_pct,
|
|
47
|
+
'sess_reset': fmt_session_reset(fh.get('resets_at')),
|
|
48
|
+
'week_pct': week_pct,
|
|
49
|
+
'week_reset': fmt_weekly_reset(sd.get('resets_at')),
|
|
50
|
+
'has_data': sess_pct is not None or week_pct is not None,
|
|
51
|
+
}))
|
|
52
|
+
except Exception:
|
|
53
|
+
print('{}')
|
|
54
|
+
" 2>/dev/null)
|
|
55
|
+
|
|
56
|
+
# ── Capture buddy art (buddy reads state files, not stdin) ───────────────────
|
|
57
|
+
BUDDY_OUTPUT=$("$BUDDY_SCRIPT" </dev/null 2>/dev/null)
|
|
58
|
+
|
|
59
|
+
# No buddy output → exit silently (muted, no state, etc.)
|
|
60
|
+
[ -z "$BUDDY_OUTPUT" ] && exit 0
|
|
61
|
+
|
|
62
|
+
# No rate-limit data → pass buddy output through unchanged
|
|
63
|
+
HAS_DATA=$(python3 -c "
|
|
64
|
+
import json, sys
|
|
65
|
+
d = json.loads('''$STATS_JSON''' or '{}')
|
|
66
|
+
print(d.get('has_data', False))
|
|
67
|
+
" 2>/dev/null)
|
|
68
|
+
|
|
69
|
+
if [ "$HAS_DATA" != "True" ]; then
|
|
70
|
+
printf '%s\n' "$BUDDY_OUTPUT"
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ── Merge stat lines into buddy output ──────────────────────────────────────
|
|
75
|
+
printf '%s\n' "$BUDDY_OUTPUT" | STATS_JSON="$STATS_JSON" python3 -c "
|
|
76
|
+
import sys, json, os, re
|
|
77
|
+
|
|
78
|
+
BRAILLE = '\u2800'
|
|
79
|
+
GREEN = '\033[32m'
|
|
80
|
+
YELLOW = '\033[33m'
|
|
81
|
+
RED = '\033[31m'
|
|
82
|
+
DIM = '\033[2m'
|
|
83
|
+
NC = '\033[0m'
|
|
84
|
+
|
|
85
|
+
def color_for(pct):
|
|
86
|
+
if pct is None: return DIM
|
|
87
|
+
if pct < 30: return GREEN
|
|
88
|
+
if pct < 70: return YELLOW
|
|
89
|
+
return RED
|
|
90
|
+
|
|
91
|
+
def build_bar(pct, width=10):
|
|
92
|
+
if pct is None:
|
|
93
|
+
return DIM + '░' * width + NC
|
|
94
|
+
c = color_for(pct)
|
|
95
|
+
filled = max(0, min(width, round(pct / 100 * width)))
|
|
96
|
+
return c + '█' * filled + NC + DIM + '░' * (width - filled) + NC
|
|
97
|
+
|
|
98
|
+
def fmt_stat(label, pct, reset):
|
|
99
|
+
c = color_for(pct)
|
|
100
|
+
pct_str = f'{round(pct):3d}%' if pct is not None else ' -%'
|
|
101
|
+
bar = build_bar(pct)
|
|
102
|
+
text = f'{DIM}{label}{NC} {c}{pct_str}{NC} {bar} {c}↻{reset}{NC}'
|
|
103
|
+
visual_width = 2 + 1 + 4 + 1 + 10 + 1 + 1 + len(reset)
|
|
104
|
+
return text, visual_width
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
stats = json.loads(os.environ.get('STATS_JSON', '{}'))
|
|
108
|
+
except Exception:
|
|
109
|
+
stats = {}
|
|
110
|
+
|
|
111
|
+
lines = sys.stdin.read().splitlines()
|
|
112
|
+
n = len(lines)
|
|
113
|
+
center = 1
|
|
114
|
+
|
|
115
|
+
stat_items = [
|
|
116
|
+
fmt_stat('5h', stats.get('sess_pct'), stats.get('sess_reset', '--')),
|
|
117
|
+
fmt_stat('7d', stats.get('week_pct'), stats.get('week_reset', '--')),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
for i, line in enumerate(lines):
|
|
121
|
+
si = i - center
|
|
122
|
+
if 0 <= si < 2 and line.startswith(BRAILLE):
|
|
123
|
+
stat_text, stat_width = stat_items[si]
|
|
124
|
+
after_braille = line[1:]
|
|
125
|
+
num_spaces = len(after_braille) - len(after_braille.lstrip(' '))
|
|
126
|
+
if num_spaces >= stat_width:
|
|
127
|
+
remaining = ' ' * (num_spaces - stat_width)
|
|
128
|
+
rest = after_braille.lstrip(' ')
|
|
129
|
+
print(BRAILLE + stat_text + remaining + rest)
|
|
130
|
+
else:
|
|
131
|
+
print(line)
|
|
132
|
+
else:
|
|
133
|
+
print(line)
|
|
134
|
+
"
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.
|
|
2
|
+
# Decimal codepoints in U+2600-U+27BF with Unicode Emoji_Presentation=Yes.
|
|
3
|
+
# Regenerate with: bun run gen:emoji-widths
|
|
4
|
+
9748 9749 9800 9801 9802 9803 9804 9805 9806 9807 9808 9809 9810 9811 9855 9875 9889 9898 9899 9917 9918 9924 9925 9934 9940 9962 9970 9971 9973 9978 9981 9989 9994 9995 10024 10060 10062 10067 10068 10069 10071 10133 10134 10135 10160 10175
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"PostToolUse": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "Bash",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "./adapters/claude/hooks/react.sh"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"Stop": [
|
|
15
|
-
{
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "./adapters/claude/hooks/buddy-comment.sh"
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
],
|
|
24
|
-
"UserPromptSubmit": [
|
|
25
|
-
{
|
|
26
|
-
"hooks": [
|
|
27
|
-
{
|
|
28
|
-
"type": "command",
|
|
29
|
-
"command": "./adapters/claude/hooks/name-react.sh"
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
}
|