@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.
@@ -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.0"
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.0",
15
+ "version": "0.7.1",
16
16
  "author": {
17
17
  "name": "ramarivera"
18
18
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-buddy",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi. MCP-based terminal pet with ASCII art, stats, reactions, and personality.",
5
5
  "author": {
6
6
  "name": "ramarivera"
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/ # PostToolUse + Stop hooks (error & comment detection)
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
- export function renderBuddyStatus(companion: Companion, reaction?: ReactionState | null): string {
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
- const suffix = reaction?.reaction ? ` — ${reaction.reaction}` : "";
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("", `“${reaction.reaction}”`);
93
+ lines.push("", ...renderReactionBubble(reaction.reaction));
37
94
  }
38
95
 
39
96
  if (achievements.length > 0) {
@@ -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, muted ? null : currentReaction);
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: [{ type: "command", command: toUnixPath(reactHook) }],
207
+ hooks: [commandHook(reactHook)],
202
208
  });
203
209
  settings.hooks.PostToolUse.push({
204
210
  matcher: "Write|Edit",
205
- hooks: [{ type: "command", command: toUnixPath(fileTypeHook) }],
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: [{ type: "command", command: toUnixPath(commentHook) }],
220
+ hooks: [commandHook(commentHook)],
215
221
  });
216
222
  settings.hooks.Stop.push({
217
- hooks: [{ type: "command", command: toUnixPath(suggestHook) }],
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: [{ type: "command", command: toUnixPath(nameHook) }],
233
+ hooks: [commandHook(nameHook)],
228
234
  });
229
235
  settings.hooks.UserPromptSubmit.push({
230
- hooks: [{ type: "command", command: toUnixPath(moodHook) }],
236
+ hooks: [commandHook(moodHook)],
231
237
  });
232
238
 
233
239
  ok("Hooks registered: PostToolUse (Bash + Write/Edit) + Stop (comment + suggest) + UserPromptSubmit (name + mood)");
@@ -1,81 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # buddy-comment Stop hook
3
- # Extracts hidden buddy comment from Claude's response.
4
- # Claude writes: <!-- buddy: *adjusts tophat* nice code -->
5
- # This hook extracts it and updates the status line bubble.
6
- # The HTML comment is invisible in rendered markdown output.
7
-
8
- # shellcheck source=../scripts/paths.sh
9
- source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
10
-
11
- STATE_DIR="$BUDDY_STATE_DIR"
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