@ramarivera/coding-buddy 0.4.0-alpha.9 → 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 +45 -0
- package/.claude-plugin/plugin.json +30 -0
- package/LICENSE +1 -1
- package/README.md +96 -45
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +130 -33
- package/cli/buddy-shell.ts +1 -1
- package/{adapters/claude/install → cli}/disable.ts +19 -32
- package/cli/doctor.ts +231 -0
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +30 -26
- package/{adapters/claude/install → cli}/install.ts +65 -83
- package/{adapters/claude/install → cli}/pick.ts +136 -57
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
- package/cli/tui.tsx +106 -17
- package/{adapters/claude/install → cli}/uninstall.ts +40 -47
- package/cli/upgrade.ts +46 -13
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +22 -9
- package/hooks/file-type-react.sh +403 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +210 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
- package/hooks/react.sh +1364 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +70 -31
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +33 -2
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -15
- package/server/art.ts +450 -1
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/{adapters/claude/server → server}/index.ts +556 -95
- package/server/mcp-launcher.sh +16 -6
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +70 -18
- package/server/shift-mood.ts +43 -0
- package/server/state.ts +552 -3
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
- package/statusline/buddy-status.sh +402 -0
- package/statusline/combined-status.sh +135 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -207
- package/adapters/claude/install/doctor.ts +0 -172
- package/adapters/claude/plugin/marketplace.json +0 -40
- package/adapters/claude/plugin/plugin.json +0 -26
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/path.ts +0 -12
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -408
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -33
- package/adapters/claude/storage/settings.ts +0 -91
- package/adapters/claude/storage/state.ts +0 -365
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
- package/server/engine.ts +0 -1
- package/server/reactions.ts +0 -1
package/server/art.ts
CHANGED
|
@@ -1 +1,450 @@
|
|
|
1
|
-
|
|
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 { Species, Eye, Hat, Rarity, StatName, BuddyBones } from "../core/engine.ts"
|
|
10
|
+
import { HAT_ART } from "../core/art-data.ts";
|
|
11
|
+
import { getRarityColor } from "./theme.ts";
|
|
12
|
+
export { HAT_ART };
|
|
13
|
+
|
|
14
|
+
// ─── Species art: 3 frames × 5 lines each ──────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const SPECIES_ART: Record<Species, string[][]> = {
|
|
17
|
+
duck: [
|
|
18
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--' "],
|
|
19
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--'~ "],
|
|
20
|
+
[" ", " __ ", " <({E} )___ ", " ( .__> ", " `--' "],
|
|
21
|
+
],
|
|
22
|
+
goose: [
|
|
23
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
24
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
25
|
+
[" ", " ({E}>> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
26
|
+
],
|
|
27
|
+
blob: [
|
|
28
|
+
[" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----' "],
|
|
29
|
+
[" ", " .------. ", " ( {E} {E} ) ", " ( ) ", " `------' "],
|
|
30
|
+
[" ", " .--. ", " ({E} {E}) ", " ( ) ", " `--' "],
|
|
31
|
+
],
|
|
32
|
+
cat: [
|
|
33
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
34
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\")~ "],
|
|
35
|
+
[" ", " /\\-/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
36
|
+
],
|
|
37
|
+
dragon: [
|
|
38
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
39
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ) ", " `-vvvv-' "],
|
|
40
|
+
[" ~ ~ ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
41
|
+
],
|
|
42
|
+
octopus: [
|
|
43
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
44
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " \\/\\/\\/\\/ "],
|
|
45
|
+
[" o ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
46
|
+
],
|
|
47
|
+
owl: [
|
|
48
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----' "],
|
|
49
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " .----. "],
|
|
50
|
+
[" ", " /\\ /\\ ", " (({E})(-)) ", " ( >< ) ", " `----' "],
|
|
51
|
+
],
|
|
52
|
+
penguin: [
|
|
53
|
+
[" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' "],
|
|
54
|
+
[" ", " .---. ", " ({E}>{E}) ", " |( )| ", " `---' "],
|
|
55
|
+
[" .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' ", " ~ ~ "],
|
|
56
|
+
],
|
|
57
|
+
turtle: [
|
|
58
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
59
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
60
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[======]\\ ", " `` `` "],
|
|
61
|
+
],
|
|
62
|
+
snail: [
|
|
63
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
64
|
+
[" ", " {E} .--. ", " | ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
65
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~ "],
|
|
66
|
+
],
|
|
67
|
+
ghost: [
|
|
68
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
|
|
69
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " `~`~~`~` "],
|
|
70
|
+
[" ~ ~ ", " .----. ", " / {E} {E} \\ ", " | | ", " ~~`~~`~~ "],
|
|
71
|
+
],
|
|
72
|
+
axolotl: [
|
|
73
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
|
|
74
|
+
[" ", "~}(______){~", "~}({E} .. {E}){~", " ( .--. ) ", " (_/ \\_) "],
|
|
75
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( -- ) ", " ~_/ \\_~ "],
|
|
76
|
+
],
|
|
77
|
+
capybara: [
|
|
78
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
79
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( Oo ) ", " `------' "],
|
|
80
|
+
[" ~ ~ ", " u______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
81
|
+
],
|
|
82
|
+
cactus: [
|
|
83
|
+
[" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
84
|
+
[" ", " ____ ", " n |{E} {E}| n ", " |_| |_| ", " | | "],
|
|
85
|
+
[" n n ", " | ____ | ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
86
|
+
],
|
|
87
|
+
robot: [
|
|
88
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
89
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ -==- ] ", " `------' "],
|
|
90
|
+
[" * ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
91
|
+
],
|
|
92
|
+
rabbit: [
|
|
93
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
94
|
+
[" ", " (|__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
95
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( . . )= ", " (\")__(\")" ],
|
|
96
|
+
],
|
|
97
|
+
mushroom: [
|
|
98
|
+
[" ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
99
|
+
[" ", " .-O-oo-O-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
100
|
+
[" . o . ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
101
|
+
],
|
|
102
|
+
chonk: [
|
|
103
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
104
|
+
[" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
105
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------'~ "],
|
|
106
|
+
],
|
|
107
|
+
wyvern: [
|
|
108
|
+
["} {",
|
|
109
|
+
"|\\^```^/|",
|
|
110
|
+
"\\ {E}' '{E} /",
|
|
111
|
+
" \\ } { /",
|
|
112
|
+
" ≈(° °)≈",
|
|
113
|
+
" '-'"],
|
|
114
|
+
["} {",
|
|
115
|
+
"|\\^```^/|",
|
|
116
|
+
"\\ {E}' '{E} /",
|
|
117
|
+
" \\ } { /",
|
|
118
|
+
" ≈(° °)≈",
|
|
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
|
+
["} {",
|
|
121
|
+
"|\\^```^/|",
|
|
122
|
+
"\\ {E}' '{E} /",
|
|
123
|
+
" \\ } { /",
|
|
124
|
+
" ≈(° °)≈",
|
|
125
|
+
" 'v'"],
|
|
126
|
+
],
|
|
127
|
+
pikachu: [
|
|
128
|
+
[" ", " /\\_/\\ ", " ({E} {E}) ", " ( ω ) ", " (__) "],
|
|
129
|
+
[" ", " /\\_/\\ ", " (- -) ", " ( ω ) ", " (__) "],
|
|
130
|
+
[" ", " /\\_/\\ ", " ({E} {E}) ", " ( ~ ) ", " (__) "],
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// Wyvern line 0 is `} {` (7 inner chars between horns).
|
|
136
|
+
// These replace that line so the hat sits between the horns.
|
|
137
|
+
const WYVERN_HAT: Partial<Record<Hat, string>> = {
|
|
138
|
+
crown: "} \\^^^/ {", // \^^^/ (5) centered in 7
|
|
139
|
+
tophat: "} [___] {", // [___] (5) centered in 7
|
|
140
|
+
propeller: "} -+- {", // -+- (3) centered in 7
|
|
141
|
+
halo: "} ( ) {", // ( ) (5) centered in 7
|
|
142
|
+
wizard: "} /^\\ {", // /^\ (3) centered in 7
|
|
143
|
+
beanie: "} (___) {", // (___) (5) centered in 7
|
|
144
|
+
tinyduck: "} ,> {", // ,> (2) slightly left of center
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function applyHat(species: Species, hat: Hat, art: string[]): void {
|
|
148
|
+
if (hat === "none") return;
|
|
149
|
+
if (species === "wyvern") {
|
|
150
|
+
const wyvernLine = WYVERN_HAT[hat];
|
|
151
|
+
if (wyvernLine) art[0] = wyvernLine;
|
|
152
|
+
} else if (!art[0].trim()) {
|
|
153
|
+
art[0] = HAT_ART[hat];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const SHINY_COLOR = "\x1b[93m"; // bright yellow
|
|
158
|
+
const BOLD = "\x1b[1m";
|
|
159
|
+
const DIM = "\x1b[2m";
|
|
160
|
+
const NC = "\x1b[0m";
|
|
161
|
+
|
|
162
|
+
export const RARITY_STARS: Record<Rarity, string> = {
|
|
163
|
+
common: "\u2605",
|
|
164
|
+
uncommon: "\u2605\u2605",
|
|
165
|
+
rare: "\u2605\u2605\u2605",
|
|
166
|
+
epic: "\u2605\u2605\u2605\u2605",
|
|
167
|
+
legendary: "\u2605\u2605\u2605\u2605\u2605",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// ─── Display width helpers ──────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function stripAnsi(s: string): string { return s.replace(/\x1b\[[^m]*m/g, ""); }
|
|
173
|
+
|
|
174
|
+
// Unicode property escapes (ES2018) are the source of truth for which
|
|
175
|
+
// codepoints terminals render 2 cols wide. The statusline (bash) can't use
|
|
176
|
+
// these directly, so scripts/gen-emoji-widths.ts exports the subset that
|
|
177
|
+
// bash needs into statusline/emoji-widths.data — regenerate on version bumps.
|
|
178
|
+
const EMOJI_PRES_RE = /\p{Emoji_Presentation}/u;
|
|
179
|
+
const EMOJI_RE = /\p{Emoji}/u;
|
|
180
|
+
|
|
181
|
+
// Precondition: ch is neither a variation selector (U+FE00-U+FE0F) nor ZWJ
|
|
182
|
+
// (U+200D); displayWidth filters those before calling in.
|
|
183
|
+
function charWidth(ch: string): number {
|
|
184
|
+
if (EMOJI_PRES_RE.test(ch)) return 2;
|
|
185
|
+
const cp = ch.codePointAt(0)!;
|
|
186
|
+
if (cp >= 0x2500 && cp <= 0x259F) return 1;
|
|
187
|
+
if (cp >= 0x3000 && cp <= 0x9FFF) return 2;
|
|
188
|
+
if (cp >= 0xFF01 && cp <= 0xFF60) return 2;
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function displayWidth(s: string): number {
|
|
193
|
+
let w = 0;
|
|
194
|
+
let upgradable = false;
|
|
195
|
+
for (const ch of stripAnsi(s)) {
|
|
196
|
+
const cp = ch.codePointAt(0)!;
|
|
197
|
+
if (cp === 0xFE0F) {
|
|
198
|
+
// VS16 forces emoji presentation on the previous codepoint; upgrade
|
|
199
|
+
// its width from 1 to 2 if it was narrow-but-emoji (e.g. ❤ + VS16).
|
|
200
|
+
if (upgradable) { w += 1; upgradable = false; }
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if ((cp >= 0xFE00 && cp <= 0xFE0E) || cp === 0x200D) {
|
|
204
|
+
upgradable = false;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const cw = charWidth(ch);
|
|
208
|
+
w += cw;
|
|
209
|
+
upgradable = cw === 1 && EMOJI_RE.test(ch);
|
|
210
|
+
}
|
|
211
|
+
return w;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Pad string with spaces to reach target display width */
|
|
215
|
+
function dpad(s: string, targetW: number): string {
|
|
216
|
+
const w = displayWidth(s);
|
|
217
|
+
return w < targetW ? s + " ".repeat(targetW - w) : s;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Render functions ───────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export function getArtFrame(species: Species, eye: Eye, frame: number = 0): string[] {
|
|
223
|
+
const frames = SPECIES_ART[species];
|
|
224
|
+
const f = frames[frame % frames.length];
|
|
225
|
+
return f.map((line) => line.replace(/\{E\}/g, eye));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Original 15-tick cycle [0,0,0,0,1,0,0,0,-1,0,0,2,0,0,0]: -1 (blink) becomes
|
|
229
|
+
// index 3 in the pre-baked frames array.
|
|
230
|
+
export const STATUS_FRAME_SEQUENCE: readonly number[] = [
|
|
231
|
+
0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 2, 0, 0, 0,
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Pre-resolves eye, hat overlay, and blink so the statusline shell does no art
|
|
235
|
+
// work — it just cycles whatever frames the server writes. Each frame is a
|
|
236
|
+
// \n-joined 5-line string (one jq call + mapfile in bash).
|
|
237
|
+
export function getStatusFrames(bones: BuddyBones): {
|
|
238
|
+
frames: string[];
|
|
239
|
+
frameSequence: number[];
|
|
240
|
+
} {
|
|
241
|
+
const resolveFrame = (frameIdx: number, eye: string): string => {
|
|
242
|
+
const raw = SPECIES_ART[bones.species][frameIdx];
|
|
243
|
+
const art = raw.map((line) => line.replace(/\{E\}/g, eye));
|
|
244
|
+
const hatLine = HAT_ART[bones.hat];
|
|
245
|
+
if (hatLine && !art[0].trim()) {
|
|
246
|
+
art[0] = hatLine;
|
|
247
|
+
}
|
|
248
|
+
return art.join("\n");
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
frames: [
|
|
253
|
+
resolveFrame(0, bones.eye),
|
|
254
|
+
resolveFrame(1, bones.eye),
|
|
255
|
+
resolveFrame(2, bones.eye),
|
|
256
|
+
resolveFrame(0, "-"),
|
|
257
|
+
],
|
|
258
|
+
frameSequence: [...STATUS_FRAME_SEQUENCE],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function renderCompanionCard(
|
|
263
|
+
bones: BuddyBones,
|
|
264
|
+
name: string,
|
|
265
|
+
personality: string,
|
|
266
|
+
reaction?: string,
|
|
267
|
+
frame: number = 0,
|
|
268
|
+
width: number = 40,
|
|
269
|
+
): string {
|
|
270
|
+
const color = getRarityColor(bones.rarity);
|
|
271
|
+
const stars = RARITY_STARS[bones.rarity];
|
|
272
|
+
const shiny = bones.shiny ? `${SHINY_COLOR}\u2728 ${NC}` : "";
|
|
273
|
+
const art = getArtFrame(bones.species, bones.eye, frame);
|
|
274
|
+
applyHat(bones.species, bones.hat, art);
|
|
275
|
+
|
|
276
|
+
// Build the card
|
|
277
|
+
const W = Math.max(24, width);
|
|
278
|
+
const hr = "\u2500".repeat(W - 2);
|
|
279
|
+
const lines: string[] = [];
|
|
280
|
+
|
|
281
|
+
// Top border
|
|
282
|
+
lines.push(`${color}\u256d${hr}\u256e${NC}`);
|
|
283
|
+
|
|
284
|
+
// Inner width = W - 2 (borders), content area = W - 4 (borders + padding)
|
|
285
|
+
const innerW = W - 4;
|
|
286
|
+
|
|
287
|
+
// Species art (centered)
|
|
288
|
+
for (const artLine of art) {
|
|
289
|
+
if (!artLine.trim()) continue;
|
|
290
|
+
lines.push(`${color}\u2502${NC} ${dpad(artLine, innerW)}${color}\u2502${NC}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Separator
|
|
294
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
295
|
+
|
|
296
|
+
// Name + rarity
|
|
297
|
+
const nameStarsRaw = `${BOLD}${name}${NC} ${color}${stars}${NC}`;
|
|
298
|
+
lines.push(`${color}\u2502${NC} ${nameStarsRaw}${" ".repeat(Math.max(0, innerW - displayWidth(name) - 2 - displayWidth(stars)))}${color}\u2502${NC}`);
|
|
299
|
+
|
|
300
|
+
const rarityRaw = `${shiny}${color}${BOLD}${bones.rarity.toUpperCase()}${NC} ${bones.species}`;
|
|
301
|
+
const rarityVis = (bones.shiny ? 3 : 0) + bones.rarity.length + 1 + bones.species.length;
|
|
302
|
+
lines.push(`${color}\u2502${NC} ${rarityRaw}${" ".repeat(Math.max(0, innerW - rarityVis))}${color}\u2502${NC}`);
|
|
303
|
+
|
|
304
|
+
// Eye + Hat info
|
|
305
|
+
const cosmeticLine = `eye: ${bones.eye} hat: ${bones.hat}`;
|
|
306
|
+
lines.push(`${color}\u2502${NC} ${DIM}${cosmeticLine}${NC}${" ".repeat(Math.max(0, innerW - displayWidth(cosmeticLine)))}${color}\u2502${NC}`);
|
|
307
|
+
|
|
308
|
+
// Separator
|
|
309
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
310
|
+
|
|
311
|
+
// Stats
|
|
312
|
+
const STAT_NAMES: StatName[] = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"];
|
|
313
|
+
for (const stat of STAT_NAMES) {
|
|
314
|
+
const val = bones.stats[stat];
|
|
315
|
+
const filled = Math.round(val / 10);
|
|
316
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
317
|
+
const label = stat.slice(0, 3).padEnd(3);
|
|
318
|
+
const marker = stat === bones.peak ? " \u25b2" : stat === bones.dump ? " \u25bc" : " ";
|
|
319
|
+
const valStr = String(val).padStart(3);
|
|
320
|
+
const statLine = `${DIM}${label}${NC} ${bar} ${valStr}${marker}`;
|
|
321
|
+
const statVis = 3 + 1 + 10 + 1 + 3 + 2; // label + spaces + bar + val + marker = 20
|
|
322
|
+
lines.push(`${color}\u2502${NC} ${statLine}${" ".repeat(Math.max(0, innerW - statVis))}${color}\u2502${NC}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Speech bubble (if reaction)
|
|
326
|
+
if (reaction) {
|
|
327
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
328
|
+
const maxMsg = innerW - 3; // "💬 " prefix (💬 = 2 cols + space)
|
|
329
|
+
const msg = displayWidth(reaction) > maxMsg ? reaction.slice(0, maxMsg - 1) + "\u2026" : reaction;
|
|
330
|
+
const msgPad = Math.max(0, innerW - displayWidth(msg) - 3);
|
|
331
|
+
lines.push(`${color}\u2502${NC} \ud83d\udcac ${msg}${" ".repeat(msgPad)}${color}\u2502${NC}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Personality
|
|
335
|
+
if (personality) {
|
|
336
|
+
lines.push(`${color}\u251c${"╌".repeat(W - 2)}\u2524${NC}`);
|
|
337
|
+
const words = personality.split(" ");
|
|
338
|
+
let line = "";
|
|
339
|
+
for (const word of words) {
|
|
340
|
+
if (displayWidth(line) + displayWidth(word) + 1 > innerW) {
|
|
341
|
+
lines.push(`${color}\u2502${NC} ${DIM}${dpad(line, innerW)}${NC}${color}\u2502${NC}`);
|
|
342
|
+
line = word;
|
|
343
|
+
} else {
|
|
344
|
+
line = line ? `${line} ${word}` : word;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (line) {
|
|
348
|
+
lines.push(`${color}\u2502${NC} ${DIM}${dpad(line, innerW)}${NC}${color}\u2502${NC}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Bottom border
|
|
353
|
+
lines.push(`${color}\u2570${hr}\u256f${NC}`);
|
|
354
|
+
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Markdown-native render (for MCP tool responses) ───────────────────────
|
|
359
|
+
//
|
|
360
|
+
// Claude Code's UI doesn't render raw ANSI escape codes properly — it strips
|
|
361
|
+
// the ESC byte but leaves "[38;2;...m" as literal text, making the output
|
|
362
|
+
// unreadable. This renderer produces pure markdown with unicode rarity dots
|
|
363
|
+
// instead of ANSI colors, so it renders cleanly in any MCP client UI.
|
|
364
|
+
|
|
365
|
+
const RARITY_DOT: Record<Rarity, string> = {
|
|
366
|
+
common: "\u26AA", // ⚪ white circle
|
|
367
|
+
uncommon: "\uD83D\uDFE2", // 🟢 green circle
|
|
368
|
+
rare: "\uD83D\uDD35", // 🔵 blue circle
|
|
369
|
+
epic: "\uD83D\uDFE3", // 🟣 purple circle
|
|
370
|
+
legendary: "\uD83D\uDFE1", // 🟡 yellow circle
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export function renderCompanionCardMarkdown(
|
|
374
|
+
bones: BuddyBones,
|
|
375
|
+
name: string,
|
|
376
|
+
personality: string,
|
|
377
|
+
reaction?: string,
|
|
378
|
+
frame: number = 0,
|
|
379
|
+
): string {
|
|
380
|
+
const dot = RARITY_DOT[bones.rarity];
|
|
381
|
+
const stars = RARITY_STARS[bones.rarity];
|
|
382
|
+
const shiny = bones.shiny ? " \u2728" : "";
|
|
383
|
+
const art = getArtFrame(bones.species, bones.eye, frame);
|
|
384
|
+
applyHat(bones.species, bones.hat, art);
|
|
385
|
+
|
|
386
|
+
// Strip empty lines from art for cleaner rendering
|
|
387
|
+
const artLines = art.filter((l) => l.trim().length > 0);
|
|
388
|
+
|
|
389
|
+
const STAT_NAMES: StatName[] = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"];
|
|
390
|
+
const statRows = STAT_NAMES.map((stat) => {
|
|
391
|
+
const val = bones.stats[stat];
|
|
392
|
+
const filled = Math.round(val / 10);
|
|
393
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
394
|
+
const marker = stat === bones.peak ? " \u25B2" : stat === bones.dump ? " \u25BC" : "";
|
|
395
|
+
const label = `**${stat.slice(0, 3)}**${stat.slice(3)}`;
|
|
396
|
+
return `| ${label} | ${val}${marker} | \`${bar}\` |`;
|
|
397
|
+
}).join("\n");
|
|
398
|
+
|
|
399
|
+
const parts: string[] = [];
|
|
400
|
+
|
|
401
|
+
// Header: rarity dot, name, species+rarity, stars, shiny
|
|
402
|
+
parts.push(`### ${dot} ${name} · \`${bones.rarity.toUpperCase()} ${bones.species}\` · ${stars}${shiny}`);
|
|
403
|
+
parts.push("");
|
|
404
|
+
|
|
405
|
+
// ASCII art in a code block (preserves monospaced formatting)
|
|
406
|
+
parts.push("```");
|
|
407
|
+
parts.push(artLines.join("\n"));
|
|
408
|
+
parts.push("```");
|
|
409
|
+
parts.push("");
|
|
410
|
+
|
|
411
|
+
// Identity line
|
|
412
|
+
parts.push(`**Identity:** eye \`${bones.eye}\` · hat \`${bones.hat}\``);
|
|
413
|
+
parts.push("");
|
|
414
|
+
|
|
415
|
+
// Stats table
|
|
416
|
+
parts.push("| Stat | Value | Bar |");
|
|
417
|
+
parts.push("|---|---|---|");
|
|
418
|
+
parts.push(statRows);
|
|
419
|
+
parts.push("");
|
|
420
|
+
|
|
421
|
+
// Reaction (if any) — reactions often already contain asterisks
|
|
422
|
+
// for actions like "*blinks slowly*", so render them verbatim to avoid
|
|
423
|
+
// accidentally turning italics into bold.
|
|
424
|
+
if (reaction) {
|
|
425
|
+
parts.push(`\ud83d\udcac ${reaction}`);
|
|
426
|
+
parts.push("");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Personality as blockquote
|
|
430
|
+
if (personality) {
|
|
431
|
+
parts.push(`> ${personality}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return parts.join("\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ─── Compact status line render ─────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
export function renderStatusLine(
|
|
440
|
+
bones: BuddyBones,
|
|
441
|
+
name: string,
|
|
442
|
+
reaction?: string,
|
|
443
|
+
): string {
|
|
444
|
+
const face = SPECIES_ART[bones.species][0][2]?.replace(/\{E\}/g, bones.eye).trim() || "(?)";
|
|
445
|
+
const color = getRarityColor(bones.rarity);
|
|
446
|
+
const stars = RARITY_STARS[bones.rarity];
|
|
447
|
+
const shiny = bones.shiny ? "\u2728" : "";
|
|
448
|
+
const msg = reaction ? ` \u2502 "${reaction}"` : "";
|
|
449
|
+
return `${color}${face}${NC} ${BOLD}${name}${NC} ${shiny}${color}${stars}${NC}${msg}`;
|
|
450
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight XP awarding script — called from shell hooks.
|
|
4
|
+
* Awards XP for coding events without loading the full MCP server.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run server/award-xp.ts <event> [slot]
|
|
8
|
+
*
|
|
9
|
+
* Events: errors_spotted | tests_passed | tests_failed | large_diff | turn | time_spent | buddy_pet
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { awardXp } from "./xp";
|
|
13
|
+
import { loadCompanionSlot, loadActiveSlot } from "./state";
|
|
14
|
+
import type { XpEvent } from "./xp";
|
|
15
|
+
|
|
16
|
+
const VALID_EVENTS = new Set([
|
|
17
|
+
"errors_spotted",
|
|
18
|
+
"tests_passed",
|
|
19
|
+
"tests_failed",
|
|
20
|
+
"large_diff",
|
|
21
|
+
"turn",
|
|
22
|
+
"time_spent",
|
|
23
|
+
"buddy_pet",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function main(): void {
|
|
27
|
+
const event = process.argv[2] as string;
|
|
28
|
+
const slot = process.argv[3] ?? loadActiveSlot();
|
|
29
|
+
|
|
30
|
+
if (!event || !VALID_EVENTS.has(event)) {
|
|
31
|
+
console.error(
|
|
32
|
+
`Usage: bun run server/award-xp.ts <event> [slot]\nValid events: ${[...VALID_EVENTS].join(" | ")}`,
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get species and rarity for bonus calculation
|
|
38
|
+
const companion = loadCompanionSlot(slot);
|
|
39
|
+
const species = companion?.bones.species;
|
|
40
|
+
const rarity = companion?.bones.rarity;
|
|
41
|
+
|
|
42
|
+
const state = awardXp(event as XpEvent, slot, species, rarity);
|
|
43
|
+
console.log(
|
|
44
|
+
`XP awarded: +${event} → Level ${state.level} (${state.totalXp.toLocaleString()} XP total)`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Check for suggestion patterns in the assistant's message.
|
|
4
|
+
* Called from suggest.sh hook.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run server/check-suggestions.ts <assistant_message>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
12
|
+
import { buddyStateDir } from "./path";
|
|
13
|
+
import {
|
|
14
|
+
checkRepeatedError,
|
|
15
|
+
checkLargeDiff,
|
|
16
|
+
checkTodoComment,
|
|
17
|
+
checkLongFunction,
|
|
18
|
+
checkNoTestsLongSession,
|
|
19
|
+
recordTurn,
|
|
20
|
+
recordSuggestion,
|
|
21
|
+
isOnCooldown,
|
|
22
|
+
type Suggestion,
|
|
23
|
+
} from "./suggestions";
|
|
24
|
+
|
|
25
|
+
const PENDING_FILE = join(buddyStateDir(), ".pending_suggestion.json");
|
|
26
|
+
|
|
27
|
+
function main(): void {
|
|
28
|
+
const message = process.argv[2] ?? "";
|
|
29
|
+
if (!message) {
|
|
30
|
+
// When called with no args, just record a turn
|
|
31
|
+
recordTurn();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse the JSON string (jq -Rs wraps in quotes and escapes)
|
|
36
|
+
const text = message.replace(/^"/, "").replace(/"$/, "").replace(/\\"/g, '"');
|
|
37
|
+
|
|
38
|
+
// Record turn happened
|
|
39
|
+
recordTurn();
|
|
40
|
+
|
|
41
|
+
// Check cooldown
|
|
42
|
+
if (isOnCooldown(180)) return;
|
|
43
|
+
|
|
44
|
+
let suggestion: Suggestion | null = null;
|
|
45
|
+
|
|
46
|
+
// Extract error messages
|
|
47
|
+
const errorPatterns = [
|
|
48
|
+
/([A-Z][a-z]+Error: .+)/,
|
|
49
|
+
/(SyntaxError: .+)/,
|
|
50
|
+
/(TypeError: .+)/,
|
|
51
|
+
/(ReferenceError: .+)/,
|
|
52
|
+
/(Error: .+)/,
|
|
53
|
+
/(panic: .+)/,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const pattern of errorPatterns) {
|
|
57
|
+
const match = text.match(pattern);
|
|
58
|
+
if (match) {
|
|
59
|
+
suggestion = checkRepeatedError(match[0]);
|
|
60
|
+
if (suggestion) break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for large diffs (look for insertion counts)
|
|
65
|
+
if (!suggestion) {
|
|
66
|
+
const diffMatch = text.match(/(\d+)\s+insertions/);
|
|
67
|
+
if (diffMatch) {
|
|
68
|
+
const lines = parseInt(diffMatch[1], 10);
|
|
69
|
+
if (lines > 50) {
|
|
70
|
+
suggestion = checkLargeDiff(lines);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for TODO/FIXME
|
|
76
|
+
if (!suggestion) {
|
|
77
|
+
suggestion = checkTodoComment(text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for long functions
|
|
81
|
+
if (!suggestion) {
|
|
82
|
+
suggestion = checkLongFunction(text);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for no-tests-long-session
|
|
86
|
+
if (!suggestion) {
|
|
87
|
+
suggestion = checkNoTestsLongSession();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (suggestion) {
|
|
91
|
+
recordSuggestion();
|
|
92
|
+
// Write suggestion to file for MCP server to pick up
|
|
93
|
+
mkdirSync(buddyStateDir(), { recursive: true });
|
|
94
|
+
writeFileSync(
|
|
95
|
+
PENDING_FILE,
|
|
96
|
+
JSON.stringify({ suggestion, timestamp: Date.now() }),
|
|
97
|
+
);
|
|
98
|
+
console.log(`Suggestion: ${suggestion.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight memory consolidation script — called from Stop hook.
|
|
4
|
+
* Extracts project, bug, and preference signals from the conversation.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run server/consolidate.ts <assistant_message> <user_prompt>
|
|
8
|
+
*
|
|
9
|
+
* Both arguments should be the raw text of the messages.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { consolidateMemory } from "./memory";
|
|
13
|
+
|
|
14
|
+
function main(): void {
|
|
15
|
+
const assistantMessage = process.argv[2] ?? "";
|
|
16
|
+
const userPrompt = process.argv[3] ?? "";
|
|
17
|
+
|
|
18
|
+
if (!assistantMessage && !userPrompt) {
|
|
19
|
+
console.error("Usage: bun run server/consolidate.ts <assistant_message> <user_prompt>");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
consolidateMemory(assistantMessage, userPrompt);
|
|
25
|
+
console.log("Memory consolidated successfully.");
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// Non-fatal — memory consolidation should never crash the hook
|
|
28
|
+
console.error("Memory consolidation failed:", e);
|
|
29
|
+
process.exit(0); // still exit 0 — don't break the hook
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|