@ramarivera/coding-buddy 0.7.0 → 0.7.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/adapters/omp/renderers.ts +61 -4
- package/adapters/omp/ui.ts +1 -1
- package/cli/install.ts +12 -6
- package/hooks/buddy-comment.sh +10 -80
- package/hooks/file-type-react.sh +10 -402
- package/hooks/hooks.json +19 -5
- package/hooks/legacy/file-type-react.sh +403 -0
- package/hooks/legacy/mood-react.sh +210 -0
- package/hooks/legacy/name-react.sh +178 -0
- package/hooks/legacy/react.sh +1364 -0
- package/hooks/mood-react.sh +10 -209
- package/hooks/name-react.sh +10 -177
- package/hooks/react.sh +10 -1363
- package/hooks/suggest.sh +10 -54
- package/package.json +1 -1
- package/server/hooks/buddy-comment.ts +93 -0
- package/server/hooks/common.ts +95 -0
- package/server/hooks/legacy-bash.ts +34 -0
- package/server/hooks/suggest.ts +60 -0
- package/statusline/buddy-status.sh +8 -5
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi (requires bun: https://bun.sh/install)",
|
|
8
|
-
"version": "0.7.
|
|
8
|
+
"version": "0.7.1"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "coding-buddy",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi. MCP-based terminal pet with ASCII art, stats, reactions, and personality. Requires bun runtime (https://bun.sh/install).",
|
|
15
|
-
"version": "0.7.
|
|
15
|
+
"version": "0.7.1",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "ramarivera"
|
|
18
18
|
},
|
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ The `claude-buddy` package ended at **v0.5.2**.
|
|
|
103
103
|
npx -y @ramarivera/coding-buddy
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
This installs the MCP server, skill, hooks, and status line. Restart Claude Code and type `/buddy`.
|
|
106
|
+
This installs the MCP server, skill, hooks, and status line. Claude Code hooks are registered as thin bash wrappers with 15s timeouts; the wrappers locate Bun in bare hook environments and delegate behavior to TypeScript or the phase-2 legacy bash bridge. Restart Claude Code and type `/buddy`.
|
|
107
107
|
|
|
108
108
|
### Pi
|
|
109
109
|
|
|
@@ -255,7 +255,8 @@ MCP is an industry-standard protocol. Skills are Markdown files. Hooks and statu
|
|
|
255
255
|
coding-buddy/
|
|
256
256
|
├── server/ # MCP server — tools, engine, art, reactions, state
|
|
257
257
|
├── skills/buddy/ # /buddy slash command
|
|
258
|
-
├── hooks/ #
|
|
258
|
+
├── hooks/ # Thin Claude Code hook wrappers and phase-2 legacy bash hooks
|
|
259
|
+
├── server/hooks/ # Hook behavior implemented in TypeScript
|
|
259
260
|
├── statusline/ # Animated right-aligned buddy display
|
|
260
261
|
└── cli/ # install, show, hunt, verify, doctor, backup, uninstall
|
|
261
262
|
```
|
|
@@ -7,11 +7,68 @@ function trimArt(line: string): string {
|
|
|
7
7
|
return line.replace(/\s+$/g, "");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const REACTION_WIDTH = 44;
|
|
11
|
+
|
|
12
|
+
function displayWidth(text: string): number {
|
|
13
|
+
let width = 0;
|
|
14
|
+
for (const character of text) {
|
|
15
|
+
width += /\p{Emoji_Presentation}/u.test(character) ? 2 : 1;
|
|
16
|
+
}
|
|
17
|
+
return width;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function truncateToWidth(text: string, width: number): string {
|
|
21
|
+
let result = "";
|
|
22
|
+
let currentWidth = 0;
|
|
23
|
+
for (const character of text) {
|
|
24
|
+
const characterWidth = /\p{Emoji_Presentation}/u.test(character) ? 2 : 1;
|
|
25
|
+
if (currentWidth + characterWidth > width) break;
|
|
26
|
+
result += character;
|
|
27
|
+
currentWidth += characterWidth;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function wrapReaction(reaction: string): string[] {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
let current = "";
|
|
35
|
+
|
|
36
|
+
for (const word of reaction.trim().split(/\s+/)) {
|
|
37
|
+
if (!word) continue;
|
|
38
|
+
if (displayWidth(word) > REACTION_WIDTH) {
|
|
39
|
+
if (current) lines.push(current);
|
|
40
|
+
lines.push(`${truncateToWidth(word, REACTION_WIDTH - 1)}…`);
|
|
41
|
+
current = "";
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
46
|
+
if (displayWidth(candidate) > REACTION_WIDTH) {
|
|
47
|
+
lines.push(current);
|
|
48
|
+
current = word;
|
|
49
|
+
} else {
|
|
50
|
+
current = candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (current) lines.push(current);
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderReactionBubble(reaction: string): string[] {
|
|
59
|
+
const lines = wrapReaction(`💬 ${reaction}`);
|
|
60
|
+
const border = `╭${"─".repeat(REACTION_WIDTH + 2)}╮`;
|
|
61
|
+
return [
|
|
62
|
+
border,
|
|
63
|
+
...lines.map((line) => `│ ${line}${" ".repeat(Math.max(0, REACTION_WIDTH - displayWidth(line)))} │`),
|
|
64
|
+
`╰${"─".repeat(REACTION_WIDTH + 2)}╯`,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function renderBuddyStatus(companion: Companion): string {
|
|
11
69
|
const shiny = companion.bones.shiny ? " ✨" : "";
|
|
12
70
|
const stars = RARITY_STARS[companion.bones.rarity];
|
|
13
|
-
|
|
14
|
-
return `${trimArt(getArtFrame(companion.bones.species, companion.bones.eye, Date.now())[0])} ${companion.name} ${stars}${shiny}${suffix}`;
|
|
71
|
+
return `${trimArt(getArtFrame(companion.bones.species, companion.bones.eye, Date.now())[0])} ${companion.name} ${stars}${shiny}`;
|
|
15
72
|
}
|
|
16
73
|
|
|
17
74
|
export function renderBuddyWidget(
|
|
@@ -33,7 +90,7 @@ export function renderBuddyWidget(
|
|
|
33
90
|
];
|
|
34
91
|
|
|
35
92
|
if (reaction?.reaction) {
|
|
36
|
-
lines.push("",
|
|
93
|
+
lines.push("", ...renderReactionBubble(reaction.reaction));
|
|
37
94
|
}
|
|
38
95
|
|
|
39
96
|
if (achievements.length > 0) {
|
package/adapters/omp/ui.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class OmpBuddyUI {
|
|
|
27
27
|
): void {
|
|
28
28
|
const currentReaction = reaction ?? null;
|
|
29
29
|
const muted = this.storage.isMuted();
|
|
30
|
-
const status = renderBuddyStatus(companion
|
|
30
|
+
const status = renderBuddyStatus(companion);
|
|
31
31
|
ctx.ui.setStatus("buddy", muted ? `${status} [muted]` : status);
|
|
32
32
|
ctx.ui.setWidget(
|
|
33
33
|
"buddy",
|
package/cli/install.ts
CHANGED
|
@@ -35,6 +35,7 @@ const SETTINGS_FILE = claudeSettingsPath();
|
|
|
35
35
|
const BUDDY_DIR = claudeSkillDir("buddy");
|
|
36
36
|
const CLAUDE_JSON_PATH = claudeUserConfigPath();
|
|
37
37
|
const PROJECT_ROOT = resolve(dirname(import.meta.dir));
|
|
38
|
+
const HOOK_TIMEOUT_SECONDS = 15;
|
|
38
39
|
|
|
39
40
|
function banner() {
|
|
40
41
|
console.log(`
|
|
@@ -187,6 +188,11 @@ function installHooks(settings: Record<string, any>) {
|
|
|
187
188
|
const suggestHook = join(PROJECT_ROOT, "hooks", "suggest.sh");
|
|
188
189
|
const nameHook = join(PROJECT_ROOT, "hooks", "name-react.sh");
|
|
189
190
|
const moodHook = join(PROJECT_ROOT, "hooks", "mood-react.sh");
|
|
191
|
+
const commandHook = (command: string) => ({
|
|
192
|
+
type: "command",
|
|
193
|
+
command: toUnixPath(command),
|
|
194
|
+
timeout: HOOK_TIMEOUT_SECONDS,
|
|
195
|
+
});
|
|
190
196
|
|
|
191
197
|
if (!settings.hooks) settings.hooks = {};
|
|
192
198
|
|
|
@@ -198,11 +204,11 @@ function installHooks(settings: Record<string, any>) {
|
|
|
198
204
|
);
|
|
199
205
|
settings.hooks.PostToolUse.push({
|
|
200
206
|
matcher: "Bash",
|
|
201
|
-
hooks: [
|
|
207
|
+
hooks: [commandHook(reactHook)],
|
|
202
208
|
});
|
|
203
209
|
settings.hooks.PostToolUse.push({
|
|
204
210
|
matcher: "Write|Edit",
|
|
205
|
-
hooks: [
|
|
211
|
+
hooks: [commandHook(fileTypeHook)],
|
|
206
212
|
});
|
|
207
213
|
|
|
208
214
|
// Stop: extract <!-- buddy: --> comment from Claude's response
|
|
@@ -211,10 +217,10 @@ function installHooks(settings: Record<string, any>) {
|
|
|
211
217
|
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
|
|
212
218
|
);
|
|
213
219
|
settings.hooks.Stop.push({
|
|
214
|
-
hooks: [
|
|
220
|
+
hooks: [commandHook(commentHook)],
|
|
215
221
|
});
|
|
216
222
|
settings.hooks.Stop.push({
|
|
217
|
-
hooks: [
|
|
223
|
+
hooks: [commandHook(suggestHook)],
|
|
218
224
|
});
|
|
219
225
|
|
|
220
226
|
// UserPromptSubmit: detect buddy's name in user message → instant status line
|
|
@@ -224,10 +230,10 @@ function installHooks(settings: Record<string, any>) {
|
|
|
224
230
|
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
|
|
225
231
|
);
|
|
226
232
|
settings.hooks.UserPromptSubmit.push({
|
|
227
|
-
hooks: [
|
|
233
|
+
hooks: [commandHook(nameHook)],
|
|
228
234
|
});
|
|
229
235
|
settings.hooks.UserPromptSubmit.push({
|
|
230
|
-
hooks: [
|
|
236
|
+
hooks: [commandHook(moodHook)],
|
|
231
237
|
});
|
|
232
238
|
|
|
233
239
|
ok("Hooks registered: PostToolUse (Bash + Write/Edit) + Stop (comment + suggest) + UserPromptSubmit (name + mood)");
|
package/hooks/buddy-comment.sh
CHANGED
|
@@ -1,81 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Per-session ID resolved by paths.sh (CLAUDE_CODE_SESSION_ID > TMUX_PANE > default)
|
|
13
|
-
SID="$BUDDY_SID"
|
|
14
|
-
STATUS_FILE="$STATE_DIR/status.json"
|
|
15
|
-
COOLDOWN_FILE="$STATE_DIR/.last_comment.$SID"
|
|
16
|
-
CONFIG_FILE="$STATE_DIR/config.json"
|
|
17
|
-
EVENTS_FILE="$STATE_DIR/events.json"
|
|
18
|
-
|
|
19
|
-
[ -f "$STATUS_FILE" ] || exit 0
|
|
20
|
-
|
|
21
|
-
# Read cooldown from config (default 30s, 0 = disabled)
|
|
22
|
-
COOLDOWN=30
|
|
23
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
24
|
-
_cd=$(jq -r '.commentCooldown // 30' "$CONFIG_FILE" 2>/dev/null || echo 30)
|
|
25
|
-
# Accept any non-negative integer (including 0 to disable cooldown)
|
|
26
|
-
[[ "$_cd" =~ ^[0-9]+$ ]] && COOLDOWN=$_cd
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
INPUT=$(cat)
|
|
30
|
-
|
|
31
|
-
# Extract last_assistant_message from hook input
|
|
32
|
-
MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)
|
|
33
|
-
[ -z "$MSG" ] && exit 0
|
|
34
|
-
|
|
35
|
-
# Extract <!-- buddy: ... --> comment (portable, no grep -P)
|
|
36
|
-
COMMENT=$(echo "$MSG" | sed -n 's/.*<!-- *buddy: *\(.*[^ ]\) *-->.*/\1/p' | tail -1)
|
|
37
|
-
[ -z "$COMMENT" ] && exit 0
|
|
38
|
-
|
|
39
|
-
# Cooldown: configurable (default 30s)
|
|
40
|
-
if [ -f "$COOLDOWN_FILE" ]; then
|
|
41
|
-
LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null)
|
|
42
|
-
NOW=$(date +%s)
|
|
43
|
-
[ $(( NOW - ${LAST:-0} )) -lt "$COOLDOWN" ] && exit 0
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
mkdir -p "$STATE_DIR"
|
|
47
|
-
date +%s > "$COOLDOWN_FILE"
|
|
48
|
-
|
|
49
|
-
# Write reaction to per-session file (read by the statusline via $SID)
|
|
50
|
-
jq -n --arg r "$COMMENT" --arg ts "$(date +%s)000" \
|
|
51
|
-
'{reaction: $r, timestamp: ($ts | tonumber), reason: "turn"}' \
|
|
52
|
-
> "$STATE_DIR/reaction.$SID.json"
|
|
53
|
-
|
|
54
|
-
# Increment achievement event counters and award XP
|
|
55
|
-
if command -v jq >/dev/null 2>&1; then
|
|
56
|
-
if [ ! -f "$EVENTS_FILE" ]; then
|
|
57
|
-
echo '{}' > "$EVENTS_FILE"
|
|
58
|
-
fi
|
|
59
|
-
TMP=$(mktemp)
|
|
60
|
-
jq '.turns = (.turns // 0 + 1)' "$EVENTS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$EVENTS_FILE"
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
# Award XP for turn (async, non-blocking)
|
|
64
|
-
if [ -x "$(command -v bun)" ]; then
|
|
65
|
-
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
66
|
-
bun run "$PLUGIN_ROOT/server/award-xp.ts" "turn" >/dev/null 2>&1 &
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
# Consolidate memory (async, non-blocking)
|
|
70
|
-
# Extract project, bug, and preference signals from conversation
|
|
71
|
-
if [ -x "$(command -v bun)" ]; then
|
|
72
|
-
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
73
|
-
# Pass the assistant message for analysis (pass empty string for user prompt if unavailable)
|
|
74
|
-
USER_MSG=$(echo "$INPUT" | jq -r '.last_user_message // ""' 2>/dev/null)
|
|
75
|
-
bun run "$PLUGIN_ROOT/server/consolidate.ts" \
|
|
76
|
-
"$(echo "$MSG" | jq -Rs .)" \
|
|
77
|
-
"$(echo "$USER_MSG" | jq -Rs .)" \
|
|
78
|
-
>/dev/null 2>&1 &
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
exit 0
|
|
2
|
+
set +e
|
|
3
|
+
SCRIPT_DIR="${BASH_SOURCE[0]%/*}"
|
|
4
|
+
ROOT="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" || exit 0
|
|
5
|
+
[ -n "$ROOT" ] || exit 0
|
|
6
|
+
BUN="$(command -v bun 2>/dev/null || true)"
|
|
7
|
+
[ -x "$BUN" ] || BUN="$HOME/.bun/bin/bun"
|
|
8
|
+
[ -x "$BUN" ] || BUN="/opt/homebrew/bin/bun"
|
|
9
|
+
[ -x "$BUN" ] || BUN="/usr/local/bin/bun"
|
|
10
|
+
[ -x "$BUN" ] || exit 0
|
|
11
|
+
exec "$BUN" run "$ROOT/server/hooks/buddy-comment.ts" || exit 0
|