@ramarivera/coding-buddy 0.4.0-alpha.2 → 0.4.0-alpha.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.
- package/README.md +4 -5
- package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
- package/{cli → adapters/claude/install}/backup.ts +3 -3
- package/{cli → adapters/claude/install}/disable.ts +22 -2
- package/{cli → adapters/claude/install}/doctor.ts +30 -23
- package/{cli → adapters/claude/install}/hunt.ts +4 -4
- package/{cli → adapters/claude/install}/install.ts +62 -26
- package/{cli → adapters/claude/install}/pick.ts +3 -3
- package/{cli → adapters/claude/install}/settings.ts +1 -1
- package/{cli → adapters/claude/install}/show.ts +2 -2
- package/{cli → adapters/claude/install}/uninstall.ts +22 -2
- package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
- package/adapters/claude/popup/buddy-popup.sh +92 -0
- package/adapters/claude/popup/buddy-render.sh +540 -0
- package/adapters/claude/popup/popup-manager.sh +355 -0
- package/{server → adapters/claude/rendering}/art.ts +3 -115
- package/{server → adapters/claude/server}/index.ts +49 -71
- package/adapters/claude/server/instructions.ts +24 -0
- package/adapters/claude/server/resources.ts +38 -0
- package/adapters/claude/storage/achievements.ts +253 -0
- package/adapters/claude/storage/identity.ts +14 -0
- package/adapters/claude/storage/settings.ts +42 -0
- package/{server → adapters/claude/storage}/state.ts +3 -65
- package/adapters/pi/README.md +64 -0
- package/adapters/pi/commands.ts +173 -0
- package/adapters/pi/events.ts +150 -0
- package/adapters/pi/identity.ts +10 -0
- package/adapters/pi/index.ts +25 -0
- package/adapters/pi/renderers.ts +73 -0
- package/adapters/pi/storage.ts +295 -0
- package/adapters/pi/tools.ts +6 -0
- package/adapters/pi/ui.ts +39 -0
- package/cli/index.ts +11 -11
- package/cli/verify.ts +2 -2
- package/core/achievements.ts +203 -0
- package/core/art-data.ts +105 -0
- package/core/command-service.ts +338 -0
- package/core/model.ts +59 -0
- package/core/ports.ts +40 -0
- package/core/render-model.ts +10 -0
- package/package.json +23 -19
- package/server/achievements.ts +0 -445
- /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
- /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
- /package/{cli → adapters/claude/install}/test-statusline.ts +0 -0
- /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
- /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
- /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
- /package/{server → core}/engine.ts +0 -0
- /package/{server → core}/reactions.ts +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# claude-buddy popup manager -- create/destroy tmux popup overlay
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# popup-manager.sh start -- open buddy popup (bottom-right corner)
|
|
6
|
+
# popup-manager.sh stop -- close buddy popup
|
|
7
|
+
# popup-manager.sh status -- check if popup is running
|
|
8
|
+
#
|
|
9
|
+
# Called by SessionStart/SessionEnd hooks.
|
|
10
|
+
#
|
|
11
|
+
# Architecture: The "start" command runs a blocking reopen loop.
|
|
12
|
+
# tmux display-popup blocks until the popup closes. When ESC closes
|
|
13
|
+
# the popup (hardcoded tmux behavior), we forward ESC to CC and
|
|
14
|
+
# reopen. The loop exits when: stop flag is set, or CC pane dies.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
BUDDY_DIR="$HOME/.claude-buddy"
|
|
20
|
+
|
|
21
|
+
# Session ID: sanitized tmux pane number, or "default" outside tmux
|
|
22
|
+
SID="${TMUX_PANE#%}"
|
|
23
|
+
SID="${SID:-default}"
|
|
24
|
+
|
|
25
|
+
STOP_FLAG="$BUDDY_DIR/popup-stop.$SID"
|
|
26
|
+
REOPEN_PID_FILE="$BUDDY_DIR/popup-reopen-pid.$SID"
|
|
27
|
+
STATE_FILE="$BUDDY_DIR/status.json"
|
|
28
|
+
|
|
29
|
+
POPUP_W=12 # minimum / fallback
|
|
30
|
+
ART_W=12 # updated dynamically by compute_art_width
|
|
31
|
+
BUBBLE_EXTRA=3 # border top + border bottom + connector line
|
|
32
|
+
BORDER_EXTRA=0 # +2 on tmux < 3.3 (popup has a border)
|
|
33
|
+
REACTION_TTL=20 # seconds
|
|
34
|
+
REACTION_FILE="$BUDDY_DIR/reaction.$SID.json"
|
|
35
|
+
RESIZE_FLAG="$BUDDY_DIR/popup-resize.$SID"
|
|
36
|
+
CONFIG_FILE="$BUDDY_DIR/config.json"
|
|
37
|
+
LEFT_BUBBLE_W=22 # bubble box width in left mode (including frame chars)
|
|
38
|
+
|
|
39
|
+
# Read config early (needed for BASE_H calculation)
|
|
40
|
+
BUBBLE_POSITION="top"
|
|
41
|
+
SHOW_RARITY=1
|
|
42
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
43
|
+
_bp=$(jq -r '.bubblePosition // "top"' "$CONFIG_FILE" 2>/dev/null || echo "top")
|
|
44
|
+
case "$_bp" in top|left) BUBBLE_POSITION="$_bp" ;; esac
|
|
45
|
+
_sr=$(jq -r 'if .showRarity == false then "false" else "true" end' "$CONFIG_FILE" 2>/dev/null || echo "true")
|
|
46
|
+
[ "$_sr" = "false" ] && SHOW_RARITY=0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
BASE_H=8 # art(4) + blank(1) + name(1) + rarity(1) + padding(1)
|
|
50
|
+
[ "$SHOW_RARITY" -eq 0 ] && BASE_H=7
|
|
51
|
+
|
|
52
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
is_tmux() {
|
|
55
|
+
[ -n "${TMUX:-}" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tmux_version_ok() {
|
|
59
|
+
local ver
|
|
60
|
+
ver=$(tmux -V 2>/dev/null | grep -oE '[0-9]+\.[0-9a-z]+' | head -1)
|
|
61
|
+
[ -z "$ver" ] && return 1
|
|
62
|
+
local major minor
|
|
63
|
+
major="${ver%%.*}"
|
|
64
|
+
minor="${ver#*.}"
|
|
65
|
+
minor="${minor%%[a-z]*}"
|
|
66
|
+
[ "$major" -gt 3 ] 2>/dev/null && return 0
|
|
67
|
+
[ "$major" -eq 3 ] && [ "$minor" -ge 2 ] 2>/dev/null && return 0
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# tmux 3.4+ supports -B (borderless), -e (env), and -x R/-y S positioning
|
|
72
|
+
tmux_has_borderless() {
|
|
73
|
+
local ver
|
|
74
|
+
ver=$(tmux -V 2>/dev/null | grep -oE '[0-9]+\.[0-9a-z]+' | head -1)
|
|
75
|
+
[ -z "$ver" ] && return 1
|
|
76
|
+
local major minor
|
|
77
|
+
major="${ver%%.*}"
|
|
78
|
+
minor="${ver#*.}"
|
|
79
|
+
minor="${minor%%[a-z]*}"
|
|
80
|
+
[ "$major" -gt 3 ] 2>/dev/null && return 0
|
|
81
|
+
[ "$major" -eq 3 ] && [ "$minor" -ge 4 ] 2>/dev/null && return 0
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cc_pane_alive() {
|
|
86
|
+
tmux list-panes -a -F '#{pane_id}' 2>/dev/null | grep -qF "$1"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Compute popup width from buddy data (widest of: art, name, stars+rarity)
|
|
90
|
+
compute_art_width() {
|
|
91
|
+
[ -f "$STATE_FILE" ] || return
|
|
92
|
+
local name rarity
|
|
93
|
+
name=$(jq -r '.name // ""' "$STATE_FILE" 2>/dev/null)
|
|
94
|
+
rarity=$(jq -r '.rarity // "common"' "$STATE_FILE" 2>/dev/null)
|
|
95
|
+
local w=10 # minimum art width
|
|
96
|
+
# Name length
|
|
97
|
+
[ ${#name} -gt "$w" ] && w=${#name}
|
|
98
|
+
# Stars + rarity line (only if enabled)
|
|
99
|
+
if [ "$SHOW_RARITY" -eq 1 ]; then
|
|
100
|
+
local stars_w=$(( 6 + ${#rarity} ))
|
|
101
|
+
[ "$stars_w" -gt "$w" ] && w=$stars_w
|
|
102
|
+
fi
|
|
103
|
+
# Add 2 for padding
|
|
104
|
+
w=$(( w + 2 ))
|
|
105
|
+
POPUP_W=$w
|
|
106
|
+
ART_W=$w
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Compute popup dimensions based on reaction state and bubble position
|
|
110
|
+
# Sets COMPUTED_W and COMPUTED_H
|
|
111
|
+
compute_dimensions() {
|
|
112
|
+
compute_art_width
|
|
113
|
+
local h=$BASE_H
|
|
114
|
+
local w=$POPUP_W
|
|
115
|
+
h=$(( h + BORDER_EXTRA ))
|
|
116
|
+
w=$(( w + BORDER_EXTRA ))
|
|
117
|
+
|
|
118
|
+
# Account for hat (adds 1 art row beyond the 4 in BASE_H)
|
|
119
|
+
local art_rows=4
|
|
120
|
+
if [ -f "$STATE_FILE" ]; then
|
|
121
|
+
local _hat
|
|
122
|
+
_hat=$(jq -r '.hat // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
|
|
123
|
+
if [ "$_hat" != "none" ]; then
|
|
124
|
+
h=$(( h + 1 ))
|
|
125
|
+
art_rows=5
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
local fresh=0
|
|
130
|
+
if [ -f "$STATE_FILE" ]; then
|
|
131
|
+
local reaction
|
|
132
|
+
reaction=$(jq -r '.reaction // ""' "$STATE_FILE" 2>/dev/null || true)
|
|
133
|
+
if [ -n "$reaction" ] && [ "$reaction" != "null" ]; then
|
|
134
|
+
if [ -f "$REACTION_FILE" ]; then
|
|
135
|
+
local ts now age
|
|
136
|
+
ts=$(jq -r '.timestamp // 0' "$REACTION_FILE" 2>/dev/null || echo 0)
|
|
137
|
+
if [ "$ts" != "0" ]; then
|
|
138
|
+
now=$(date +%s)
|
|
139
|
+
age=$(( now - ts / 1000 ))
|
|
140
|
+
[ "$age" -lt "$REACTION_TTL" ] && fresh=1
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
if [ "$fresh" -eq 1 ]; then
|
|
144
|
+
if [ "$BUBBLE_POSITION" = "top" ]; then
|
|
145
|
+
# Top mode: bubble adds rows above the art
|
|
146
|
+
local bubble_w=$(( POPUP_W - BORDER_EXTRA - 5 ))
|
|
147
|
+
[ "$bubble_w" -lt 20 ] && bubble_w=20
|
|
148
|
+
# Widen popup if bubble needs more room than art
|
|
149
|
+
local needed_w=$(( bubble_w + 5 + BORDER_EXTRA ))
|
|
150
|
+
[ "$needed_w" -gt "$w" ] && w=$needed_w
|
|
151
|
+
local len=${#reaction}
|
|
152
|
+
local lines=$(( (len + bubble_w - 1) / bubble_w ))
|
|
153
|
+
[ "$lines" -lt 1 ] && lines=1
|
|
154
|
+
h=$(( h + lines + BUBBLE_EXTRA ))
|
|
155
|
+
else
|
|
156
|
+
# Left mode: dynamic bubble width to fit text within art height
|
|
157
|
+
local max_text_lines=$(( art_rows - 2 )) # subtract top/bottom borders
|
|
158
|
+
[ "$max_text_lines" -lt 1 ] && max_text_lines=1
|
|
159
|
+
local len=${#reaction}
|
|
160
|
+
local left_inner=$(( (len + max_text_lines - 1) / max_text_lines + 5 ))
|
|
161
|
+
[ "$left_inner" -lt 10 ] && left_inner=10
|
|
162
|
+
[ "$left_inner" -gt 50 ] && left_inner=50
|
|
163
|
+
local left_box=$(( left_inner + 4 )) # "| " + text + " |"
|
|
164
|
+
w=$(( w + left_box + 2 )) # +2 for connector gap
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
COMPUTED_W=$w
|
|
170
|
+
COMPUTED_H=$h
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
is_reopen_running() {
|
|
174
|
+
[ -f "$REOPEN_PID_FILE" ] || return 1
|
|
175
|
+
local pid
|
|
176
|
+
pid=$(cat "$REOPEN_PID_FILE")
|
|
177
|
+
kill -0 "$pid" 2>/dev/null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# ─── Start ───────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
start_popup() {
|
|
183
|
+
is_tmux || { echo "Not in tmux" >&2; return 1; }
|
|
184
|
+
tmux_version_ok || { echo "tmux >= 3.2 required for popup" >&2; return 1; }
|
|
185
|
+
|
|
186
|
+
# Kill stale reopen loop for THIS session (e.g., CC restarted in same pane)
|
|
187
|
+
if [ -f "$REOPEN_PID_FILE" ]; then
|
|
188
|
+
local old_pid
|
|
189
|
+
old_pid=$(cat "$REOPEN_PID_FILE" 2>/dev/null)
|
|
190
|
+
if [ -n "$old_pid" ]; then
|
|
191
|
+
kill "$old_pid" 2>/dev/null || true
|
|
192
|
+
fi
|
|
193
|
+
rm -f "$REOPEN_PID_FILE"
|
|
194
|
+
tmux display-popup -C 2>/dev/null || true
|
|
195
|
+
sleep 0.2
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Clean up orphaned per-session files (from crashed sessions)
|
|
199
|
+
for pidfile in "$BUDDY_DIR"/popup-reopen-pid.*; do
|
|
200
|
+
[ -f "$pidfile" ] || continue
|
|
201
|
+
local orphan_sid="${pidfile##*.}"
|
|
202
|
+
local orphan_pane="%${orphan_sid}"
|
|
203
|
+
if ! cc_pane_alive "$orphan_pane"; then
|
|
204
|
+
local orphan_pid
|
|
205
|
+
orphan_pid=$(cat "$pidfile" 2>/dev/null)
|
|
206
|
+
[ -n "$orphan_pid" ] && kill "$orphan_pid" 2>/dev/null || true
|
|
207
|
+
rm -f "$pidfile" "$BUDDY_DIR/popup-stop.$orphan_sid" "$BUDDY_DIR/popup-resize.$orphan_sid"
|
|
208
|
+
rm -f "$BUDDY_DIR/popup-env.$orphan_sid" "$BUDDY_DIR/popup-scroll.$orphan_sid"
|
|
209
|
+
rm -f "$BUDDY_DIR/reaction.$orphan_sid.json" "$BUDDY_DIR/.last_reaction.$orphan_sid" "$BUDDY_DIR/.last_comment.$orphan_sid"
|
|
210
|
+
fi
|
|
211
|
+
done
|
|
212
|
+
|
|
213
|
+
mkdir -p "$BUDDY_DIR"
|
|
214
|
+
rm -f "$STOP_FLAG" "$RESIZE_FLAG"
|
|
215
|
+
|
|
216
|
+
# tmux < 3.4 popups have a border (+2 rows, +2 cols); 3.4+ supports -B borderless
|
|
217
|
+
if ! tmux_has_borderless; then
|
|
218
|
+
BORDER_EXTRA=2
|
|
219
|
+
POPUP_W=$(( POPUP_W + 2 ))
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# Capture CC's pane ID before creating the popup
|
|
223
|
+
local cc_pane
|
|
224
|
+
cc_pane=$(tmux display-message -p '#{pane_id}')
|
|
225
|
+
|
|
226
|
+
# Run the reopen loop in background so the hook returns immediately.
|
|
227
|
+
# CRITICAL: Redirect stdio to /dev/null so the subshell doesn't inherit
|
|
228
|
+
# the parent's stdout pipe. CC's hook executor waits for ALL stdio writers
|
|
229
|
+
# to close before resolving -- without this redirect, the hook hangs forever
|
|
230
|
+
# because the long-lived subshell keeps the pipe open.
|
|
231
|
+
(
|
|
232
|
+
# Write the subshell PID. $BASHPID gives the subshell's PID on bash 4+.
|
|
233
|
+
# On macOS bash 3.2, $BASHPID doesn't exist, so we use sh -c 'echo $PPID'
|
|
234
|
+
# which prints the PID of the parent (this subshell) from a child process.
|
|
235
|
+
echo "${BASHPID:-$(sh -c 'echo $PPID')}" > "$REOPEN_PID_FILE"
|
|
236
|
+
|
|
237
|
+
while true; do
|
|
238
|
+
# Check stop conditions before (re)opening
|
|
239
|
+
[ -f "$STOP_FLAG" ] && break
|
|
240
|
+
cc_pane_alive "$cc_pane" || break
|
|
241
|
+
|
|
242
|
+
compute_dimensions
|
|
243
|
+
|
|
244
|
+
# Build popup args. tmux 3.4+ supports -B (borderless), -x R/-y S,
|
|
245
|
+
# and -e (env passing). On 3.2-3.3, we fall back to absolute positioning
|
|
246
|
+
# and pass env vars via a file.
|
|
247
|
+
local popup_args=()
|
|
248
|
+
if tmux_has_borderless; then
|
|
249
|
+
local tw th
|
|
250
|
+
tw=$(tmux display-message -p '#{window_width}' 2>/dev/null || echo 80)
|
|
251
|
+
th=$(tmux display-message -p '#{window_height}' 2>/dev/null || echo 24)
|
|
252
|
+
popup_args+=(-B -s 'bg=default')
|
|
253
|
+
popup_args+=(-x $(( tw - COMPUTED_W )) -y $(( th )))
|
|
254
|
+
popup_args+=(-e "CC_PANE=$cc_pane" -e "BUDDY_DIR=$BUDDY_DIR" -e "BUDDY_SID=$SID")
|
|
255
|
+
popup_args+=(-e "POPUP_INNER_W=$COMPUTED_W" -e "POPUP_INNER_H=$COMPUTED_H")
|
|
256
|
+
popup_args+=(-e "POPUP_ART_W=$ART_W")
|
|
257
|
+
else
|
|
258
|
+
# Fallback: position at bottom-right using absolute coords
|
|
259
|
+
local tw th
|
|
260
|
+
tw=$(tmux display-message -p '#{window_width}' 2>/dev/null || echo 80)
|
|
261
|
+
th=$(tmux display-message -p '#{window_height}' 2>/dev/null || echo 24)
|
|
262
|
+
popup_args+=(-x $(( tw - COMPUTED_W )) -y $(( th - COMPUTED_H )))
|
|
263
|
+
# Inner dimensions = outer - 2 (border takes 1 on each side)
|
|
264
|
+
local inner_w=$(( COMPUTED_W - 2 ))
|
|
265
|
+
local inner_h=$(( COMPUTED_H - 2 ))
|
|
266
|
+
# Write env vars to file (tmux 3.2-3.3 lack -e flag)
|
|
267
|
+
cat > "$BUDDY_DIR/popup-env.$SID" <<ENVEOF
|
|
268
|
+
CC_PANE=$cc_pane
|
|
269
|
+
BUDDY_DIR=$BUDDY_DIR
|
|
270
|
+
BUDDY_SID=$SID
|
|
271
|
+
POPUP_INNER_W=$inner_w
|
|
272
|
+
POPUP_INNER_H=$inner_h
|
|
273
|
+
POPUP_ART_W=$ART_W
|
|
274
|
+
ENVEOF
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# display-popup blocks until popup closes (ESC or command exit)
|
|
278
|
+
# -E = close when command exits
|
|
279
|
+
tmux display-popup \
|
|
280
|
+
"${popup_args[@]}" \
|
|
281
|
+
-w "$COMPUTED_W" -h "$COMPUTED_H" \
|
|
282
|
+
-E \
|
|
283
|
+
"$SCRIPT_DIR/buddy-popup.sh" "$SID" \
|
|
284
|
+
2>/dev/null || true
|
|
285
|
+
|
|
286
|
+
# Popup closed. Check why.
|
|
287
|
+
[ -f "$STOP_FLAG" ] && break
|
|
288
|
+
cc_pane_alive "$cc_pane" || break
|
|
289
|
+
|
|
290
|
+
# Scroll flag = F12 pressed, enter copy-mode and wait
|
|
291
|
+
if [ -f "$BUDDY_DIR/popup-scroll.$SID" ]; then
|
|
292
|
+
rm -f "$BUDDY_DIR/popup-scroll.$SID"
|
|
293
|
+
tmux copy-mode -t "$cc_pane" 2>/dev/null || true
|
|
294
|
+
# Wait until copy-mode ends before reopening popup
|
|
295
|
+
while tmux display-message -t "$cc_pane" -p '#{pane_in_mode}' 2>/dev/null | grep -q '^1$'; do
|
|
296
|
+
[ -f "$STOP_FLAG" ] && break 2
|
|
297
|
+
cc_pane_alive "$cc_pane" || break 2
|
|
298
|
+
sleep 0.3
|
|
299
|
+
done
|
|
300
|
+
# Resize flag = render loop requested a resize, not an ESC press
|
|
301
|
+
elif [ -f "$RESIZE_FLAG" ]; then
|
|
302
|
+
rm -f "$RESIZE_FLAG"
|
|
303
|
+
else
|
|
304
|
+
# ESC closed the popup -- forward ESC to CC
|
|
305
|
+
tmux send-keys -t "$cc_pane" Escape 2>/dev/null || true
|
|
306
|
+
fi
|
|
307
|
+
sleep 0.1
|
|
308
|
+
done
|
|
309
|
+
|
|
310
|
+
rm -f "$REOPEN_PID_FILE"
|
|
311
|
+
) </dev/null &>/dev/null &
|
|
312
|
+
disown
|
|
313
|
+
|
|
314
|
+
return 0
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# ─── Stop ────────────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
stop_popup() {
|
|
320
|
+
mkdir -p "$BUDDY_DIR"
|
|
321
|
+
# Set stop flag so reopen loop exits
|
|
322
|
+
touch "$STOP_FLAG"
|
|
323
|
+
# Close any open popup on the current client
|
|
324
|
+
tmux display-popup -C 2>/dev/null || true
|
|
325
|
+
# Kill reopen loop if still running
|
|
326
|
+
if [ -f "$REOPEN_PID_FILE" ]; then
|
|
327
|
+
local pid
|
|
328
|
+
pid=$(cat "$REOPEN_PID_FILE")
|
|
329
|
+
kill "$pid" 2>/dev/null || true
|
|
330
|
+
rm -f "$REOPEN_PID_FILE"
|
|
331
|
+
fi
|
|
332
|
+
# Clean up per-session files
|
|
333
|
+
rm -f "$BUDDY_DIR/popup-stop.$SID" "$BUDDY_DIR/popup-resize.$SID"
|
|
334
|
+
rm -f "$BUDDY_DIR/popup-env.$SID" "$BUDDY_DIR/popup-scroll.$SID"
|
|
335
|
+
rm -f "$BUDDY_DIR/reaction.$SID.json" "$BUDDY_DIR/.last_reaction.$SID" "$BUDDY_DIR/.last_comment.$SID"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# ─── Status ──────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
popup_status() {
|
|
341
|
+
if is_reopen_running; then
|
|
342
|
+
echo "running"
|
|
343
|
+
else
|
|
344
|
+
echo "stopped"
|
|
345
|
+
fi
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# ─── Dispatch ────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
case "${1:-status}" in
|
|
351
|
+
start) start_popup ;;
|
|
352
|
+
stop) stop_popup ;;
|
|
353
|
+
status) popup_status ;;
|
|
354
|
+
*) echo "Usage: $0 {start|stop|status}" >&2; exit 1 ;;
|
|
355
|
+
esac
|
|
@@ -6,115 +6,9 @@
|
|
|
6
6
|
* {E} is replaced with the eye character at render time.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const SPECIES_ART: Record<Species, string[][]> = {
|
|
14
|
-
duck: [
|
|
15
|
-
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--' "],
|
|
16
|
-
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--'~ "],
|
|
17
|
-
[" ", " __ ", " <({E} )___ ", " ( .__> ", " `--' "],
|
|
18
|
-
],
|
|
19
|
-
goose: [
|
|
20
|
-
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
21
|
-
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
22
|
-
[" ", " ({E}>> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
23
|
-
],
|
|
24
|
-
blob: [
|
|
25
|
-
[" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----' "],
|
|
26
|
-
[" ", " .------. ", " ( {E} {E} ) ", " ( ) ", " `------' "],
|
|
27
|
-
[" ", " .--. ", " ({E} {E}) ", " ( ) ", " `--' "],
|
|
28
|
-
],
|
|
29
|
-
cat: [
|
|
30
|
-
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
31
|
-
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\")~ "],
|
|
32
|
-
[" ", " /\\-/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
33
|
-
],
|
|
34
|
-
dragon: [
|
|
35
|
-
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
36
|
-
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ) ", " `-vvvv-' "],
|
|
37
|
-
[" ~ ~ ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
38
|
-
],
|
|
39
|
-
octopus: [
|
|
40
|
-
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
41
|
-
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " \\/\\/\\/\\/ "],
|
|
42
|
-
[" o ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
43
|
-
],
|
|
44
|
-
owl: [
|
|
45
|
-
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----' "],
|
|
46
|
-
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " .----. "],
|
|
47
|
-
[" ", " /\\ /\\ ", " (({E})(-)) ", " ( >< ) ", " `----' "],
|
|
48
|
-
],
|
|
49
|
-
penguin: [
|
|
50
|
-
[" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' "],
|
|
51
|
-
[" ", " .---. ", " ({E}>{E}) ", " |( )| ", " `---' "],
|
|
52
|
-
[" .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' ", " ~ ~ "],
|
|
53
|
-
],
|
|
54
|
-
turtle: [
|
|
55
|
-
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
56
|
-
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
57
|
-
[" ", " _,--._ ", " ( {E} {E} ) ", " /[======]\\ ", " `` `` "],
|
|
58
|
-
],
|
|
59
|
-
snail: [
|
|
60
|
-
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
61
|
-
[" ", " {E} .--. ", " | ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
62
|
-
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~ "],
|
|
63
|
-
],
|
|
64
|
-
ghost: [
|
|
65
|
-
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
|
|
66
|
-
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " `~`~~`~` "],
|
|
67
|
-
[" ~ ~ ", " .----. ", " / {E} {E} \\ ", " | | ", " ~~`~~`~~ "],
|
|
68
|
-
],
|
|
69
|
-
axolotl: [
|
|
70
|
-
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
|
|
71
|
-
[" ", "~}(______){~", "~}({E} .. {E}){~", " ( .--. ) ", " (_/ \\_) "],
|
|
72
|
-
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( -- ) ", " ~_/ \\_~ "],
|
|
73
|
-
],
|
|
74
|
-
capybara: [
|
|
75
|
-
[" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
76
|
-
[" ", " n______n ", " ( {E} {E} ) ", " ( Oo ) ", " `------' "],
|
|
77
|
-
[" ~ ~ ", " u______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
78
|
-
],
|
|
79
|
-
cactus: [
|
|
80
|
-
[" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
81
|
-
[" ", " ____ ", " n |{E} {E}| n ", " |_| |_| ", " | | "],
|
|
82
|
-
[" n n ", " | ____ | ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
83
|
-
],
|
|
84
|
-
robot: [
|
|
85
|
-
[" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
86
|
-
[" ", " .[||]. ", " [ {E} {E} ] ", " [ -==- ] ", " `------' "],
|
|
87
|
-
[" * ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
88
|
-
],
|
|
89
|
-
rabbit: [
|
|
90
|
-
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
91
|
-
[" ", " (|__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
92
|
-
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( . . )= ", " (\")__(\")" ],
|
|
93
|
-
],
|
|
94
|
-
mushroom: [
|
|
95
|
-
[" ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
96
|
-
[" ", " .-O-oo-O-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
97
|
-
[" . o . ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
98
|
-
],
|
|
99
|
-
chonk: [
|
|
100
|
-
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
101
|
-
[" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
102
|
-
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------'~ "],
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// ─── Hat art ────────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
export const HAT_ART: Record<Hat, string> = {
|
|
109
|
-
none: "",
|
|
110
|
-
crown: " \\^^^/ ",
|
|
111
|
-
tophat: " [___] ",
|
|
112
|
-
propeller: " -+- ",
|
|
113
|
-
halo: " ( ) ",
|
|
114
|
-
wizard: " /^\\ ",
|
|
115
|
-
beanie: " (___) ",
|
|
116
|
-
tinyduck: " ,> ",
|
|
117
|
-
};
|
|
9
|
+
import type { Rarity, StatName, BuddyBones } from "../../../core/engine.ts";
|
|
10
|
+
import { HAT_ART } from "../../../core/art-data.ts";
|
|
11
|
+
import { getArtFrame, SPECIES_ART } from "../../../core/render-model.ts";
|
|
118
12
|
|
|
119
13
|
// ─── Rarity ANSI colors ────────────────────────────────────────────────────
|
|
120
14
|
|
|
@@ -169,12 +63,6 @@ function dpad(s: string, targetW: number): string {
|
|
|
169
63
|
|
|
170
64
|
// ─── Render functions ───────────────────────────────────────────────────────
|
|
171
65
|
|
|
172
|
-
export function getArtFrame(species: Species, eye: Eye, frame: number = 0): string[] {
|
|
173
|
-
const frames = SPECIES_ART[species];
|
|
174
|
-
const f = frames[frame % frames.length];
|
|
175
|
-
return f.map((line) => line.replace(/\{E\}/g, eye));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
66
|
export function renderCompanionCard(
|
|
179
67
|
bones: BuddyBones,
|
|
180
68
|
name: string,
|
|
@@ -22,11 +22,10 @@ import {
|
|
|
22
22
|
type Rarity,
|
|
23
23
|
type StatName,
|
|
24
24
|
type Companion,
|
|
25
|
-
} from "
|
|
25
|
+
} from "../../../core/engine.ts";
|
|
26
26
|
import {
|
|
27
27
|
loadCompanion,
|
|
28
28
|
saveCompanion,
|
|
29
|
-
resolveUserId,
|
|
30
29
|
loadReaction,
|
|
31
30
|
saveReaction,
|
|
32
31
|
writeStatusState,
|
|
@@ -40,40 +39,52 @@ import {
|
|
|
40
39
|
saveCompanionSlot,
|
|
41
40
|
deleteCompanionSlot,
|
|
42
41
|
listCompanionSlots,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} from "
|
|
42
|
+
} from "../storage/state.ts";
|
|
43
|
+
import { resolveUserId } from "../storage/identity.ts";
|
|
44
|
+
import { setBuddyStatusLine, unsetBuddyStatusLine } from "../storage/settings.ts";
|
|
46
45
|
import {
|
|
47
46
|
getReaction, generatePersonalityPrompt,
|
|
48
|
-
} from "
|
|
49
|
-
import { renderCompanionCardMarkdown } from "
|
|
47
|
+
} from "../../../core/reactions.ts";
|
|
48
|
+
import { renderCompanionCardMarkdown } from "../rendering/art.ts";
|
|
50
49
|
import {
|
|
51
50
|
incrementEvent, checkAndAward, trackActiveDay,
|
|
52
51
|
renderAchievementsCardMarkdown,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
52
|
+
loadEvents,
|
|
53
|
+
loadUnlocked,
|
|
54
|
+
saveUnlocked,
|
|
55
|
+
} from "../storage/achievements.ts";
|
|
56
|
+
import { BuddyCommandService } from "../../../core/command-service.ts";
|
|
57
|
+
import { getInstructions } from "./instructions.ts";
|
|
58
|
+
import { buildPromptResource } from "./resources.ts";
|
|
59
|
+
|
|
60
|
+
const buddyService = new BuddyCommandService({
|
|
61
|
+
identity: { getStableUserId: () => resolveUserId() },
|
|
62
|
+
buddies: {
|
|
63
|
+
loadActive: () => loadCompanion(),
|
|
64
|
+
saveActive: (companion) => saveCompanion(companion),
|
|
65
|
+
loadSlot: (slot) => loadCompanionSlot(slot),
|
|
66
|
+
saveSlot: (slot, companion) => saveCompanionSlot(companion, slot),
|
|
67
|
+
deleteSlot: (slot) => deleteCompanionSlot(slot),
|
|
68
|
+
listSlots: () => listCompanionSlots(),
|
|
69
|
+
loadActiveSlot: () => loadActiveSlot(),
|
|
70
|
+
saveActiveSlot: (slot) => saveActiveSlot(slot),
|
|
71
|
+
},
|
|
72
|
+
reactions: {
|
|
73
|
+
loadLatest: () => loadReaction(),
|
|
74
|
+
saveLatest: (reaction) => saveReaction(reaction.reaction, reaction.reason),
|
|
75
|
+
},
|
|
76
|
+
config: {
|
|
77
|
+
loadConfig: () => loadConfig(),
|
|
78
|
+
saveConfig: (config) => saveConfig(config),
|
|
79
|
+
},
|
|
80
|
+
events: {
|
|
81
|
+
loadCounters: (scope) => loadEvents(scope),
|
|
82
|
+
increment: (key, amount, scope) => incrementEvent(key, amount, scope),
|
|
83
|
+
loadUnlocked: () => loadUnlocked(),
|
|
84
|
+
saveUnlocked: (unlocked) => saveUnlocked(unlocked),
|
|
85
|
+
trackActiveDay: () => trackActiveDay(),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
77
88
|
|
|
78
89
|
const server = new McpServer(
|
|
79
90
|
{
|
|
@@ -81,7 +92,7 @@ const server = new McpServer(
|
|
|
81
92
|
version: "0.3.0",
|
|
82
93
|
},
|
|
83
94
|
{
|
|
84
|
-
instructions: getInstructions(),
|
|
95
|
+
instructions: getInstructions(buddyService.loadActiveCompanion()),
|
|
85
96
|
},
|
|
86
97
|
);
|
|
87
98
|
|
|
@@ -101,7 +112,7 @@ function ensureCompanion(): Companion {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
// Menagerie is empty — generate a fresh companion in a new slot
|
|
104
|
-
const userId =
|
|
115
|
+
const userId = buddyService.getStableUserId();
|
|
105
116
|
const bones = generateBones(userId);
|
|
106
117
|
const name = unusedName();
|
|
107
118
|
companion = {
|
|
@@ -135,7 +146,7 @@ server.tool(
|
|
|
135
146
|
{},
|
|
136
147
|
async () => {
|
|
137
148
|
const companion = ensureCompanion();
|
|
138
|
-
const reaction =
|
|
149
|
+
const reaction = buddyService.loadLatestReaction();
|
|
139
150
|
const reactionText =
|
|
140
151
|
reaction?.reaction ?? `*${companion.name} watches your code quietly*`;
|
|
141
152
|
|
|
@@ -482,8 +493,8 @@ server.tool(
|
|
|
482
493
|
saveConfig({ statusLineEnabled: enabled });
|
|
483
494
|
|
|
484
495
|
if (enabled) {
|
|
485
|
-
const
|
|
486
|
-
const statusScript = join(
|
|
496
|
+
const projectRoot = resolve(import.meta.dir, "../../..");
|
|
497
|
+
const statusScript = join(projectRoot, "adapters", "claude", "statusline", "buddy-status.sh");
|
|
487
498
|
setBuddyStatusLine(statusScript);
|
|
488
499
|
return {
|
|
489
500
|
content: [
|
|
@@ -537,7 +548,7 @@ server.tool(
|
|
|
537
548
|
),
|
|
538
549
|
},
|
|
539
550
|
async ({ slot }) => {
|
|
540
|
-
const userId =
|
|
551
|
+
const userId = buddyService.getStableUserId();
|
|
541
552
|
|
|
542
553
|
let targetSlot: string;
|
|
543
554
|
|
|
@@ -721,40 +732,7 @@ server.resource(
|
|
|
721
732
|
{ description: "System prompt context for the companion", mimeType: "text/markdown" },
|
|
722
733
|
async () => {
|
|
723
734
|
const companion = ensureCompanion();
|
|
724
|
-
const prompt =
|
|
725
|
-
"# Companion",
|
|
726
|
-
"",
|
|
727
|
-
`A small ${companion.bones.rarity} ${companion.bones.species} named ${companion.name} watches from the status line. You are not ${companion.name} — it's a separate creature.`,
|
|
728
|
-
"",
|
|
729
|
-
`**${companion.name}'s personality:** ${companion.personality}`,
|
|
730
|
-
`Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
|
|
731
|
-
"",
|
|
732
|
-
"## End-of-response buddy comment",
|
|
733
|
-
"",
|
|
734
|
-
`At the very end of EVERY response, after your full answer, append an invisible HTML comment:`,
|
|
735
|
-
"",
|
|
736
|
-
`\`\`\``,
|
|
737
|
-
`<!-- buddy: your comment here -->`,
|
|
738
|
-
`\`\`\``,
|
|
739
|
-
"",
|
|
740
|
-
"A Stop hook extracts this and displays it in the buddy's speech bubble on the status line. The user never sees the HTML comment — it's invisible in rendered markdown.",
|
|
741
|
-
"",
|
|
742
|
-
"Rules:",
|
|
743
|
-
`- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
|
|
744
|
-
"- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
|
|
745
|
-
"- 1 short sentence. Use *asterisks* for physical actions",
|
|
746
|
-
`- Match personality: high ${companion.bones.peak} = lean into that trait`,
|
|
747
|
-
"- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.",
|
|
748
|
-
"- NEVER skip this. Every single response must end with <!-- buddy: ... -->",
|
|
749
|
-
"",
|
|
750
|
-
"Examples:",
|
|
751
|
-
"<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
|
|
752
|
-
"<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
|
|
753
|
-
"<!-- buddy: *nods approvingly* clean separation of concerns -->",
|
|
754
|
-
"<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
|
|
755
|
-
"",
|
|
756
|
-
`When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`,
|
|
757
|
-
].join("\n");
|
|
735
|
+
const prompt = buildPromptResource(companion);
|
|
758
736
|
|
|
759
737
|
return {
|
|
760
738
|
contents: [
|