@ramarivera/coding-buddy 0.4.0-alpha.9 → 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.
Files changed (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
  3. package/LICENSE +1 -1
  4. package/README.md +88 -37
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +130 -33
  22. package/cli/buddy-shell.ts +1 -1
  23. package/{adapters/claude/install → cli}/disable.ts +19 -32
  24. package/cli/doctor.ts +231 -0
  25. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  26. package/cli/index.ts +30 -26
  27. package/{adapters/claude/install → cli}/install.ts +65 -83
  28. package/{adapters/claude/install → cli}/pick.ts +111 -50
  29. package/{adapters/claude/install → cli}/settings.ts +2 -2
  30. package/{adapters/claude/install → cli}/show.ts +4 -4
  31. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  32. package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
  33. package/cli/tui.tsx +106 -17
  34. package/{adapters/claude/install → cli}/uninstall.ts +40 -47
  35. package/cli/upgrade.ts +2 -2
  36. package/cli/validate-species.ts +209 -0
  37. package/cli/verify.ts +3 -3
  38. package/core/achievements.ts +12 -20
  39. package/core/art-data.ts +26 -0
  40. package/core/command-service.ts +99 -97
  41. package/core/engine.ts +83 -1
  42. package/core/ports.ts +10 -0
  43. package/core/reactions.ts +533 -95
  44. package/core/slot-slug.ts +22 -0
  45. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +19 -1
  46. package/hooks/file-type-react.sh +401 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +208 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
  50. package/hooks/react.sh +1368 -0
  51. package/hooks/suggest.sh +55 -0
  52. package/package.json +70 -31
  53. package/scripts/gen-emoji-widths.ts +34 -0
  54. package/scripts/paths.sh +17 -1
  55. package/scripts/postinstall.cjs +42 -0
  56. package/scripts/snapshot-statusline.sh +54 -0
  57. package/server/achievements.ts +1419 -15
  58. package/server/art.ts +450 -1
  59. package/server/award-xp.ts +48 -0
  60. package/server/check-suggestions.ts +102 -0
  61. package/server/consolidate.ts +33 -0
  62. package/{adapters/claude/server → server}/index.ts +556 -95
  63. package/server/mcp-launcher.sh +16 -6
  64. package/server/memory.ts +404 -0
  65. package/server/mood.ts +254 -0
  66. package/server/path.ts +70 -18
  67. package/server/shift-mood.ts +43 -0
  68. package/server/state.ts +540 -3
  69. package/server/suggestions.ts +335 -0
  70. package/server/theme.ts +53 -0
  71. package/server/xp.ts +435 -0
  72. package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
  73. package/statusline/buddy-status.sh +397 -0
  74. package/statusline/combined-status.sh +134 -0
  75. package/statusline/emoji-widths.data +4 -0
  76. package/adapters/claude/hooks/hooks.json +0 -35
  77. package/adapters/claude/hooks/react.sh +0 -207
  78. package/adapters/claude/install/doctor.ts +0 -172
  79. package/adapters/claude/rendering/art.ts +0 -264
  80. package/adapters/claude/server/instructions.ts +0 -24
  81. package/adapters/claude/server/path.ts +0 -12
  82. package/adapters/claude/server/resources.ts +0 -38
  83. package/adapters/claude/statusline/buddy-status.sh +0 -408
  84. package/adapters/claude/storage/achievements.ts +0 -253
  85. package/adapters/claude/storage/identity.ts +0 -13
  86. package/adapters/claude/storage/paths.ts +0 -33
  87. package/adapters/claude/storage/settings.ts +0 -91
  88. package/adapters/claude/storage/state.ts +0 -365
  89. package/adapters/pi/README.md +0 -83
  90. package/adapters/pi/tools.ts +0 -6
  91. package/server/engine.ts +0 -1
  92. package/server/reactions.ts +0 -1
@@ -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
- }