@ramarivera/coding-buddy 0.6.0 → 0.7.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.
@@ -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.0"
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.0",
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.0",
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)
@@ -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
@@ -389,7 +389,7 @@ jq '.mcpServers["claude-buddy"]' ~/.claude.json
389
389
  Known MVP issue on some terminal/platform combos — different terminals render Braille Pattern Blank (U+2800) at different widths.
390
390
 
391
391
  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)
392
+ 1. Run `bun run doctor` and paste output in a [new issue](https://github.com/ramarivera/coding-buddy/issues/new)
393
393
  2. Run `bun run test-statusline` and screenshot the result
394
394
  3. Then `bun run test-statusline restore`
395
395
 
@@ -443,15 +443,15 @@ bun run uninstall # full clean removal
443
443
 
444
444
  Thank you to everyone who helped bring buddies back to life.
445
445
 
446
- <a href="https://github.com/1270011/claude-buddy/graphs/contributors">
447
- <img src="https://contrib.rocks/image?repo=1270011/claude-buddy" alt="Contributors" />
446
+ <a href="https://github.com/ramarivera/coding-buddy/graphs/contributors">
447
+ <img src="https://contrib.rocks/image?repo=ramarivera/coding-buddy" alt="Contributors" />
448
448
  </a>
449
449
 
450
- <sub>Automatically generated from the [contributors graph](https://github.com/1270011/claude-buddy/graphs/contributors) via [contrib.rocks](https://contrib.rocks).</sub>
450
+ <sub>Automatically generated from the [contributors graph](https://github.com/ramarivera/coding-buddy/graphs/contributors) via [contrib.rocks](https://contrib.rocks).</sub>
451
451
 
452
452
  <br>
453
453
 
454
- Want to help? New species, better reactions, bugfixes, wild new features — [PRs welcome](https://github.com/1270011/claude-buddy/pulls).
454
+ Want to help? New species, better reactions, bugfixes, wild new features — [PRs welcome](https://github.com/ramarivera/coding-buddy/pulls).
455
455
 
456
456
  <br>
457
457
 
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");
@@ -9,9 +9,8 @@
9
9
  source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
10
10
 
11
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}"
12
+ # Per-session ID resolved by paths.sh (CLAUDE_CODE_SESSION_ID > TMUX_PANE > default)
13
+ SID="$BUDDY_SID"
15
14
  STATUS_FILE="$STATE_DIR/status.json"
16
15
  COOLDOWN_FILE="$STATE_DIR/.last_comment.$SID"
17
16
  CONFIG_FILE="$STATE_DIR/config.json"
@@ -47,11 +46,7 @@ fi
47
46
  mkdir -p "$STATE_DIR"
48
47
  date +%s > "$COOLDOWN_FILE"
49
48
 
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)
49
+ # Write reaction to per-session file (read by the statusline via $SID)
55
50
  jq -n --arg r "$COMMENT" --arg ts "$(date +%s)000" \
56
51
  '{reaction: $r, timestamp: ($ts | tonumber), reason: "turn"}' \
57
52
  > "$STATE_DIR/reaction.$SID.json"
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- STATE_DIR="$HOME/.claude-buddy"
4
- SID="${TMUX_PANE#%}"
5
- SID="${SID:-default}"
3
+ # shellcheck source=../scripts/paths.sh
4
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
5
+
6
+ STATE_DIR="$BUDDY_STATE_DIR"
7
+ SID="$BUDDY_SID"
6
8
  REACTION_FILE="$STATE_DIR/reaction.$SID.json"
7
9
  STATUS_FILE="$STATE_DIR/status.json"
8
10
  COOLDOWN_FILE="$STATE_DIR/.last_reaction.$SID"
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- STATE_DIR="$HOME/.claude-buddy"
4
- SID="${TMUX_PANE#%}"
5
- SID="${SID:-default}"
3
+ # shellcheck source=../scripts/paths.sh
4
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
5
+
6
+ STATE_DIR="$BUDDY_STATE_DIR"
7
+ SID="$BUDDY_SID"
6
8
  REACTION_FILE="$STATE_DIR/reaction.$SID.json"
7
9
  STATUS_FILE="$STATE_DIR/status.json"
8
10
  COOLDOWN_FILE="$STATE_DIR/.last_mood.$SID"
@@ -8,9 +8,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
8
8
 
9
9
  STATE_DIR="$BUDDY_STATE_DIR"
10
10
  STATUS_FILE="$STATE_DIR/status.json"
11
- # Session ID: sanitized tmux pane number, or "default" outside tmux
12
- SID="${TMUX_PANE#%}"
13
- SID="${SID:-default}"
11
+ # Per-session ID resolved by paths.sh (CLAUDE_CODE_SESSION_ID > TMUX_PANE > default)
12
+ SID="$BUDDY_SID"
14
13
 
15
14
  [ -f "$STATUS_FILE" ] || exit 0
16
15
 
package/hooks/react.sh CHANGED
@@ -4,9 +4,8 @@
4
4
  source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
5
5
 
6
6
  STATE_DIR="$BUDDY_STATE_DIR"
7
- # Session ID: sanitized tmux pane number, or "default" outside tmux
8
- SID="${TMUX_PANE#%}"
9
- SID="${SID:-default}"
7
+ # Per-session ID resolved by paths.sh (CLAUDE_CODE_SESSION_ID > TMUX_PANE > default)
8
+ SID="$BUDDY_SID"
10
9
  REACTION_FILE="$STATE_DIR/reaction.$SID.json"
11
10
  STATUS_FILE="$STATE_DIR/status.json"
12
11
  COOLDOWN_FILE="$STATE_DIR/.last_reaction.$SID"
@@ -1310,9 +1309,6 @@ if [ -n "$REASON" ] && [ -n "$REACTION" ]; then
1310
1309
  '{reaction: $r, timestamp: ($ts | tonumber), reason: $reason}' \
1311
1310
  > "$REACTION_FILE"
1312
1311
 
1313
- TMP=$(mktemp)
1314
- jq --arg r "$REACTION" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
1315
-
1316
1312
  if command -v jq >/dev/null 2>&1; then
1317
1313
  XP_EVENT=""
1318
1314
  if [ ! -f "$EVENTS_FILE" ]; then
package/hooks/suggest.sh CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-buddy",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi",
5
5
  "type": "module",
6
6
  "omp": {
@@ -66,9 +66,9 @@
66
66
  ],
67
67
  "repository": {
68
68
  "type": "git",
69
- "url": "git+https://github.com/1270011/claude-buddy.git"
69
+ "url": "git+https://github.com/ramarivera/coding-buddy.git"
70
70
  },
71
- "homepage": "https://github.com/1270011/claude-buddy#readme",
71
+ "homepage": "https://github.com/ramarivera/coding-buddy#readme",
72
72
  "license": "MIT",
73
73
  "dependencies": {
74
74
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
package/scripts/paths.sh CHANGED
@@ -37,7 +37,22 @@ else
37
37
  BUDDY_STATE_DIR="$HOME/.claude-buddy"
38
38
  fi
39
39
 
40
- export CLAUDE_CFG_DIR CLAUDE_SETTINGS_FILE CLAUDE_USER_CONFIG BUDDY_STATE_DIR
40
+ # Per-session ID for namespacing transient buddy state files
41
+ # (reaction.$SID.json, .last_reaction.$SID, etc.). Precedence:
42
+ # 1. $CLAUDE_CODE_SESSION_ID — Claude Code sets a UUID per session and
43
+ # propagates it to every child process (hooks, MCP server, statusline).
44
+ # Shortened to first 8 chars for filename hygiene.
45
+ # 2. $TMUX_PANE — legacy tmux-pane isolation, kept as a fallback.
46
+ # 3. "default" — single-session world.
47
+ if [[ -n "${CLAUDE_CODE_SESSION_ID:-}" ]]; then
48
+ BUDDY_SID="${CLAUDE_CODE_SESSION_ID:0:8}"
49
+ elif [[ -n "${TMUX_PANE:-}" ]]; then
50
+ BUDDY_SID="${TMUX_PANE#%}"
51
+ else
52
+ BUDDY_SID="default"
53
+ fi
54
+
55
+ export CLAUDE_CFG_DIR CLAUDE_SETTINGS_FILE CLAUDE_USER_CONFIG BUDDY_STATE_DIR BUDDY_SID
41
56
 
42
57
  # Windows: Chocolatey installs a PowerShell shim for jq that doesn't work in
43
58
  # Git Bash. If jq is not found on PATH, check the real Chocolatey binary path
package/server/art.ts CHANGED
@@ -116,7 +116,7 @@ export const SPECIES_ART: Record<Species, string[][]> = {
116
116
  "\\ {E}' '{E} /",
117
117
  " \\ } { /",
118
118
  " ≈(° °)≈",
119
- " \x1b[38;2;255;120;0m//|\\\\\x1b[0m"],
119
+ " \x1b[38;2;255;120;0m/\x1b[0m'\x1b[38;2;255;120;0m|\x1b[0m'\x1b[38;2;255;120;0m\\\x1b[0m"],
120
120
  ["} {",
121
121
  "|\\^```^/|",
122
122
  "\\ {E}' '{E} /",
package/server/state.ts CHANGED
@@ -42,9 +42,20 @@ export const STATE_DIR = buddyStateDir();
42
42
  const MANIFEST_FILE = join(STATE_DIR, "menagerie.json");
43
43
  const CONFIG_FILE = join(STATE_DIR, "config.json");
44
44
 
45
- // ─── Session ID (PR #6: tmux session isolation) ─────────────────────────────
45
+ // ─── Session ID (Claude Code session isolation, tmux pane fallback) ─────────
46
+ //
47
+ // Precedence:
48
+ // 1. $CLAUDE_CODE_SESSION_ID — Claude Code sets a UUID per session and
49
+ // propagates it to the MCP server, hooks, and statusline. Shortened
50
+ // to first 8 chars to keep filenames readable.
51
+ // 2. $TMUX_PANE — legacy tmux-pane isolation kept as a fallback.
52
+ // 3. "default" — single-session world.
53
+ //
54
+ // Must match the bash equivalent in scripts/paths.sh.
46
55
 
47
56
  function sessionId(): string {
57
+ const ccSid = process.env.CLAUDE_CODE_SESSION_ID;
58
+ if (ccSid && ccSid.length > 0) return ccSid.slice(0, 8);
48
59
  const pane = process.env.TMUX_PANE;
49
60
  if (!pane) return "default";
50
61
  return pane.replace(/^%/, "");
@@ -416,7 +427,7 @@ export function writeStatusState(
416
427
  eye: companion.bones.eye,
417
428
  shiny: companion.bones.shiny,
418
429
  hat: companion.bones.hat,
419
- reaction: reaction ?? "",
430
+ reaction: "",
420
431
  muted: muted ?? false,
421
432
  achievement: achievement ?? "",
422
433
  frames,
@@ -426,6 +437,7 @@ export function writeStatusState(
426
437
  mood: moodStr,
427
438
  };
428
439
  writeFileSync(join(STATE_DIR, "status.json"), JSON.stringify(state));
440
+ if (reaction) saveReaction(reaction, "mcp");
429
441
  }
430
442
 
431
443
  // ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
@@ -19,9 +19,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
19
19
 
20
20
  STATE="$BUDDY_STATE_DIR/status.json"
21
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}"
22
+ # Per-session ID resolved by paths.sh (CLAUDE_CODE_SESSION_ID > TMUX_PANE > default)
23
+ SID="$BUDDY_SID"
25
24
 
26
25
  [ -f "$STATE" ] || exit 0
27
26
 
@@ -33,7 +32,8 @@ NAME=$(jq -r '.name // ""' "$STATE" 2>/dev/null)
33
32
 
34
33
  RARITY=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
35
34
  SHINY=$(jq -r '.shiny // false' "$STATE" 2>/dev/null)
36
- REACTION=$(jq -r '.reaction // ""' "$STATE" 2>/dev/null)
35
+ REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
36
+ REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
37
37
  ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
38
38
  LEVEL=$(jq -r '.level // 1' "$STATE" 2>/dev/null)
39
39
  MOOD=$(jq -r '.mood // "focused"' "$STATE" 2>/dev/null)
@@ -150,7 +150,6 @@ BUBBLE=""
150
150
  if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
151
151
  BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
152
152
  fi
153
- REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
154
153
  REACTION_TTL=0
155
154
  INNER_W=44
156
155
  MARGIN=8
@@ -221,7 +220,13 @@ for line in "${ART_LINES[@]}"; do
221
220
  done
222
221
  ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$DIM")
223
222
 
224
- ART_W=14
223
+ case "$SPECIES" in
224
+ axolotl|capybara|rabbit|mushroom|chonk) ART_W=10 ;;
225
+ duck|blob|cat|dragon|robot|wyvern) ART_W=9 ;;
226
+ octopus|owl|turtle|snail|ghost|cactus) ART_W=8 ;;
227
+ goose|penguin) ART_W=7 ;;
228
+ *) ART_W=10 ;;
229
+ esac
225
230
  ART_COUNT=${#ALL_LINES[@]}
226
231
 
227
232
  # ─── Speech bubble (left of art, word-wrapped) ──────────────────────────────
@@ -99,9 +99,11 @@ def fmt_stat(label, pct, reset):
99
99
  c = color_for(pct)
100
100
  pct_str = f'{round(pct):3d}%' if pct is not None else ' -%'
101
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
102
+ bar_text = f'{DIM}{label}{NC} {c}{pct_str}{NC} {bar}'
103
+ bar_width = 2 + 1 + 4 + 1 + 10
104
+ timer_text = f' {c}↻{reset}{NC}'
105
+ timer_width = 3 + 1 + len(reset)
106
+ return (bar_text, bar_width), (timer_text, timer_width)
105
107
 
106
108
  try:
107
109
  stats = json.loads(os.environ.get('STATS_JSON', '{}'))
@@ -112,14 +114,13 @@ lines = sys.stdin.read().splitlines()
112
114
  n = len(lines)
113
115
  center = 1
114
116
 
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
- ]
117
+ sess = fmt_stat('5h', stats.get('sess_pct'), stats.get('sess_reset', '--'))
118
+ week = fmt_stat('7d', stats.get('week_pct'), stats.get('week_reset', '--'))
119
+ stat_items = [sess[0], sess[1], week[0], week[1]]
119
120
 
120
121
  for i, line in enumerate(lines):
121
122
  si = i - center
122
- if 0 <= si < 2 and line.startswith(BRAILLE):
123
+ if 0 <= si < 4 and line.startswith(BRAILLE):
123
124
  stat_text, stat_width = stat_items[si]
124
125
  after_braille = line[1:]
125
126
  num_spaces = len(after_braille) - len(after_braille.lstrip(' '))