@ramarivera/coding-buddy 0.4.0-alpha.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.
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * claude-buddy test-statusline — temporarily install a test status line
4
+ *
5
+ * Run: bun run test-statusline # install test
6
+ * bun run test-statusline restore # restore original
7
+ *
8
+ * The test status line outputs multiple padding strategies side-by-side
9
+ * so you can see in actual Claude Code which one renders correctly.
10
+ *
11
+ * Your original statusLine config is backed up to ~/.claude-buddy/statusline.bak
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, existsSync, copyFileSync, chmodSync, mkdirSync } from "fs";
15
+ import { join, dirname, resolve } from "path";
16
+ import { homedir } from "os";
17
+
18
+ const HOME = homedir();
19
+ const SETTINGS = join(HOME, ".claude", "settings.json");
20
+ const BACKUP = join(HOME, ".claude-buddy", "statusline.bak");
21
+ const TEST_SCRIPT = join(HOME, ".claude-buddy", "test-statusline.sh");
22
+ const SOURCE_SCRIPT = resolve(import.meta.dir, "test-statusline.sh");
23
+
24
+ const RED = "\x1b[31m";
25
+ const GREEN = "\x1b[32m";
26
+ const YELLOW = "\x1b[33m";
27
+ const CYAN = "\x1b[36m";
28
+ const BOLD = "\x1b[1m";
29
+ const NC = "\x1b[0m";
30
+
31
+ function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
32
+ function info(msg: string) { console.log(`${CYAN}→${NC} ${msg}`); }
33
+ function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
34
+ function err(msg: string) { console.log(`${RED}✗${NC} ${msg}`); }
35
+
36
+ const action = process.argv[2] || "install";
37
+
38
+ // ─── Install ────────────────────────────────────────────────────────────────
39
+
40
+ if (action === "install") {
41
+ console.log(`\n${BOLD}claude-buddy test status line installer${NC}\n`);
42
+
43
+ if (!existsSync(SETTINGS)) {
44
+ err("~/.claude/settings.json not found");
45
+ process.exit(1);
46
+ }
47
+ if (!existsSync(SOURCE_SCRIPT)) {
48
+ err(`Source test script missing: ${SOURCE_SCRIPT}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ // Backup
53
+ if (existsSync(BACKUP)) {
54
+ warn(`Backup already exists at ${BACKUP}`);
55
+ warn("Run 'bun run test-statusline restore' first to revert");
56
+ process.exit(1);
57
+ }
58
+
59
+ mkdirSync(dirname(BACKUP), { recursive: true });
60
+ copyFileSync(SETTINGS, BACKUP);
61
+ ok(`Backed up settings to ${BACKUP}`);
62
+
63
+ // Copy the test script (no inline TS interpolation issues)
64
+ copyFileSync(SOURCE_SCRIPT, TEST_SCRIPT);
65
+ chmodSync(TEST_SCRIPT, 0o755);
66
+ ok(`Test script copied to ${TEST_SCRIPT}`);
67
+
68
+ // Update settings to use test script
69
+ const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
70
+ settings.statusLine = {
71
+ type: "command",
72
+ command: TEST_SCRIPT,
73
+ padding: 1,
74
+ };
75
+ writeFileSync(SETTINGS, JSON.stringify(settings, null, 2));
76
+ ok("settings.json updated");
77
+
78
+ console.log(`
79
+ ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
80
+ ${CYAN} NEXT STEPS:${NC}
81
+
82
+ ${BOLD}1.${NC} Restart Claude Code completely
83
+ ${BOLD}2.${NC} Take a screenshot of the status line area
84
+ ${BOLD}3.${NC} Note which lines are visible and which markers align
85
+ ${BOLD}4.${NC} Restore your original config:
86
+
87
+ ${GREEN}bun run test-statusline restore${NC}
88
+
89
+ ${BOLD}What to look for:${NC}
90
+ - Are all 12 lines visible? (or only some?)
91
+ - Do SPACE_30_END, BRAILLE_30_END, NBSP_30_END align?
92
+ - Does the mushroom art appear in gold color?
93
+ ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
94
+ `);
95
+ process.exit(0);
96
+ }
97
+
98
+ // ─── Restore ────────────────────────────────────────────────────────────────
99
+
100
+ if (action === "restore") {
101
+ console.log(`\n${BOLD}claude-buddy test status line restore${NC}\n`);
102
+
103
+ if (!existsSync(BACKUP)) {
104
+ err(`No backup found at ${BACKUP}`);
105
+ err("Nothing to restore");
106
+ process.exit(1);
107
+ }
108
+
109
+ copyFileSync(BACKUP, SETTINGS);
110
+ ok("Original settings.json restored");
111
+
112
+ const { unlinkSync } = require("fs");
113
+ try { unlinkSync(BACKUP); ok("Backup file removed"); } catch { /* noop */ }
114
+ try { unlinkSync(TEST_SCRIPT); ok("Test script removed"); } catch { /* noop */ }
115
+
116
+ console.log(`\n${GREEN}Done.${NC} Restart Claude Code to apply.\n`);
117
+ process.exit(0);
118
+ }
119
+
120
+ err(`Unknown action: ${action}`);
121
+ console.log(`Usage: bun run test-statusline [install|restore]`);
122
+ process.exit(1);
@@ -0,0 +1,110 @@
1
+ /**
2
+ * claude-buddy uninstall — remove all integrations
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+
9
+ const GREEN = "\x1b[32m";
10
+ const YELLOW = "\x1b[33m";
11
+ const NC = "\x1b[0m";
12
+
13
+ function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
14
+ function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
15
+
16
+ const CLAUDE_DIR = join(homedir(), ".claude");
17
+ const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
18
+ const SKILL_DIR = join(CLAUDE_DIR, "skills", "buddy");
19
+ const STATE_DIR = join(homedir(), ".claude-buddy");
20
+
21
+ console.log("\nclaude-buddy uninstall\n");
22
+
23
+ // Stop all popup reopen loops and close any running popup
24
+ try {
25
+ if (existsSync(STATE_DIR)) {
26
+ // Kill all session popup loops (popup-reopen-pid.*)
27
+ for (const f of readdirSync(STATE_DIR).filter(f => f.startsWith("popup-reopen-pid."))) {
28
+ const pidPath = join(STATE_DIR, f);
29
+ const pid = parseInt(readFileSync(pidPath, "utf8").trim(), 10);
30
+ if (pid > 0) { try { process.kill(pid); } catch { /* already dead */ } }
31
+ rmSync(pidPath, { force: true });
32
+ }
33
+ // Clean up all session-scoped files
34
+ const patterns = ["popup-stop.", "popup-resize.", "popup-env.", "popup-scroll.",
35
+ "reaction.", ".last_reaction.", ".last_comment."];
36
+ for (const f of readdirSync(STATE_DIR)) {
37
+ if (patterns.some(p => f.startsWith(p))) {
38
+ rmSync(join(STATE_DIR, f), { force: true });
39
+ }
40
+ }
41
+ }
42
+ // Close any open popup
43
+ if (process.env.TMUX) {
44
+ const { execSync } = await import("child_process");
45
+ execSync("tmux display-popup -C 2>/dev/null", { stdio: "ignore" });
46
+ }
47
+ ok("Popup stopped");
48
+ } catch { /* not in tmux or no popup */ }
49
+
50
+ // Remove MCP server from ~/.claude.json
51
+ try {
52
+ const claudeJsonPath = join(homedir(), ".claude.json");
53
+ const claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
54
+ if (claudeJson.mcpServers?.["claude-buddy"]) {
55
+ delete claudeJson.mcpServers["claude-buddy"];
56
+ if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
57
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
58
+ ok("MCP server removed from ~/.claude.json");
59
+ }
60
+ } catch {
61
+ warn("Could not update ~/.claude.json");
62
+ }
63
+
64
+ // Remove hooks and statusline from settings.json
65
+ try {
66
+ const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
67
+ let changed = false;
68
+
69
+ if (settings.statusLine?.command?.includes("buddy")) {
70
+ delete settings.statusLine;
71
+ ok("Status line removed");
72
+ changed = true;
73
+ }
74
+
75
+ for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd"] as const) {
76
+ if (settings.hooks?.[hookType]) {
77
+ const before = settings.hooks[hookType].length;
78
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
79
+ (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
80
+ );
81
+ if (settings.hooks[hookType].length < before) {
82
+ ok(`${hookType} hooks removed`);
83
+ changed = true;
84
+ }
85
+ if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
86
+ }
87
+ }
88
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
89
+
90
+ if (changed) {
91
+ writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
92
+ }
93
+ } catch {
94
+ warn("Could not update settings.json");
95
+ }
96
+
97
+ // Remove skill
98
+ if (existsSync(SKILL_DIR)) {
99
+ rmSync(SKILL_DIR, { recursive: true });
100
+ ok("Skill removed");
101
+ } else {
102
+ warn("Skill not found (already removed)");
103
+ }
104
+
105
+ // Keep state dir (companion data) — user might want it back
106
+ if (existsSync(STATE_DIR)) {
107
+ warn(`Companion data kept at ${STATE_DIR} — delete manually if not needed`);
108
+ }
109
+
110
+ console.log(`\n${GREEN}Done.${NC} Restart Claude Code to apply changes.\n`);
package/cli/verify.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * claude-buddy verify — show what buddy a user ID produces
3
+ */
4
+
5
+ import { generateBones, renderBuddy, STAT_NAMES } from "../server/engine.ts";
6
+ import { resolveUserId } from "../server/state.ts";
7
+
8
+ const userId = process.argv[3] || resolveUserId();
9
+
10
+ console.log(`\nUser ID: ${userId.slice(0, 16)}...`);
11
+ console.log("");
12
+
13
+ const bones = generateBones(userId);
14
+ console.log(renderBuddy(bones));
15
+
16
+ const statLine = STAT_NAMES.map((n) => `${n}:${bones.stats[n]}`).join(" | ");
17
+ console.log(`\n ${statLine}`);
18
+ console.log(` peak=${bones.peak} dump=${bones.dump}`);
19
+ console.log("");
@@ -0,0 +1,65 @@
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
+ STATE_DIR="$HOME/.claude-buddy"
9
+ # Session ID: sanitized tmux pane number, or "default" outside tmux
10
+ SID="${TMUX_PANE#%}"
11
+ SID="${SID:-default}"
12
+ STATUS_FILE="$STATE_DIR/status.json"
13
+ COOLDOWN_FILE="$STATE_DIR/.last_comment.$SID"
14
+ CONFIG_FILE="$STATE_DIR/config.json"
15
+ EVENTS_FILE="$STATE_DIR/events.json"
16
+
17
+ [ -f "$STATUS_FILE" ] || exit 0
18
+
19
+ # Read cooldown from config (default 30s, 0 = disabled)
20
+ COOLDOWN=30
21
+ if [ -f "$CONFIG_FILE" ]; then
22
+ _cd=$(jq -r '.commentCooldown // 30' "$CONFIG_FILE" 2>/dev/null || echo 30)
23
+ # Accept any non-negative integer (including 0 to disable cooldown)
24
+ [[ "$_cd" =~ ^[0-9]+$ ]] && COOLDOWN=$_cd
25
+ fi
26
+
27
+ INPUT=$(cat)
28
+
29
+ # Extract last_assistant_message from hook input
30
+ MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)
31
+ [ -z "$MSG" ] && exit 0
32
+
33
+ # Extract <!-- buddy: ... --> comment (portable, no grep -P)
34
+ COMMENT=$(echo "$MSG" | sed -n 's/.*<!-- *buddy: *\(.*[^ ]\) *-->.*/\1/p' | tail -1)
35
+ [ -z "$COMMENT" ] && exit 0
36
+
37
+ # Cooldown: configurable (default 30s)
38
+ if [ -f "$COOLDOWN_FILE" ]; then
39
+ LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null)
40
+ NOW=$(date +%s)
41
+ [ $(( NOW - ${LAST:-0} )) -lt "$COOLDOWN" ] && exit 0
42
+ fi
43
+
44
+ mkdir -p "$STATE_DIR"
45
+ date +%s > "$COOLDOWN_FILE"
46
+
47
+ # Update status.json with the reaction
48
+ TMP=$(mktemp)
49
+ jq --arg r "$COMMENT" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
50
+
51
+ # Also write reaction file (use jq for safe JSON encoding)
52
+ jq -n --arg r "$COMMENT" --arg ts "$(date +%s)000" \
53
+ '{reaction: $r, timestamp: ($ts | tonumber), reason: "turn"}' \
54
+ > "$STATE_DIR/reaction.$SID.json"
55
+
56
+ # Increment achievement event counters
57
+ if command -v jq >/dev/null 2>&1; then
58
+ if [ ! -f "$EVENTS_FILE" ]; then
59
+ echo '{}' > "$EVENTS_FILE"
60
+ fi
61
+ TMP=$(mktemp)
62
+ jq '.turns = (.turns // 0 + 1)' "$EVENTS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$EVENTS_FILE"
63
+ fi
64
+
65
+ exit 0
@@ -0,0 +1,35 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "./hooks/react.sh"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "./hooks/buddy-comment.sh"
20
+ }
21
+ ]
22
+ }
23
+ ],
24
+ "UserPromptSubmit": [
25
+ {
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "./hooks/name-react.sh"
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ # claude-buddy UserPromptSubmit hook
3
+ # Detects the buddy's name in the user's message → status line reaction.
4
+ # No cooldown — name mentions are intentional.
5
+
6
+ STATE_DIR="$HOME/.claude-buddy"
7
+ STATUS_FILE="$STATE_DIR/status.json"
8
+ # Session ID: sanitized tmux pane number, or "default" outside tmux
9
+ SID="${TMUX_PANE#%}"
10
+ SID="${SID:-default}"
11
+
12
+ [ -f "$STATUS_FILE" ] || exit 0
13
+
14
+ INPUT=$(cat)
15
+
16
+ # Claude Code sends the prompt in different fields depending on version
17
+ PROMPT=$(echo "$INPUT" | jq -r '
18
+ .prompt // .message // .user_message //
19
+ (.messages[-1].content // "") | if type=="array" then .[0].text else . end
20
+ ' 2>/dev/null)
21
+ [ -z "$PROMPT" ] && exit 0
22
+
23
+ NAME=$(jq -r '.name // ""' "$STATUS_FILE" 2>/dev/null)
24
+ [ -z "$NAME" ] && exit 0
25
+
26
+ # Case-insensitive whole-word match
27
+ echo "$PROMPT" | grep -qiE "(^|[^a-zA-Z])${NAME}([^a-zA-Z]|$)" 2>/dev/null || exit 0
28
+
29
+ SPECIES=$(jq -r '.species // "blob"' "$STATUS_FILE" 2>/dev/null)
30
+ MUTED=$(jq -r '.muted // false' "$STATUS_FILE" 2>/dev/null)
31
+ [ "$MUTED" = "true" ] && exit 0
32
+
33
+ # Species-specific name-call reactions
34
+ case "$SPECIES" in
35
+ dragon)
36
+ REACTIONS=(
37
+ "*one eye opens slowly*"
38
+ "...you called?"
39
+ "*smoke curls from nostril* yes."
40
+ "*regards you from above*"
41
+ ) ;;
42
+ owl)
43
+ REACTIONS=(
44
+ "*swivels head 180°*"
45
+ "*blinks once, deliberately*"
46
+ "hm."
47
+ "*adjusts perch*"
48
+ ) ;;
49
+ cat)
50
+ REACTIONS=(
51
+ "*ear flicks*"
52
+ "...what."
53
+ "*ignores you, but heard*"
54
+ "*opens one eye*"
55
+ ) ;;
56
+ duck)
57
+ REACTIONS=(
58
+ "*quack*"
59
+ "*looks up mid-waddle*"
60
+ "*attentive duck noises*"
61
+ ) ;;
62
+ ghost)
63
+ REACTIONS=(
64
+ "*materialises*"
65
+ "...boo?"
66
+ "*phases closer*"
67
+ ) ;;
68
+ robot)
69
+ REACTIONS=(
70
+ "NAME DETECTED."
71
+ "*whirrs attentively*"
72
+ "STANDING BY."
73
+ ) ;;
74
+ capybara)
75
+ REACTIONS=(
76
+ "*barely moves*"
77
+ "*blinks slowly*"
78
+ "...yes, friend."
79
+ ) ;;
80
+ axolotl)
81
+ REACTIONS=(
82
+ "*gill flutter*"
83
+ "*smiles gently*"
84
+ "oh! hello."
85
+ ) ;;
86
+ blob)
87
+ REACTIONS=(
88
+ "*jiggles*"
89
+ "*oozes toward you*"
90
+ "*wobbles excitedly*"
91
+ ) ;;
92
+ turtle)
93
+ REACTIONS=(
94
+ "*slowly extends neck*"
95
+ "...you called?"
96
+ "*ancient eyes open*"
97
+ "*shell creaks thoughtfully*"
98
+ "*blinks once, patiently*"
99
+ ) ;;
100
+ goose)
101
+ REACTIONS=(
102
+ "HONK."
103
+ "*necks aggressively*"
104
+ "*wing flap*"
105
+ "*honks in recognition*"
106
+ ) ;;
107
+ octopus)
108
+ REACTIONS=(
109
+ "*eight eyes open*"
110
+ "*curls an arm toward you*"
111
+ "*changes color curiously*"
112
+ "...yes, friend?"
113
+ ) ;;
114
+ penguin)
115
+ REACTIONS=(
116
+ "*adjusts tie*"
117
+ "*dignified waddle*"
118
+ "*bows slightly*"
119
+ "...yes, quite?"
120
+ ) ;;
121
+ snail)
122
+ REACTIONS=(
123
+ "*slow head extension*"
124
+ "...mmm?"
125
+ "*trails slowly toward you*"
126
+ "*antenna twitches*"
127
+ ) ;;
128
+ cactus)
129
+ REACTIONS=(
130
+ "*stands silent*"
131
+ "...hm."
132
+ "*spine twitches*"
133
+ "*slowly rotates*"
134
+ ) ;;
135
+ rabbit)
136
+ REACTIONS=(
137
+ "*ears perk up*"
138
+ "*nose twitches*"
139
+ "yes?"
140
+ "*hops closer*"
141
+ ) ;;
142
+ mushroom)
143
+ REACTIONS=(
144
+ "*releases a tiny spore*"
145
+ "*cap tilts*"
146
+ "*stands mysterious*"
147
+ "...yes?"
148
+ ) ;;
149
+ chonk)
150
+ REACTIONS=(
151
+ "*barely opens one eye*"
152
+ "...mrrp?"
153
+ "*yawns heavily*"
154
+ "*rolls over toward you*"
155
+ ) ;;
156
+ *)
157
+ REACTIONS=(
158
+ "*perks up*"
159
+ "...yes?"
160
+ "*looks your way*"
161
+ ) ;;
162
+ esac
163
+
164
+ N=${#REACTIONS[@]}
165
+ REACTION="${REACTIONS[$((RANDOM % N))]}"
166
+
167
+ mkdir -p "$STATE_DIR"
168
+
169
+ TMP=$(mktemp)
170
+ jq --arg r "$REACTION" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
171
+
172
+ jq -n --arg r "$REACTION" --arg ts "$(date +%s)000" \
173
+ '{reaction: $r, timestamp: ($ts | tonumber), reason: "name"}' \
174
+ > "$STATE_DIR/reaction.$SID.json"
175
+
176
+ exit 0