@ramarivera/coding-buddy 0.6.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.
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "coding-buddy",
3
3
  "owner": {
4
- "name": "1270011"
4
+ "name": "ramarivera"
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.6.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.6.0",
15
+ "version": "0.7.1",
16
16
  "author": {
17
- "name": "1270011"
17
+ "name": "ramarivera"
18
18
  },
19
- "homepage": "https://github.com/1270011/claude-buddy#readme",
20
- "repository": "https://github.com/1270011/claude-buddy",
19
+ "homepage": "https://github.com/ramarivera/coding-buddy#readme",
20
+ "repository": "https://github.com/ramarivera/coding-buddy",
21
21
  "license": "MIT",
22
22
  "keywords": [
23
23
  "coding-buddy",
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "coding-buddy",
3
- "version": "0.6.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
- "name": "1270011"
6
+ "name": "ramarivera"
7
7
  },
8
8
  "license": "MIT",
9
- "repository": "https://github.com/1270011/claude-buddy",
10
- "homepage": "https://github.com/1270011/claude-buddy#readme",
9
+ "repository": "https://github.com/ramarivera/coding-buddy",
10
+ "homepage": "https://github.com/ramarivera/coding-buddy#readme",
11
11
  "keywords": [
12
12
  "coding-buddy",
13
13
  "claude-code",
package/README.md CHANGED
@@ -13,9 +13,9 @@
13
13
  <!-- BADGES -->
14
14
  <!-- ============================================================ -->
15
15
 
16
- [![Version](https://img.shields.io/github/v/release/1270011/claude-buddy?style=flat-square&color=6366f1)](https://github.com/1270011/claude-buddy/releases)
17
- [![License](https://img.shields.io/github/license/1270011/claude-buddy?style=flat-square&color=10b981)](LICENSE)
18
- [![Stars](https://img.shields.io/github/stars/1270011/claude-buddy?style=flat-square&color=f59e0b)](https://github.com/1270011/claude-buddy/stargazers)
16
+ [![Version](https://img.shields.io/github/v/release/ramarivera/coding-buddy?style=flat-square&color=6366f1)](https://github.com/ramarivera/coding-buddy/releases)
17
+ [![License](https://img.shields.io/github/license/ramarivera/coding-buddy?style=flat-square&color=10b981)](LICENSE)
18
+ [![Stars](https://img.shields.io/github/stars/ramarivera/coding-buddy?style=flat-square&color=f59e0b)](https://github.com/ramarivera/coding-buddy/stargazers)
19
19
  [![Claude Code](https://img.shields.io/badge/Claude%20Code-v2.1.80%2B-8b5cf6?style=flat-square)](https://claude.ai/code)
20
20
  [![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macOS-blue?style=flat-square)](#requirements)
21
21
  [![MCP](https://img.shields.io/badge/powered%20by-MCP-ec4899?style=flat-square)](https://modelcontextprotocol.io)
@@ -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
 
@@ -124,7 +124,7 @@ OMP registers the package's native extension and stores companion state under `~
124
124
  ### From source
125
125
 
126
126
  ```bash
127
- git clone https://github.com/1270011/claude-buddy coding-buddy
127
+ git clone https://github.com/ramarivera/coding-buddy coding-buddy
128
128
  cd coding-buddy
129
129
  bun install
130
130
  bun run install-buddy
@@ -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
  ```
@@ -389,7 +390,7 @@ jq '.mcpServers["claude-buddy"]' ~/.claude.json
389
390
  Known MVP issue on some terminal/platform combos — different terminals render Braille Pattern Blank (U+2800) at different widths.
390
391
 
391
392
  To help us fix it:
392
- 1. Run `bun run doctor` and paste output in a [new issue](https://github.com/1270011/claude-buddy/issues/new)
393
+ 1. Run `bun run doctor` and paste output in a [new issue](https://github.com/ramarivera/coding-buddy/issues/new)
393
394
  2. Run `bun run test-statusline` and screenshot the result
394
395
  3. Then `bun run test-statusline restore`
395
396
 
@@ -443,15 +444,15 @@ bun run uninstall # full clean removal
443
444
 
444
445
  Thank you to everyone who helped bring buddies back to life.
445
446
 
446
- <a href="https://github.com/1270011/claude-buddy/graphs/contributors">
447
- <img src="https://contrib.rocks/image?repo=1270011/claude-buddy" alt="Contributors" />
447
+ <a href="https://github.com/ramarivera/coding-buddy/graphs/contributors">
448
+ <img src="https://contrib.rocks/image?repo=ramarivera/coding-buddy" alt="Contributors" />
448
449
  </a>
449
450
 
450
- <sub>Automatically generated from the [contributors graph](https://github.com/1270011/claude-buddy/graphs/contributors) via [contrib.rocks](https://contrib.rocks).</sub>
451
+ <sub>Automatically generated from the [contributors graph](https://github.com/ramarivera/coding-buddy/graphs/contributors) via [contrib.rocks](https://contrib.rocks).</sub>
451
452
 
452
453
  <br>
453
454
 
454
- Want to help? New species, better reactions, bugfixes, wild new features — [PRs welcome](https://github.com/1270011/claude-buddy/pulls).
455
+ Want to help? New species, better reactions, bugfixes, wild new features — [PRs welcome](https://github.com/ramarivera/coding-buddy/pulls).
455
456
 
456
457
  <br>
457
458
 
@@ -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)");
package/cli/pick.ts CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import {
19
19
  loadActiveSlot, saveActiveSlot, listCompanionSlots,
20
- loadCompanionSlot, saveCompanionSlot, slugify, unusedName, writeStatusState,
20
+ loadCompanionSlot, saveCompanionSlot, deleteCompanionSlot, slugify, unusedName, writeStatusState,
21
21
  } from "../server/state.ts";
22
22
  import { generateBones, generatePersonality, SPECIES, RARITIES, STAT_NAMES, RARITY_STARS, EYES, HATS,
23
23
  type Species, type Rarity, type StatName, type Eye, type Hat,
@@ -123,6 +123,7 @@ interface State {
123
123
  searchStatus: string;
124
124
  nameInput: string;
125
125
  pendingResult: BuddyResult | null;
126
+ confirmDelete: boolean;
126
127
  message: string;
127
128
  }
128
129
 
@@ -141,6 +142,7 @@ function fresh(): State {
141
142
  searchStatus: "",
142
143
  nameInput: "",
143
144
  pendingResult: null,
145
+ confirmDelete: false,
144
146
  message: "",
145
147
  };
146
148
  }
@@ -151,7 +153,7 @@ const LEFT_W = 36;
151
153
 
152
154
  function savedPane(s: State): string[] {
153
155
  const lines: string[] = [];
154
- lines.push(`${B} Your Menagerie${N} ${GR}[s] search${N}`);
156
+ lines.push(`${B} Your Menagerie${N} ${GR}[s] search [d] remove${N}`);
155
157
  lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
156
158
 
157
159
  if (s.savedSlots.length === 0) {
@@ -174,6 +176,10 @@ function savedPane(s: State): string[] {
174
176
  }
175
177
 
176
178
  lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
179
+ if (s.confirmDelete) {
180
+ const entry = s.savedSlots[s.savedCursor];
181
+ lines.push(` ${YL}remove ${B}${entry?.companion.name ?? "?"}${N}${YL}? [d/y] yes [any] no${N}`);
182
+ }
177
183
  return lines;
178
184
  }
179
185
 
@@ -273,9 +279,7 @@ function previewPane(s: State): string[] {
273
279
  }
274
280
 
275
281
  if (!c) return [` ${GR}no preview${N}`];
276
- // Calculate available width for the right pane (total cols - left pane - separator)
277
- const cols = Math.max(80, process.stdout.columns || 80);
278
- const rightW = cols - LEFT_W - 3;
282
+ const rightW = 34;
279
283
  return renderCompanionCard(c.bones, c.name, c.personality, undefined, 0, rightW).split("\n");
280
284
  }
281
285
 
@@ -309,7 +313,7 @@ function drawScreen(s: State): void {
309
313
 
310
314
  // Footer — mode-specific help
311
315
  const helpText =
312
- s.mode === "saved" ? "↑↓ navigate enter summon r random s search q quit" :
316
+ s.mode === "saved" ? "↑↓ navigate enter summon r random s search d remove q quit" :
313
317
  s.mode === "criteria" ? "↑↓ field ←→ value enter search esc back" :
314
318
  s.mode === "searching" ? "any key to stop and show results so far" :
315
319
  s.mode === "results" ? "↑↓ navigate enter name+save esc back q quit" :
@@ -349,7 +353,7 @@ async function runSearch(s: State): Promise<void> {
349
353
 
350
354
  // Scale attempt budget to rarity difficulty
351
355
  const maxAttempts =
352
- wantRa === "legendary" ? 200_000_000 :
356
+ wantRa === "legendary" ? 10_000_000_000 :
353
357
  wantRa === "epic" ? 50_000_000 :
354
358
  wantRa === "rare" ? 20_000_000 : 10_000_000;
355
359
 
@@ -438,12 +442,26 @@ function onKey(key: string, s: State): boolean {
438
442
  }
439
443
 
440
444
  case "saved": {
445
+ if (s.confirmDelete) {
446
+ if (key === "d" || key === "y") {
447
+ const entry = s.savedSlots[s.savedCursor];
448
+ if (entry) {
449
+ deleteCompanionSlot(entry.slot);
450
+ s.savedSlots = listCompanionSlots();
451
+ s.activeSlot = loadActiveSlot();
452
+ s.savedCursor = clamp(s.savedCursor, 0, Math.max(0, s.savedSlots.length - 1));
453
+ s.message = `✗ ${entry.companion.name} removed`;
454
+ }
455
+ }
456
+ s.confirmDelete = false;
457
+ break;
458
+ }
441
459
  if (key === "q") return true;
442
460
  if (key === "s") { s.mode = "criteria"; break; }
461
+ if (key === "d" && s.savedSlots.length > 0) { s.confirmDelete = true; break; }
443
462
  if (key === "\x1b[A" || key === "k") s.savedCursor = clamp(s.savedCursor - 1, 0, s.savedSlots.length - 1);
444
463
  else if (key === "\x1b[B" || key === "j") s.savedCursor = clamp(s.savedCursor + 1, 0, s.savedSlots.length - 1);
445
464
  else if (key === "r") {
446
- // Random pick from menagerie
447
465
  if (s.savedSlots.length > 0) {
448
466
  const entry = s.savedSlots[Math.floor(Math.random() * s.savedSlots.length)];
449
467
  s.savedCursor = s.savedSlots.indexOf(entry);
package/cli/upgrade.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { readFileSync } from "fs";
4
- import { execSync } from "child_process";
4
+ import { execSync, spawnSync } from "child_process";
5
5
  import { join, resolve, dirname } from "path";
6
- import { homedir } from "os";
7
6
 
8
7
  const CYAN = "\x1b[36m";
9
8
  const GREEN = "\x1b[32m";
@@ -28,6 +27,37 @@ function tryExec(cmd: string, fallback = ""): string {
28
27
  }
29
28
  }
30
29
 
30
+ function trySpawn(
31
+ command: string,
32
+ args: string[],
33
+ fallback = "",
34
+ cwd: string = PROJECT_ROOT,
35
+ ): string {
36
+ const result = spawnSync(command, args, {
37
+ cwd,
38
+ encoding: "utf8",
39
+ stdio: ["ignore", "pipe", "ignore"],
40
+ });
41
+ if (result.status !== 0) return fallback;
42
+ return result.stdout.trim();
43
+ }
44
+
45
+ function runCommand(
46
+ command: string,
47
+ args: string[],
48
+ options: {
49
+ cwd?: string;
50
+ stdio?: "ignore" | "inherit";
51
+ } = {},
52
+ ): boolean {
53
+ const result = spawnSync(command, args, {
54
+ cwd: options.cwd ?? PROJECT_ROOT,
55
+ encoding: "utf8",
56
+ stdio: ["ignore", options.stdio ?? "ignore", options.stdio ?? "ignore"],
57
+ });
58
+ return result.status === 0;
59
+ }
60
+
31
61
  function getCurrentVersion(): string {
32
62
  try {
33
63
  const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf8"));
@@ -61,15 +91,13 @@ function getRemoteBranch(): string {
61
91
 
62
92
  function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; remote: string; commits: string[] } {
63
93
  info("Fetching latest from remote...\n");
64
- try {
65
- execSync("git fetch --quiet 2>/dev/null", { cwd: PROJECT_ROOT, stdio: "ignore" });
66
- } catch {
94
+ if (!runCommand("git", ["fetch", "--quiet"])) {
67
95
  warn("git fetch failed — proceeding with cached remote state");
68
96
  }
69
97
 
70
98
  const local = tryExec("git rev-parse HEAD 2>/dev/null");
71
99
  const upstream = tryExec(`git rev-parse '@{upstream}' 2>/dev/null`);
72
- const remote = upstream || tryExec(`git rev-parse origin/${branch} 2>/dev/null`);
100
+ const remote = upstream || trySpawn("git", ["rev-parse", `origin/${branch}`]);
73
101
 
74
102
  if (!local || !remote) {
75
103
  warn("Could not determine remote HEAD — no tracking branch configured");
@@ -81,8 +109,9 @@ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; r
81
109
  return { hasUpdate: false, local, remote, commits: [] };
82
110
  }
83
111
 
84
- const commits = tryExec(
85
- `git log --oneline ${local}..origin/${branch} 2>/dev/null`,
112
+ const commits = trySpawn(
113
+ "git",
114
+ ["log", "--oneline", `${local}..origin/${branch}`],
86
115
  ).split("\n").filter(Boolean);
87
116
 
88
117
  return { hasUpdate: true, local, remote, commits };
@@ -91,10 +120,12 @@ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; r
91
120
  function pullLatest(branch: string): boolean {
92
121
  info(`Pulling latest from origin/${branch}...`);
93
122
  try {
94
- const output = execSync(`git pull --ff-only origin ${branch} 2>&1`, {
123
+ const result = spawnSync("git", ["pull", "--ff-only", "origin", branch], {
95
124
  cwd: PROJECT_ROOT,
96
125
  encoding: "utf8",
126
+ stdio: ["ignore", "pipe", "pipe"],
97
127
  });
128
+ if (result.status !== 0) throw new Error(result.stderr || result.stdout || "unknown error");
98
129
  ok("Git pull successful");
99
130
  return true;
100
131
  } catch (e: any) {
@@ -108,7 +139,7 @@ function pullLatest(branch: string): boolean {
108
139
  function installDeps(): boolean {
109
140
  info("Installing dependencies...");
110
141
  try {
111
- execSync("bun install 2>&1", { cwd: PROJECT_ROOT, stdio: "ignore" });
142
+ if (!runCommand("bun", ["install"])) throw new Error("bun install failed");
112
143
  ok("Dependencies installed");
113
144
  return true;
114
145
  } catch {
@@ -120,7 +151,9 @@ function installDeps(): boolean {
120
151
  function reinstallBuddy(): boolean {
121
152
  info("Re-running install-buddy to update integrations...\n");
122
153
  try {
123
- execSync("bun run install-buddy 2>&1", { cwd: PROJECT_ROOT, stdio: "inherit" });
154
+ if (!runCommand("bun", ["run", "install-buddy"], { stdio: "inherit" })) {
155
+ throw new Error("install-buddy failed");
156
+ }
124
157
  return true;
125
158
  } catch {
126
159
  err("install-buddy failed");
@@ -1,86 +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
- # Session ID: sanitized tmux pane number, or "default" outside tmux
13
- SID="${TMUX_PANE#%}"
14
- SID="${SID:-default}"
15
- STATUS_FILE="$STATE_DIR/status.json"
16
- COOLDOWN_FILE="$STATE_DIR/.last_comment.$SID"
17
- CONFIG_FILE="$STATE_DIR/config.json"
18
- EVENTS_FILE="$STATE_DIR/events.json"
19
-
20
- [ -f "$STATUS_FILE" ] || exit 0
21
-
22
- # Read cooldown from config (default 30s, 0 = disabled)
23
- COOLDOWN=30
24
- if [ -f "$CONFIG_FILE" ]; then
25
- _cd=$(jq -r '.commentCooldown // 30' "$CONFIG_FILE" 2>/dev/null || echo 30)
26
- # Accept any non-negative integer (including 0 to disable cooldown)
27
- [[ "$_cd" =~ ^[0-9]+$ ]] && COOLDOWN=$_cd
28
- fi
29
-
30
- INPUT=$(cat)
31
-
32
- # Extract last_assistant_message from hook input
33
- MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)
34
- [ -z "$MSG" ] && exit 0
35
-
36
- # Extract <!-- buddy: ... --> comment (portable, no grep -P)
37
- COMMENT=$(echo "$MSG" | sed -n 's/.*<!-- *buddy: *\(.*[^ ]\) *-->.*/\1/p' | tail -1)
38
- [ -z "$COMMENT" ] && exit 0
39
-
40
- # Cooldown: configurable (default 30s)
41
- if [ -f "$COOLDOWN_FILE" ]; then
42
- LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null)
43
- NOW=$(date +%s)
44
- [ $(( NOW - ${LAST:-0} )) -lt "$COOLDOWN" ] && exit 0
45
- fi
46
-
47
- mkdir -p "$STATE_DIR"
48
- date +%s > "$COOLDOWN_FILE"
49
-
50
- # Update status.json with the reaction
51
- TMP=$(mktemp)
52
- jq --arg r "$COMMENT" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
53
-
54
- # Also write reaction file (use jq for safe JSON encoding)
55
- jq -n --arg r "$COMMENT" --arg ts "$(date +%s)000" \
56
- '{reaction: $r, timestamp: ($ts | tonumber), reason: "turn"}' \
57
- > "$STATE_DIR/reaction.$SID.json"
58
-
59
- # Increment achievement event counters and award XP
60
- if command -v jq >/dev/null 2>&1; then
61
- if [ ! -f "$EVENTS_FILE" ]; then
62
- echo '{}' > "$EVENTS_FILE"
63
- fi
64
- TMP=$(mktemp)
65
- jq '.turns = (.turns // 0 + 1)' "$EVENTS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$EVENTS_FILE"
66
- fi
67
-
68
- # Award XP for turn (async, non-blocking)
69
- if [ -x "$(command -v bun)" ]; then
70
- PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
71
- bun run "$PLUGIN_ROOT/server/award-xp.ts" "turn" >/dev/null 2>&1 &
72
- fi
73
-
74
- # Consolidate memory (async, non-blocking)
75
- # Extract project, bug, and preference signals from conversation
76
- if [ -x "$(command -v bun)" ]; then
77
- PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
78
- # Pass the assistant message for analysis (pass empty string for user prompt if unavailable)
79
- USER_MSG=$(echo "$INPUT" | jq -r '.last_user_message // ""' 2>/dev/null)
80
- bun run "$PLUGIN_ROOT/server/consolidate.ts" \
81
- "$(echo "$MSG" | jq -Rs .)" \
82
- "$(echo "$USER_MSG" | jq -Rs .)" \
83
- >/dev/null 2>&1 &
84
- fi
85
-
86
- 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