@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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +9 -9
- package/cli/pick.ts +26 -8
- package/cli/upgrade.ts +44 -11
- package/hooks/buddy-comment.sh +3 -8
- package/hooks/file-type-react.sh +5 -3
- package/hooks/mood-react.sh +5 -3
- package/hooks/name-react.sh +2 -3
- package/hooks/react.sh +2 -6
- package/hooks/suggest.sh +0 -0
- package/package.json +3 -3
- package/scripts/paths.sh +16 -1
- package/server/art.ts +1 -1
- package/server/state.ts +14 -2
- package/statusline/buddy-status.sh +11 -6
- package/statusline/combined-status.sh +9 -8
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-buddy",
|
|
3
3
|
"owner": {
|
|
4
|
-
"name": "
|
|
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.
|
|
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.
|
|
15
|
+
"version": "0.7.0",
|
|
16
16
|
"author": {
|
|
17
|
-
"name": "
|
|
17
|
+
"name": "ramarivera"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/
|
|
20
|
-
"repository": "https://github.com/
|
|
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.
|
|
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": "
|
|
6
|
+
"name": "ramarivera"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"repository": "https://github.com/
|
|
10
|
-
"homepage": "https://github.com/
|
|
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
|
-
[](https://github.com/ramarivera/coding-buddy/releases)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](https://github.com/ramarivera/coding-buddy/stargazers)
|
|
19
19
|
[](https://claude.ai/code)
|
|
20
20
|
[](#requirements)
|
|
21
21
|
[](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/
|
|
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/
|
|
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/
|
|
447
|
-
<img src="https://contrib.rocks/image?repo=
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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" ?
|
|
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
|
-
|
|
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 ||
|
|
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 =
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/hooks/buddy-comment.sh
CHANGED
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
|
|
10
10
|
|
|
11
11
|
STATE_DIR="$BUDDY_STATE_DIR"
|
|
12
|
-
#
|
|
13
|
-
SID="$
|
|
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
|
-
#
|
|
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"
|
package/hooks/file-type-react.sh
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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"
|
package/hooks/mood-react.sh
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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"
|
package/hooks/name-react.sh
CHANGED
|
@@ -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
|
-
#
|
|
12
|
-
SID="$
|
|
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
|
-
#
|
|
8
|
-
SID="$
|
|
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.
|
|
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/
|
|
69
|
+
"url": "git+https://github.com/ramarivera/coding-buddy.git"
|
|
70
70
|
},
|
|
71
|
-
"homepage": "https://github.com/
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
-
#
|
|
23
|
-
SID="$
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 <
|
|
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(' '))
|