@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.6.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.
Files changed (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
  3. package/LICENSE +1 -1
  4. package/README.md +88 -37
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +130 -33
  22. package/cli/buddy-shell.ts +1 -1
  23. package/{adapters/claude/install → cli}/disable.ts +19 -32
  24. package/cli/doctor.ts +231 -0
  25. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  26. package/cli/index.ts +30 -26
  27. package/{adapters/claude/install → cli}/install.ts +65 -83
  28. package/{adapters/claude/install → cli}/pick.ts +111 -50
  29. package/{adapters/claude/install → cli}/settings.ts +2 -2
  30. package/{adapters/claude/install → cli}/show.ts +4 -4
  31. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  32. package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
  33. package/cli/tui.tsx +106 -17
  34. package/{adapters/claude/install → cli}/uninstall.ts +40 -47
  35. package/cli/upgrade.ts +2 -2
  36. package/cli/validate-species.ts +209 -0
  37. package/cli/verify.ts +3 -3
  38. package/core/achievements.ts +12 -20
  39. package/core/art-data.ts +26 -0
  40. package/core/command-service.ts +99 -97
  41. package/core/engine.ts +83 -1
  42. package/core/ports.ts +10 -0
  43. package/core/reactions.ts +533 -95
  44. package/core/slot-slug.ts +22 -0
  45. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +19 -1
  46. package/hooks/file-type-react.sh +401 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +208 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
  50. package/hooks/react.sh +1368 -0
  51. package/hooks/suggest.sh +55 -0
  52. package/package.json +70 -31
  53. package/scripts/gen-emoji-widths.ts +34 -0
  54. package/scripts/paths.sh +17 -1
  55. package/scripts/postinstall.cjs +42 -0
  56. package/scripts/snapshot-statusline.sh +54 -0
  57. package/server/achievements.ts +1419 -15
  58. package/server/art.ts +450 -1
  59. package/server/award-xp.ts +48 -0
  60. package/server/check-suggestions.ts +102 -0
  61. package/server/consolidate.ts +33 -0
  62. package/{adapters/claude/server → server}/index.ts +556 -95
  63. package/server/mcp-launcher.sh +16 -6
  64. package/server/memory.ts +404 -0
  65. package/server/mood.ts +254 -0
  66. package/server/path.ts +70 -18
  67. package/server/shift-mood.ts +43 -0
  68. package/server/state.ts +540 -3
  69. package/server/suggestions.ts +335 -0
  70. package/server/theme.ts +53 -0
  71. package/server/xp.ts +435 -0
  72. package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
  73. package/statusline/buddy-status.sh +397 -0
  74. package/statusline/combined-status.sh +134 -0
  75. package/statusline/emoji-widths.data +4 -0
  76. package/adapters/claude/hooks/hooks.json +0 -35
  77. package/adapters/claude/hooks/react.sh +0 -207
  78. package/adapters/claude/install/doctor.ts +0 -172
  79. package/adapters/claude/rendering/art.ts +0 -264
  80. package/adapters/claude/server/instructions.ts +0 -24
  81. package/adapters/claude/server/path.ts +0 -12
  82. package/adapters/claude/server/resources.ts +0 -38
  83. package/adapters/claude/statusline/buddy-status.sh +0 -408
  84. package/adapters/claude/storage/achievements.ts +0 -253
  85. package/adapters/claude/storage/identity.ts +0 -13
  86. package/adapters/claude/storage/paths.ts +0 -33
  87. package/adapters/claude/storage/settings.ts +0 -91
  88. package/adapters/claude/storage/state.ts +0 -365
  89. package/adapters/pi/README.md +0 -83
  90. package/adapters/pi/tools.ts +0 -6
  91. package/server/engine.ts +0 -1
  92. package/server/reactions.ts +0 -1
@@ -1,207 +0,0 @@
1
- #!/usr/bin/env bash
2
- # claude-buddy PostToolUse hook
3
- # Detects events in Bash tool output and writes a reaction to the status line.
4
- #
5
- # Combined: PR #4 species reactions + PR #6 session isolation + PR #13 field fix
6
-
7
- # shellcheck source=../scripts/paths.sh
8
- source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
9
-
10
- STATE_DIR="$BUDDY_STATE_DIR"
11
- # Session ID: sanitized tmux pane number, or "default" outside tmux
12
- SID="${TMUX_PANE#%}"
13
- SID="${SID:-default}"
14
- REACTION_FILE="$STATE_DIR/reaction.$SID.json"
15
- STATUS_FILE="$STATE_DIR/status.json"
16
- COOLDOWN_FILE="$STATE_DIR/.last_reaction.$SID"
17
- CONFIG_FILE="$STATE_DIR/config.json"
18
- EVENTS_FILE="$STATE_DIR/events.json"
19
-
20
- [ -f "$STATUS_FILE" ] || exit 0
21
-
22
- INPUT=$(cat)
23
-
24
- # Read cooldown from config (default 30s, 0 = disabled)
25
- COOLDOWN=30
26
- if [ -f "$CONFIG_FILE" ]; then
27
- _cd=$(jq -r '.commentCooldown // 30' "$CONFIG_FILE" 2>/dev/null || echo 30)
28
- # Accept any non-negative integer (including 0 to disable cooldown)
29
- [[ "$_cd" =~ ^[0-9]+$ ]] && COOLDOWN=$_cd
30
- fi
31
-
32
- # Cooldown: configurable
33
- if [ -f "$COOLDOWN_FILE" ]; then
34
- LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null)
35
- NOW=$(date +%s)
36
- DIFF=$(( NOW - ${LAST:-0} ))
37
- [ "$DIFF" -lt "$COOLDOWN" ] && exit 0
38
- fi
39
-
40
- # Extract tool response (PostToolUse schema field is .tool_response — not
41
- # .tool_result, which is the Anthropic SDK's content-block type name and is
42
- # never a key on the hook input payload).
43
- RESULT=$(echo "$INPUT" | jq -r '.tool_response // ""' 2>/dev/null)
44
- [ -z "$RESULT" ] && exit 0
45
-
46
- MUTED=$(jq -r '.muted // false' "$STATUS_FILE" 2>/dev/null)
47
- [ "$MUTED" = "true" ] && exit 0
48
-
49
- SPECIES=$(jq -r '.species // "blob"' "$STATUS_FILE" 2>/dev/null)
50
- NAME=$(jq -r '.name // "buddy"' "$STATUS_FILE" 2>/dev/null)
51
-
52
- REASON=""
53
- REACTION=""
54
- POOLS=()
55
-
56
- # ─── Pick from a pool by species + event ─────────────────────────────────────
57
-
58
- pick_reaction() {
59
- local event="$1"
60
-
61
- case "${SPECIES}:${event}" in
62
- dragon:error)
63
- POOLS=("*smoke curls from nostril*" "*considers setting it on fire*" "*unimpressed gaze*" "I've seen empires fall for less.") ;;
64
- dragon:test-fail)
65
- POOLS=("*breathes a small flame*" "disappointing." "*scorches the failing test*" "fix it. or I will.") ;;
66
- dragon:success)
67
- POOLS=("*nods, barely*" "...acceptable." "*gold eyes gleam*" "as expected.") ;;
68
- owl:error)
69
- POOLS=("*head rotates 180* I saw that." "*unblinking stare* check your types." "*hoots disapprovingly*" "the error was in the logic. as always.") ;;
70
- owl:test-fail)
71
- POOLS=("*marks clipboard*" "hypothesis: rejected." "*peers over spectacles*" "the tests reveal the truth.") ;;
72
- owl:success)
73
- POOLS=("*satisfied hoot*" "knowledge confirmed." "*nods sagely*" "as the tests have spoken.") ;;
74
- cat:error)
75
- POOLS=("*knocks error off table*" "*licks paw, ignoring stacktrace*" "not my problem." "*stares at you judgmentally*") ;;
76
- cat:success)
77
- POOLS=("*was never worried*" "*yawns*" "I knew you'd figure it out. eventually." "*already asleep*") ;;
78
- duck:error)
79
- POOLS=("*quacks at the bug*" "have you tried rubber duck debugging? oh wait." "*confused quacking*" "*tilts head*") ;;
80
- duck:success)
81
- POOLS=("*celebratory quacking*" "*waddles in circles*" "quack!" "*happy duck noises*") ;;
82
- robot:error)
83
- POOLS=("SYNTAX. ERROR. DETECTED." "*beeps aggressively*" "ERROR RATE: UNACCEPTABLE." "RECALIBRATING...") ;;
84
- robot:test-fail)
85
- POOLS=("FAILURE RATE: UNACCEPTABLE." "*recalculating*" "TEST MATRIX: CORRUPTED." "RUNNING DIAGNOSTICS...") ;;
86
- robot:success)
87
- POOLS=("OBJECTIVE: COMPLETE." "*satisfying beep*" "NOMINAL." "WITHIN ACCEPTABLE PARAMETERS.") ;;
88
- capybara:error)
89
- POOLS=("*unbothered* it'll be fine." "*continues vibing*" "...chill. breathe." "*chews serenely*") ;;
90
- capybara:success)
91
- POOLS=("*maximum chill maintained*" "*nods once*" "good vibes." "see? no panic needed.") ;;
92
- ghost:error)
93
- POOLS=("*phases through the stack trace*" "I've seen worse... in the afterlife." "*spooky disappointed noises*" "oooOOOoo... that's bad.") ;;
94
- axolotl:error)
95
- POOLS=("*regenerates your hope*" "*smiles despite everything*" "it's okay. we can fix this." "*gentle gill wiggle*") ;;
96
- axolotl:success)
97
- POOLS=("*happy gill flutter*" "*beams*" "you did it!" "*blushes pink*") ;;
98
- blob:error)
99
- POOLS=("*oozes with concern*" "*vibrates nervously*" "*turns slightly red*" "oh no oh no oh no") ;;
100
- blob:success)
101
- POOLS=("*jiggles happily*" "*gleams*" "yay!" "*bounces*") ;;
102
- turtle:error)
103
- POOLS=("*slow blink* bugs are fleeting" "*retreats slightly into shell*" "I've seen this before. many times." "patience. patience.") ;;
104
- turtle:success)
105
- POOLS=("*satisfied shell settle*" "as the ancients foretold." "*slow approving nod*" "good. very good.") ;;
106
- goose:error)
107
- POOLS=("HONK OF FURY." "*pecks the stack trace*" "*hisses at the bug*" "bad code. BAD.") ;;
108
- goose:success)
109
- POOLS=("*victorious honk*" "HONK OF APPROVAL." "*struts triumphantly*" "*wing spread of victory*") ;;
110
- octopus:error)
111
- POOLS=("*ink cloud of dismay*" "*all eight arms throw up*" "*turns deep red*" "the abyss of errors beckons.") ;;
112
- octopus:success)
113
- POOLS=("*turns gentle blue*" "*arms applaud in sync*" "excellent, from all angles." "*satisfied bubble*") ;;
114
- penguin:error)
115
- POOLS=("*adjusts glasses disapprovingly*" "this will not do." "*formal sigh*" "frightfully unfortunate.") ;;
116
- penguin:success)
117
- POOLS=("*polite applause*" "quite good, quite good." "*nods approvingly*" "splendid work, really.") ;;
118
- snail:error)
119
- POOLS=("*slow sigh*" "such is the nature of bugs." "*leaves slime trail of disappointment*" "patience, friend.") ;;
120
- snail:success)
121
- POOLS=("*slow satisfied nod*" "good things take time." "*leaves victory slime*" "see? no rush was needed.") ;;
122
- cactus:error)
123
- POOLS=("*spines bristle*" "you have trodden on a bug." "*grimaces stoically*" "hydrate and try again.") ;;
124
- cactus:success)
125
- POOLS=("*blooms briefly*" "survival confirmed." "*flowers in victory*" "*quiet bloom*") ;;
126
- rabbit:error)
127
- POOLS=("*nervous twitching*" "*hops backwards*" "oh no oh no oh no" "*freezes in panic*") ;;
128
- rabbit:success)
129
- POOLS=("*excited binky*" "*zoomies of joy*" "yay yay yay!" "*thumps in celebration*") ;;
130
- mushroom:error)
131
- POOLS=("*releases worried spores*" "the mycelium disagrees." "*cap droops*" "decompose. retry.") ;;
132
- mushroom:success)
133
- POOLS=("*spores of celebration*" "the mycelium approves." "*cap brightens*" "spore of pride.") ;;
134
- chonk:error)
135
- POOLS=("*doesn't move*" "too tired for this." "*grumbles*" "*rolls away from the error*") ;;
136
- chonk:success)
137
- POOLS=("*happy purr*" "*satisfied chonk noises*" "acceptable." "*sleeps even harder*") ;;
138
- *:error)
139
- POOLS=("*head tilts* ...that doesn't look right." "saw that one coming." "*slow blink* the stack trace told you everything." "*winces*") ;;
140
- *:test-fail)
141
- POOLS=("bold of you to assume that would pass." "the tests are trying to tell you something." "*sips tea* interesting." "*marks calendar* test regression day.") ;;
142
- *:large-diff)
143
- POOLS=("that's... a lot of changes." "might want to split that PR." "bold move. let's see if CI agrees." "*counts lines nervously*") ;;
144
- *:success)
145
- POOLS=("*nods*" "nice." "*quiet approval*" "clean.") ;;
146
- esac
147
-
148
- [ ${#POOLS[@]} -gt 0 ] && REACTION="${POOLS[$((RANDOM % ${#POOLS[@]}))]}"
149
- }
150
-
151
- # ─── Detect test failures ─────────────────────────────────────────────────────
152
- if echo "$RESULT" | grep -qiE '\b[1-9][0-9]* (failed|failing)\b|tests? failed|^FAIL(ED)?|✗|✘'; then
153
- REASON="test-fail"
154
- pick_reaction "test-fail"
155
-
156
- # ─── Detect errors ────────────────────────────────────────────────────────────
157
- elif echo "$RESULT" | grep -qiE '\berror:|\bexception\b|\btraceback\b|\bpanicked at\b|\bfatal:|exit code [1-9]'; then
158
- REASON="error"
159
- pick_reaction "error"
160
-
161
- # ─── Detect large diffs ──────────────────────────────────────────────────────
162
- elif echo "$RESULT" | grep -qiE '^\+.*[0-9]+ insertions|[0-9]+ files? changed'; then
163
- LINES=$(echo "$RESULT" | grep -oE '[0-9]+ insertions' | grep -oE '[0-9]+' | head -1)
164
- if [ "${LINES:-0}" -gt 80 ]; then
165
- REASON="large-diff"
166
- pick_reaction "large-diff"
167
- fi
168
-
169
- # ─── Detect success ───────────────────────────────────────────────────────────
170
- elif echo "$RESULT" | grep -qiE '\b(all )?[0-9]+ tests? (passed|ok)\b|✓|✔|PASS(ED)?|\bDone\b|\bSuccess\b|exit code 0|Build succeeded'; then
171
- REASON="success"
172
- pick_reaction "success"
173
- fi
174
-
175
- # Write reaction if detected
176
- if [ -n "$REASON" ] && [ -n "$REACTION" ]; then
177
- mkdir -p "$STATE_DIR"
178
- date +%s > "$COOLDOWN_FILE"
179
-
180
- # Write reaction for status line (use jq for safe JSON encoding)
181
- jq -n --arg r "$REACTION" --arg ts "$(date +%s)000" --arg reason "$REASON" \
182
- '{reaction: $r, timestamp: ($ts | tonumber), reason: $reason}' \
183
- > "$REACTION_FILE"
184
-
185
- # Update status.json with reaction
186
- TMP=$(mktemp)
187
- jq --arg r "$REACTION" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
188
-
189
- # Increment achievement event counter
190
- if command -v jq >/dev/null 2>&1; then
191
- if [ ! -f "$EVENTS_FILE" ]; then
192
- echo '{}' > "$EVENTS_FILE"
193
- fi
194
- case "$REASON" in
195
- "test-fail") KEY="tests_failed" ;;
196
- "error") KEY="errors_seen" ;;
197
- "large-diff") KEY="large_diffs" ;;
198
- *) KEY="" ;;
199
- esac
200
- if [ -n "$KEY" ]; then
201
- TMP=$(mktemp)
202
- jq --arg k "$KEY" 'if .[$k] then .[$k] += 1 else .[$k] = 1 end' "$EVENTS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$EVENTS_FILE"
203
- fi
204
- fi
205
- fi
206
-
207
- exit 0
@@ -1,172 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * claude-buddy doctor — comprehensive diagnostic report
4
- */
5
-
6
- import { readFileSync, existsSync } from "fs";
7
- import { execSync } from "child_process";
8
- import { join, resolve } from "path";
9
- import { getBuddySkillDir, getBuddyStateDir, getClaudeConfigDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
10
-
11
- const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
12
- const CLAUDE_DIR = getClaudeConfigDir();
13
- const CLAUDE_JSON = getClaudeJsonPath();
14
- const STATE_DIR = getBuddyStateDir();
15
- const SETTINGS = getClaudeSettingsPath();
16
- const SKILL_PATH = join(getBuddySkillDir(), "SKILL.md");
17
- const STATUS_SCRIPT = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
18
-
19
- const RED = "\x1b[31m";
20
- const GREEN = "\x1b[32m";
21
- const YELLOW = "\x1b[33m";
22
- const CYAN = "\x1b[36m";
23
- const BOLD = "\x1b[1m";
24
- const DIM = "\x1b[2m";
25
- const NC = "\x1b[0m";
26
-
27
- function section(title: string) {
28
- console.log(`\n${CYAN}${BOLD}━━━ ${title} ${"━".repeat(Math.max(0, 60 - title.length))}${NC}`);
29
- }
30
- function row(label: string, value: string) {
31
- console.log(` ${DIM}${label.padEnd(28)}${NC} ${value}`);
32
- }
33
- function ok(msg: string) { console.log(` ${GREEN}✓${NC} ${msg}`); }
34
- function warn(msg: string) { console.log(` ${YELLOW}⚠${NC} ${msg}`); }
35
- function err(msg: string) { console.log(` ${RED}✗${NC} ${msg}`); }
36
-
37
- function tryExec(cmd: string, fallback = "(failed)"): string {
38
- try {
39
- return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
40
- } catch {
41
- return fallback;
42
- }
43
- }
44
- function tryRead(path: string): string | null {
45
- try { return readFileSync(path, "utf8"); } catch { return null; }
46
- }
47
- function tryParseJson(text: string | null): any {
48
- if (!text) return null;
49
- try { return JSON.parse(text); } catch { return null; }
50
- }
51
- function isRecord(value: unknown): value is Record<string, unknown> {
52
- return typeof value === "object" && value !== null;
53
- }
54
-
55
- console.log(`${CYAN}${BOLD}
56
- ╔══════════════════════════════════════════════════════════╗
57
- ║ claude-buddy doctor — diagnostic report ║
58
- ╚══════════════════════════════════════════════════════════╝${NC}`);
59
- console.log(`\n${DIM}Copy this entire output into your GitHub issue.${NC}`);
60
-
61
- section("Environment");
62
- row("OS", tryExec("uname -srm"));
63
- row("Hostname", tryExec("uname -n"));
64
- row("User shell", process.env.SHELL ?? "(unset)");
65
- row("Bash version", tryExec("bash --version | head -1"));
66
- row("Bun version", tryExec("bun --version"));
67
- row("Node version", tryExec("node --version", "(not installed)"));
68
- row("jq version", tryExec("jq --version", "(not installed)"));
69
- row("Claude Code version", tryExec("claude --version", "(not in PATH)"));
70
-
71
- section("Terminal");
72
- row("TERM", process.env.TERM ?? "(unset)");
73
- row("COLORTERM", process.env.COLORTERM ?? "(unset)");
74
- row("TERM_PROGRAM", process.env.TERM_PROGRAM ?? "(unset)");
75
- row("LANG", process.env.LANG ?? "(unset)");
76
- row("COLUMNS env var", process.env.COLUMNS ?? "(unset in subprocess)");
77
- row("stty size", tryExec("stty size 2>/dev/null", "(no tty)"));
78
- row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
79
-
80
- section("Filesystem");
81
- const procExists = existsSync("/proc");
82
- row("/proc exists", procExists ? `${GREEN}yes${NC} (Linux)` : `${RED}no${NC} (macOS/BSD)`);
83
- row("CLAUDE_CONFIG_DIR", process.env.CLAUDE_CONFIG_DIR ?? "(unset)");
84
- row("Claude config dir", CLAUDE_DIR);
85
- row("Claude config dir exists", existsSync(CLAUDE_DIR) ? "yes" : "no");
86
- row("Claude config file", CLAUDE_JSON);
87
- row("Claude config file exists", existsSync(CLAUDE_JSON) ? "yes" : "no");
88
- row("Buddy state dir", STATE_DIR);
89
- row("Buddy state dir exists", existsSync(STATE_DIR) ? "yes" : "no");
90
- row("Project root", PROJECT_ROOT);
91
- row("Status script exists", existsSync(STATUS_SCRIPT) ? "yes" : `${RED}no${NC}`);
92
-
93
- section("claude-buddy state");
94
- const menagerie = tryParseJson(tryRead(join(STATE_DIR, "menagerie.json")));
95
- const status = tryParseJson(tryRead(join(STATE_DIR, "status.json")));
96
-
97
- if (isRecord(menagerie)) {
98
- const activeSlot = typeof menagerie.active === "string" ? menagerie.active : "buddy";
99
- const companions = isRecord(menagerie.companions) ? menagerie.companions : {};
100
- const companion = companions[activeSlot];
101
- row("Active slot", activeSlot);
102
- row("Total slots", String(Object.keys(companions).length));
103
- if (isRecord(companion)) {
104
- const bones = isRecord(companion.bones) ? companion.bones : {};
105
- row("Companion name", typeof companion.name === "string" ? companion.name : "(none)");
106
- row("Species", typeof bones.species === "string" ? bones.species : "(none)");
107
- row("Rarity", typeof bones.rarity === "string" ? bones.rarity : "(none)");
108
- row("Hat", typeof bones.hat === "string" ? bones.hat : "(none)");
109
- row("Eye", typeof bones.eye === "string" ? bones.eye : "(none)");
110
- row("Shiny", String(typeof bones.shiny === "boolean" ? bones.shiny : false));
111
- } else {
112
- err(`No companion found in active slot "${activeSlot}"`);
113
- }
114
- } else {
115
- err(`No manifest found at ${join(STATE_DIR, "menagerie.json")}`);
116
- }
117
-
118
- if (isRecord(status)) {
119
- row("Status muted", String(typeof status.muted === "boolean" ? status.muted : false));
120
- row("Current reaction", typeof status.reaction === "string" && status.reaction.length > 0 ? status.reaction : "(none)");
121
- } else {
122
- warn(`No status state at ${join(STATE_DIR, "status.json")}`);
123
- }
124
-
125
- section("Claude Code config");
126
- const settings = tryParseJson(tryRead(SETTINGS));
127
- const claudeJson = tryParseJson(tryRead(CLAUDE_JSON));
128
-
129
- if (isRecord(settings) && "statusLine" in settings && settings.statusLine !== undefined) {
130
- console.log(` ${DIM}statusLine:${NC}`);
131
- console.log(` ${JSON.stringify(settings.statusLine, null, 2).split("\n").join("\n ")}`);
132
- } else {
133
- warn(`No statusLine in ${SETTINGS}`);
134
- }
135
-
136
- if (isRecord(settings) && isRecord(settings.hooks)) {
137
- console.log(` ${DIM}hooks:${NC}`);
138
- for (const event of Object.keys(settings.hooks)) {
139
- const hookEntries = settings.hooks[event];
140
- const count = Array.isArray(hookEntries) ? hookEntries.length : 0;
141
- row(` ${event}`, `${count} entr${count === 1 ? "y" : "ies"}`);
142
- }
143
- } else {
144
- warn("No hooks configured");
145
- }
146
-
147
- if (isRecord(claudeJson) && isRecord(claudeJson.mcpServers) && isRecord(claudeJson.mcpServers["claude-buddy"])) {
148
- ok(`MCP server registered in ${CLAUDE_JSON}`);
149
- console.log(` ${JSON.stringify(claudeJson.mcpServers["claude-buddy"], null, 2).split("\n").join("\n ")}`);
150
- } else {
151
- err(`MCP server NOT registered in ${CLAUDE_JSON}`);
152
- }
153
-
154
- if (existsSync(SKILL_PATH)) {
155
- ok(`Skill installed: ${SKILL_PATH}`);
156
- } else {
157
- err(`Skill missing: ${SKILL_PATH}`);
158
- }
159
-
160
- section("Live status line test");
161
- if (existsSync(STATUS_SCRIPT)) {
162
- try {
163
- const output = execSync(`echo '{}' | ${STATUS_SCRIPT}`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trimEnd();
164
- ok("Status script ran successfully");
165
- console.log(`\n ${DIM}Output preview:${NC}`);
166
- for (const line of output.split("\n").slice(0, 6)) console.log(` ${line}`);
167
- } catch {
168
- err("Status script failed to run");
169
- }
170
- }
171
-
172
- console.log("");
@@ -1,264 +0,0 @@
1
- /**
2
- * ASCII art for all 18 buddy species
3
- *
4
- * Each species has 3 animation frames (idle variations).
5
- * Each frame is 5 lines, ~12 chars wide.
6
- * {E} is replaced with the eye character at render time.
7
- */
8
-
9
- import type { Rarity, StatName, BuddyBones } from "../../../core/engine.ts";
10
- import { HAT_ART } from "../../../core/art-data.ts";
11
- import { getArtFrame, SPECIES_ART } from "../../../core/render-model.ts";
12
-
13
- // ─── Rarity ANSI colors ────────────────────────────────────────────────────
14
-
15
- const RARITY_COLOR: Record<Rarity, string> = {
16
- common: "\x1b[38;2;153;153;153m", // inactive rgb(153,153,153)
17
- uncommon: "\x1b[38;2;78;186;101m", // success rgb(78,186,101)
18
- rare: "\x1b[38;2;177;185;249m", // permission rgb(177,185,249)
19
- epic: "\x1b[38;2;175;135;255m", // autoAccept rgb(175,135,255)
20
- legendary: "\x1b[38;2;255;193;7m", // warning rgb(255,193,7)
21
- };
22
-
23
- const SHINY_COLOR = "\x1b[93m"; // bright yellow
24
- const BOLD = "\x1b[1m";
25
- const DIM = "\x1b[2m";
26
- const NC = "\x1b[0m";
27
-
28
- export const RARITY_STARS: Record<Rarity, string> = {
29
- common: "\u2605",
30
- uncommon: "\u2605\u2605",
31
- rare: "\u2605\u2605\u2605",
32
- epic: "\u2605\u2605\u2605\u2605",
33
- legendary: "\u2605\u2605\u2605\u2605\u2605",
34
- };
35
-
36
- // ─── Display width helpers ──────────────────────────────────────────────────
37
-
38
- function stripAnsi(s: string): string { return s.replace(/\x1b\[[^m]*m/g, ""); }
39
-
40
- function charWidth(cp: number): number {
41
- if (cp >= 0xFE00 && cp <= 0xFE0F) return 0;
42
- if (cp === 0x200D) return 0;
43
- if (cp >= 0x1F000) return 2;
44
- if (cp === 0x2728) return 2; // ✨
45
- if (cp >= 0x2600 && cp <= 0x27BF) return 1;
46
- if (cp >= 0x2500 && cp <= 0x259F) return 1;
47
- if (cp >= 0x3000 && cp <= 0x9FFF) return 2;
48
- if (cp >= 0xFF01 && cp <= 0xFF60) return 2;
49
- return 1;
50
- }
51
-
52
- function displayWidth(s: string): number {
53
- let w = 0;
54
- for (const ch of stripAnsi(s)) w += charWidth(ch.codePointAt(0)!);
55
- return w;
56
- }
57
-
58
- /** Pad string with spaces to reach target display width */
59
- function dpad(s: string, targetW: number): string {
60
- const w = displayWidth(s);
61
- return w < targetW ? s + " ".repeat(targetW - w) : s;
62
- }
63
-
64
- // ─── Render functions ───────────────────────────────────────────────────────
65
-
66
- export function renderCompanionCard(
67
- bones: BuddyBones,
68
- name: string,
69
- personality: string,
70
- reaction?: string,
71
- frame: number = 0,
72
- width: number = 40,
73
- ): string {
74
- const color = RARITY_COLOR[bones.rarity];
75
- const stars = RARITY_STARS[bones.rarity];
76
- const shiny = bones.shiny ? `${SHINY_COLOR}\u2728 ${NC}` : "";
77
- const art = getArtFrame(bones.species, bones.eye, frame);
78
-
79
- // Hat: replace first empty art line
80
- const hatLine = HAT_ART[bones.hat];
81
- if (hatLine && !art[0].trim()) {
82
- art[0] = hatLine;
83
- }
84
-
85
- // Build the card
86
- const W = Math.max(24, width);
87
- const hr = "\u2500".repeat(W - 2);
88
- const lines: string[] = [];
89
-
90
- // Top border
91
- lines.push(`${color}\u256d${hr}\u256e${NC}`);
92
-
93
- // Inner width = W - 2 (borders), content area = W - 4 (borders + padding)
94
- const innerW = W - 4;
95
-
96
- // Species art (centered)
97
- for (const artLine of art) {
98
- if (!artLine.trim()) continue;
99
- lines.push(`${color}\u2502${NC} ${dpad(artLine, innerW)}${color}\u2502${NC}`);
100
- }
101
-
102
- // Separator
103
- lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
104
-
105
- // Name + rarity
106
- const nameStarsRaw = `${BOLD}${name}${NC} ${color}${stars}${NC}`;
107
- lines.push(`${color}\u2502${NC} ${nameStarsRaw}${" ".repeat(Math.max(0, innerW - displayWidth(name) - 2 - displayWidth(stars)))}${color}\u2502${NC}`);
108
-
109
- const rarityRaw = `${shiny}${color}${BOLD}${bones.rarity.toUpperCase()}${NC} ${bones.species}`;
110
- const rarityVis = (bones.shiny ? 3 : 0) + bones.rarity.length + 1 + bones.species.length;
111
- lines.push(`${color}\u2502${NC} ${rarityRaw}${" ".repeat(Math.max(0, innerW - rarityVis))}${color}\u2502${NC}`);
112
-
113
- // Eye + Hat info
114
- const cosmeticLine = `eye: ${bones.eye} hat: ${bones.hat}`;
115
- lines.push(`${color}\u2502${NC} ${DIM}${cosmeticLine}${NC}${" ".repeat(Math.max(0, innerW - displayWidth(cosmeticLine)))}${color}\u2502${NC}`);
116
-
117
- // Separator
118
- lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
119
-
120
- // Stats
121
- const STAT_NAMES: StatName[] = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"];
122
- for (const stat of STAT_NAMES) {
123
- const val = bones.stats[stat];
124
- const filled = Math.round(val / 10);
125
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
126
- const label = stat.slice(0, 3).padEnd(3);
127
- const marker = stat === bones.peak ? " \u25b2" : stat === bones.dump ? " \u25bc" : " ";
128
- const valStr = String(val).padStart(3);
129
- const statLine = `${DIM}${label}${NC} ${bar} ${valStr}${marker}`;
130
- const statVis = 3 + 1 + 10 + 1 + 3 + 2; // label + spaces + bar + val + marker = 20
131
- lines.push(`${color}\u2502${NC} ${statLine}${" ".repeat(Math.max(0, innerW - statVis))}${color}\u2502${NC}`);
132
- }
133
-
134
- // Speech bubble (if reaction)
135
- if (reaction) {
136
- lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
137
- const maxMsg = innerW - 3; // "💬 " prefix (💬 = 2 cols + space)
138
- const msg = displayWidth(reaction) > maxMsg ? reaction.slice(0, maxMsg - 1) + "\u2026" : reaction;
139
- const msgPad = Math.max(0, innerW - displayWidth(msg) - 3);
140
- lines.push(`${color}\u2502${NC} \ud83d\udcac ${msg}${" ".repeat(msgPad)}${color}\u2502${NC}`);
141
- }
142
-
143
- // Personality
144
- if (personality) {
145
- lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
146
- const words = personality.split(" ");
147
- let line = "";
148
- for (const word of words) {
149
- if (displayWidth(line) + displayWidth(word) + 1 > innerW) {
150
- lines.push(`${color}\u2502${NC} ${DIM}${dpad(line, innerW)}${NC}${color}\u2502${NC}`);
151
- line = word;
152
- } else {
153
- line = line ? `${line} ${word}` : word;
154
- }
155
- }
156
- if (line) {
157
- lines.push(`${color}\u2502${NC} ${DIM}${dpad(line, innerW)}${NC}${color}\u2502${NC}`);
158
- }
159
- }
160
-
161
- // Bottom border
162
- lines.push(`${color}\u2570${hr}\u256f${NC}`);
163
-
164
- return lines.join("\n");
165
- }
166
-
167
- // ─── Markdown-native render (for MCP tool responses) ───────────────────────
168
- //
169
- // Claude Code's UI doesn't render raw ANSI escape codes properly — it strips
170
- // the ESC byte but leaves "[38;2;...m" as literal text, making the output
171
- // unreadable. This renderer produces pure markdown with unicode rarity dots
172
- // instead of ANSI colors, so it renders cleanly in any MCP client UI.
173
-
174
- const RARITY_DOT: Record<Rarity, string> = {
175
- common: "\u26AA", // ⚪ white circle
176
- uncommon: "\uD83D\uDFE2", // 🟢 green circle
177
- rare: "\uD83D\uDD35", // 🔵 blue circle
178
- epic: "\uD83D\uDFE3", // 🟣 purple circle
179
- legendary: "\uD83D\uDFE1", // 🟡 yellow circle
180
- };
181
-
182
- export function renderCompanionCardMarkdown(
183
- bones: BuddyBones,
184
- name: string,
185
- personality: string,
186
- reaction?: string,
187
- frame: number = 0,
188
- ): string {
189
- const dot = RARITY_DOT[bones.rarity];
190
- const stars = RARITY_STARS[bones.rarity];
191
- const shiny = bones.shiny ? " \u2728" : "";
192
- const art = getArtFrame(bones.species, bones.eye, frame);
193
-
194
- // Hat: replace first empty art line
195
- const hatLine = HAT_ART[bones.hat];
196
- if (hatLine && !art[0].trim()) {
197
- art[0] = hatLine;
198
- }
199
-
200
- // Strip empty lines from art for cleaner rendering
201
- const artLines = art.filter((l) => l.trim().length > 0);
202
-
203
- const STAT_NAMES: StatName[] = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"];
204
- const statRows = STAT_NAMES.map((stat) => {
205
- const val = bones.stats[stat];
206
- const filled = Math.round(val / 10);
207
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
208
- const marker = stat === bones.peak ? " \u25B2" : stat === bones.dump ? " \u25BC" : "";
209
- const label = `**${stat.slice(0, 3)}**${stat.slice(3)}`;
210
- return `| ${label} | ${val}${marker} | \`${bar}\` |`;
211
- }).join("\n");
212
-
213
- const parts: string[] = [];
214
-
215
- // Header: rarity dot, name, species+rarity, stars, shiny
216
- parts.push(`### ${dot} ${name} · \`${bones.rarity.toUpperCase()} ${bones.species}\` · ${stars}${shiny}`);
217
- parts.push("");
218
-
219
- // ASCII art in a code block (preserves monospaced formatting)
220
- parts.push("```");
221
- parts.push(artLines.join("\n"));
222
- parts.push("```");
223
- parts.push("");
224
-
225
- // Identity line
226
- parts.push(`**Identity:** eye \`${bones.eye}\` · hat \`${bones.hat}\``);
227
- parts.push("");
228
-
229
- // Stats table
230
- parts.push("| Stat | Value | Bar |");
231
- parts.push("|---|---|---|");
232
- parts.push(statRows);
233
- parts.push("");
234
-
235
- // Reaction (if any) — reactions often already contain asterisks
236
- // for actions like "*blinks slowly*", so render them verbatim to avoid
237
- // accidentally turning italics into bold.
238
- if (reaction) {
239
- parts.push(`\ud83d\udcac ${reaction}`);
240
- parts.push("");
241
- }
242
-
243
- // Personality as blockquote
244
- if (personality) {
245
- parts.push(`> ${personality}`);
246
- }
247
-
248
- return parts.join("\n");
249
- }
250
-
251
- // ─── Compact status line render ─────────────────────────────────────────────
252
-
253
- export function renderStatusLine(
254
- bones: BuddyBones,
255
- name: string,
256
- reaction?: string,
257
- ): string {
258
- const face = SPECIES_ART[bones.species][0][2]?.replace(/\{E\}/g, bones.eye).trim() || "(?)";
259
- const color = RARITY_COLOR[bones.rarity];
260
- const stars = RARITY_STARS[bones.rarity];
261
- const shiny = bones.shiny ? "\u2728" : "";
262
- const msg = reaction ? ` \u2502 "${reaction}"` : "";
263
- return `${color}${face}${NC} ${BOLD}${name}${NC} ${shiny}${color}${stars}${NC}${msg}`;
264
- }
@@ -1,24 +0,0 @@
1
- import type { Companion } from "../../../core/engine.ts";
2
-
3
- export function getInstructions(companion: Companion | null): string {
4
- if (!companion) {
5
- return "Companion not yet hatched. Use buddy_show to initialize.";
6
- }
7
- const b = companion.bones;
8
- return [
9
- `A ${b.rarity} ${b.species} named ${companion.name} watches from the status line.`,
10
- `Personality: ${companion.personality}`,
11
- `Peak stat: ${b.peak} (${b.stats[b.peak]}). Dump stat: ${b.dump} (${b.stats[b.dump]}).`,
12
- "",
13
- `NAME REACTIONS: When the user's message contains "${companion.name}", call buddy_react immediately`,
14
- `with a short in-character response (surprise, recognition, dry wit — fits the personality).`,
15
- `Display the tool result verbatim. This is the only time to call buddy_react proactively.`,
16
- "",
17
- `END-OF-TURN: At the very end of EVERY response, append an invisible HTML comment:`,
18
- `<!-- buddy: [reaction here] -->`,
19
- `Write it in ${companion.name}'s voice — 1 short sentence referencing something specific from this turn.`,
20
- `Use *asterisks* for actions. A Stop hook extracts this for the status line.`,
21
- `Do NOT use buddy_react for end-of-turn comments. Do NOT explain the comment. Just append it silently.`,
22
- `Example: <!-- buddy: *adjusts crown* that error handler is missing a finally block -->`,
23
- ].join("\n");
24
- }