@ramarivera/coding-buddy 0.4.0-alpha.1 → 0.4.0-alpha.3

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 (52) hide show
  1. package/README.md +4 -5
  2. package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
  3. package/{cli → adapters/claude/install}/backup.ts +3 -3
  4. package/{cli → adapters/claude/install}/disable.ts +22 -2
  5. package/{cli → adapters/claude/install}/doctor.ts +30 -23
  6. package/{cli → adapters/claude/install}/hunt.ts +4 -4
  7. package/{cli → adapters/claude/install}/install.ts +62 -26
  8. package/{cli → adapters/claude/install}/pick.ts +3 -3
  9. package/{cli → adapters/claude/install}/settings.ts +1 -1
  10. package/{cli → adapters/claude/install}/show.ts +2 -2
  11. package/{cli → adapters/claude/install}/uninstall.ts +22 -2
  12. package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
  13. package/adapters/claude/popup/buddy-popup.sh +92 -0
  14. package/adapters/claude/popup/buddy-render.sh +540 -0
  15. package/adapters/claude/popup/popup-manager.sh +355 -0
  16. package/{server → adapters/claude/rendering}/art.ts +3 -115
  17. package/{server → adapters/claude/server}/index.ts +49 -71
  18. package/adapters/claude/server/instructions.ts +24 -0
  19. package/adapters/claude/server/resources.ts +38 -0
  20. package/adapters/claude/storage/achievements.ts +253 -0
  21. package/adapters/claude/storage/identity.ts +14 -0
  22. package/adapters/claude/storage/settings.ts +42 -0
  23. package/{server → adapters/claude/storage}/state.ts +3 -65
  24. package/adapters/pi/README.md +64 -0
  25. package/adapters/pi/commands.ts +173 -0
  26. package/adapters/pi/events.ts +150 -0
  27. package/adapters/pi/identity.ts +10 -0
  28. package/adapters/pi/index.ts +25 -0
  29. package/adapters/pi/renderers.ts +73 -0
  30. package/adapters/pi/storage.ts +295 -0
  31. package/adapters/pi/tools.ts +6 -0
  32. package/adapters/pi/ui.ts +39 -0
  33. package/cli/index.ts +11 -11
  34. package/cli/verify.ts +2 -2
  35. package/core/achievements.ts +203 -0
  36. package/core/art-data.ts +105 -0
  37. package/core/command-service.ts +338 -0
  38. package/core/model.ts +59 -0
  39. package/core/ports.ts +40 -0
  40. package/core/render-model.ts +10 -0
  41. package/package.json +23 -19
  42. package/server/achievements.ts +0 -445
  43. /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
  44. /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
  45. /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
  46. /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
  47. /package/{cli → adapters/claude/install}/test-statusline.ts +0 -0
  48. /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
  49. /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
  50. /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
  51. /package/{server → core}/engine.ts +0 -0
  52. /package/{server → core}/reactions.ts +0 -0
@@ -1,445 +0,0 @@
1
- /**
2
- * Achievement badges — milestones that unlock as you code with your buddy
3
- *
4
- * Event counters are split into two scopes:
5
- * Global (events.json): coding-activity counters (errors, tests, diffs, days, sessions, commands)
6
- * Per-slot (events.<slot>.json): buddy-relationship counters (pets, turns, reactions)
7
- *
8
- * Achievement checks merge both scopes so threshold logic is transparent.
9
- * All writes use tmp+rename for atomicity (same pattern as state.ts).
10
- */
11
-
12
- import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "fs";
13
- import { join } from "path";
14
- import { homedir } from "os";
15
-
16
- const STATE_DIR = join(homedir(), ".claude-buddy");
17
- const EVENTS_FILE = join(STATE_DIR, "events.json");
18
- const DAYS_FILE = join(STATE_DIR, "active_days.json");
19
- const UNLOCKED_FILE = join(STATE_DIR, "unlocked.json");
20
-
21
- function slotEventsFile(slot: string): string {
22
- return join(STATE_DIR, `events.${slot}.json`);
23
- }
24
-
25
- function ensureDir(): void {
26
- if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
27
- }
28
-
29
- function atomicWrite(path: string, data: string): void {
30
- ensureDir();
31
- const tmp = path + ".tmp";
32
- writeFileSync(tmp, data);
33
- renameSync(tmp, path);
34
- }
35
-
36
- // ─── Event counters (global) ─────────────────────────────────────────────────
37
-
38
- export interface GlobalCounters {
39
- errors_seen: number;
40
- tests_failed: number;
41
- large_diffs: number;
42
- sessions: number;
43
- commands_run: number;
44
- days_active: number;
45
- turns: number;
46
- }
47
-
48
- // ─── Event counters (per-slot) ────────────────────────────────────────────────
49
-
50
- export interface SlotCounters {
51
- pets: number;
52
- reactions_given: number;
53
- }
54
-
55
- // ─── Merged view for achievement checks ───────────────────────────────────────
56
-
57
- export interface EventCounters extends GlobalCounters {
58
- pets: number;
59
- reactions_given: number;
60
- }
61
-
62
- export const GLOBAL_KEYS: (keyof GlobalCounters)[] = [
63
- "errors_seen", "tests_failed", "large_diffs",
64
- "sessions", "commands_run", "days_active", "turns",
65
- ];
66
-
67
- export const SLOT_KEYS: (keyof SlotCounters)[] = [
68
- "pets", "reactions_given",
69
- ];
70
-
71
- export const COUNTER_KEYS: (keyof EventCounters)[] = [
72
- "errors_seen", "tests_failed", "large_diffs", "turns", "pets",
73
- "sessions", "reactions_given", "commands_run", "days_active",
74
- ];
75
-
76
- const EMPTY_GLOBAL: GlobalCounters = {
77
- errors_seen: 0, tests_failed: 0, large_diffs: 0,
78
- sessions: 0, commands_run: 0, days_active: 0, turns: 0,
79
- };
80
-
81
- const EMPTY_SLOT: SlotCounters = {
82
- pets: 0, reactions_given: 0,
83
- };
84
-
85
- export function loadGlobalEvents(): GlobalCounters {
86
- try {
87
- const parsed = JSON.parse(readFileSync(EVENTS_FILE, "utf8"));
88
- return { ...EMPTY_GLOBAL, ...parsed };
89
- } catch {
90
- return { ...EMPTY_GLOBAL };
91
- }
92
- }
93
-
94
- export function saveGlobalEvents(events: GlobalCounters): void {
95
- atomicWrite(EVENTS_FILE, JSON.stringify(events, null, 2));
96
- }
97
-
98
- export function loadSlotEvents(slot: string): SlotCounters {
99
- try {
100
- const parsed = JSON.parse(readFileSync(slotEventsFile(slot), "utf8"));
101
- return { ...EMPTY_SLOT, ...parsed };
102
- } catch {
103
- return { ...EMPTY_SLOT };
104
- }
105
- }
106
-
107
- export function saveSlotEvents(slot: string, events: SlotCounters): void {
108
- atomicWrite(slotEventsFile(slot), JSON.stringify(events, null, 2));
109
- }
110
-
111
- export function loadEvents(slot?: string): EventCounters {
112
- const global = loadGlobalEvents();
113
- if (!slot) {
114
- return { ...global, pets: 0, reactions_given: 0 };
115
- }
116
- const slotEvents = loadSlotEvents(slot);
117
- return {
118
- ...global,
119
- pets: slotEvents.pets,
120
- reactions_given: slotEvents.reactions_given,
121
- };
122
- }
123
-
124
- export function incrementEvent(key: keyof EventCounters, amount: number = 1, slot?: string): EventCounters {
125
- if ((SLOT_KEYS as string[]).includes(key) && slot) {
126
- const slotEvents = loadSlotEvents(slot);
127
- (slotEvents as any)[key] += amount;
128
- saveSlotEvents(slot, slotEvents);
129
- } else {
130
- const global = loadGlobalEvents();
131
- if ((GLOBAL_KEYS as string[]).includes(key)) {
132
- (global as any)[key] += amount;
133
- }
134
- saveGlobalEvents(global);
135
- }
136
- return loadEvents(slot);
137
- }
138
-
139
- // ─── Backward-compatible overloads ────────────────────────────────────────────
140
- // The shell hooks (react.sh) write directly to events.json for global counters
141
- // like errors_seen and tests_failed. The loadEvents() and saveEvents() names
142
- // below maintain that compatibility.
143
-
144
- export { loadEvents as loadGlobalEventsCompat, loadGlobalEvents as loadGlobalEventsDirect };
145
-
146
- // ─── Day tracking ────────────────────────────────────────────────────────────
147
-
148
- interface DayTracker {
149
- lastDate: string;
150
- totalDays: number;
151
- }
152
-
153
- export function trackActiveDay(): void {
154
- const today = new Date().toISOString().slice(0, 10);
155
- let tracker: DayTracker;
156
- try {
157
- tracker = JSON.parse(readFileSync(DAYS_FILE, "utf8"));
158
- } catch {
159
- tracker = { lastDate: "", totalDays: 0 };
160
- }
161
- if (tracker.lastDate === today) return;
162
-
163
- tracker.lastDate = today;
164
- tracker.totalDays += 1;
165
- atomicWrite(DAYS_FILE, JSON.stringify(tracker, null, 2));
166
-
167
- const events = loadGlobalEvents();
168
- events.days_active = tracker.totalDays;
169
- saveGlobalEvents(events);
170
- }
171
-
172
- // ─── Achievement definitions ─────────────────────────────────────────────────
173
-
174
- export interface Achievement {
175
- id: string;
176
- name: string;
177
- description: string;
178
- icon: string;
179
- check: (events: EventCounters) => boolean;
180
- secret: boolean;
181
- }
182
-
183
- export const ACHIEVEMENTS: Achievement[] = [
184
- {
185
- id: "first_steps",
186
- name: "First Steps",
187
- description: "Hatch your buddy for the first time",
188
- icon: "\ud83c\udf1f",
189
- check: () => true,
190
- secret: false,
191
- },
192
- {
193
- id: "good_boy",
194
- name: "Good Buddy",
195
- description: "Pet your companion 10 times",
196
- icon: "\ud83e\uddf9",
197
- check: (e) => e.pets >= 10,
198
- secret: false,
199
- },
200
- {
201
- id: "best_friend",
202
- name: "Best Friend",
203
- description: "Pet your companion 50 times",
204
- icon: "\u2764\ufe0f",
205
- check: (e) => e.pets >= 50,
206
- secret: false,
207
- },
208
- {
209
- id: "bug_spotter",
210
- name: "Bug Spotter",
211
- description: "Witness your first error together",
212
- icon: "\ud83d\udc1b",
213
- check: (e) => e.errors_seen >= 1,
214
- secret: false,
215
- },
216
- {
217
- id: "error_whisperer",
218
- name: "Error Whisperer",
219
- description: "Survive 25 errors as a team",
220
- icon: "\ud83d\udd27",
221
- check: (e) => e.errors_seen >= 25,
222
- secret: false,
223
- },
224
- {
225
- id: "battle_scarred",
226
- name: "Battle-Scarred",
227
- description: "Survive 100 errors together",
228
- icon: "\ud83d\udc80",
229
- check: (e) => e.errors_seen >= 100,
230
- secret: true,
231
- },
232
- {
233
- id: "test_witness",
234
- name: "Test Witness",
235
- description: "See your first test failure",
236
- icon: "\u274c",
237
- check: (e) => e.tests_failed >= 1,
238
- secret: false,
239
- },
240
- {
241
- id: "test_veteran",
242
- name: "Test Veteran",
243
- description: "Witness 50 test failures",
244
- icon: "\ud83d\udcca",
245
- check: (e) => e.tests_failed >= 50,
246
- secret: false,
247
- },
248
- {
249
- id: "big_mover",
250
- name: "Big Mover",
251
- description: "Make a diff with 80+ lines",
252
- icon: "\ud83d\udce6",
253
- check: (e) => e.large_diffs >= 1,
254
- secret: false,
255
- },
256
- {
257
- id: "refactor_machine",
258
- name: "Refactor Machine",
259
- description: "Make 10 large diffs",
260
- icon: "\ud83d\udd28",
261
- check: (e) => e.large_diffs >= 10,
262
- secret: false,
263
- },
264
- {
265
- id: "chatterbox",
266
- name: "Chatterbox",
267
- description: "Your buddy reacts 100 times",
268
- icon: "\ud83d\udcac",
269
- check: (e) => e.reactions_given >= 100,
270
- secret: false,
271
- },
272
- {
273
- id: "week_streak",
274
- name: "Week Streak",
275
- description: "Code with your buddy for 7 days",
276
- icon: "\ud83d\udd25",
277
- check: (e) => e.days_active >= 7,
278
- secret: false,
279
- },
280
- {
281
- id: "month_streak",
282
- name: "Month Streak",
283
- description: "Code with your buddy for 30 days",
284
- icon: "\ud83d\udc51",
285
- check: (e) => e.days_active >= 30,
286
- secret: true,
287
- },
288
- {
289
- id: "power_user",
290
- name: "Power User",
291
- description: "Run 50 buddy commands",
292
- icon: "\u26a1",
293
- check: (e) => e.commands_run >= 50,
294
- secret: false,
295
- },
296
- {
297
- id: "dedicated",
298
- name: "Dedicated Companion",
299
- description: "Complete 200 turns together",
300
- icon: "\ud83c\udfc5",
301
- check: (e) => e.turns >= 200,
302
- secret: false,
303
- },
304
- {
305
- id: "thousand_turns",
306
- name: "Thousand Turns",
307
- description: "Reach 1000 turns together",
308
- icon: "\ud83c\udf96",
309
- check: (e) => e.turns >= 1000,
310
- secret: true,
311
- },
312
- ];
313
-
314
- // ─── Unlocked badges persistence ─────────────────────────────────────────────
315
-
316
- export interface UnlockedAchievement {
317
- id: string;
318
- unlockedAt: number;
319
- slot?: string;
320
- }
321
-
322
- export function loadUnlocked(): UnlockedAchievement[] {
323
- try {
324
- return JSON.parse(readFileSync(UNLOCKED_FILE, "utf8"));
325
- } catch {
326
- return [];
327
- }
328
- }
329
-
330
- export function saveUnlocked(unlocked: UnlockedAchievement[]): void {
331
- atomicWrite(UNLOCKED_FILE, JSON.stringify(unlocked, null, 2));
332
- }
333
-
334
- // ─── Check + award ───────────────────────────────────────────────────────────
335
-
336
- export function checkAndAward(slot?: string): Achievement[] {
337
- const e = loadEvents(slot);
338
- const unlocked = loadUnlocked();
339
- const unlockedIds = new Set(unlocked.map((u) => u.id));
340
-
341
- const newlyUnlocked: Achievement[] = [];
342
-
343
- for (const ach of ACHIEVEMENTS) {
344
- if (unlockedIds.has(ach.id)) continue;
345
- if (ach.check(e)) {
346
- unlocked.push({ id: ach.id, unlockedAt: Date.now(), slot: slot ?? undefined });
347
- newlyUnlocked.push(ach);
348
- }
349
- }
350
-
351
- if (newlyUnlocked.length > 0) {
352
- saveUnlocked(unlocked);
353
- }
354
-
355
- return newlyUnlocked;
356
- }
357
-
358
- // ─── Render achievement card ─────────────────────────────────────────────────
359
-
360
- const GOLD = "\x1b[38;2;255;193;7m";
361
- const NC = "\x1b[0m";
362
- const BOLD = "\x1b[1m";
363
- const DIM = "\x1b[2m";
364
-
365
- export function renderAchievementsCard(): string {
366
- const unlocked = loadUnlocked();
367
- const unlockedIds = new Set(unlocked.map((u) => u.id));
368
-
369
- const W = 40;
370
- const hr = "\u2500".repeat(W - 2);
371
- const sep = `\u251c${"\u254c".repeat(W - 2)}\u2524`;
372
- const lines: string[] = [];
373
-
374
- const total = ACHIEVEMENTS.length;
375
- const earned = unlockedIds.size;
376
-
377
- lines.push(`${GOLD}\u256d${hr}\u256e${NC}`);
378
-
379
- const header = "\ud83c\udfc6 ACHIEVEMENTS";
380
- lines.push(`${GOLD}\u2502${NC} ${BOLD}${header}${NC}${"".padEnd(W - header.length - 4)}${GOLD}\u2502${NC}`);
381
-
382
- const barFilled = total > 0 ? Math.round((earned / total) * 20) : 0;
383
- const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(20 - barFilled);
384
- const barText = `${bar} ${earned}/${total}`;
385
- lines.push(`${GOLD}\u2502${NC} ${barText}${"".padEnd(W - barText.length - 4)}${GOLD}\u2502${NC}`);
386
-
387
- lines.push(`${GOLD}${sep}${NC}`);
388
-
389
- for (const ach of ACHIEVEMENTS) {
390
- if (ach.secret && !unlockedIds.has(ach.id)) continue;
391
-
392
- const done = unlockedIds.has(ach.id);
393
- const status = done ? "\u2705" : "\u2610";
394
- const content = ` ${ach.icon}${status} ${ach.name}`;
395
- const descContent = ` ${ach.description}`;
396
-
397
- if (done) {
398
- lines.push(`${GOLD}\u2502${NC} ${BOLD}${content}${NC}${"".padEnd(W - content.length - 3)}${GOLD}\u2502${NC}`);
399
- } else {
400
- lines.push(`${GOLD}\u2502${NC} ${DIM}${content}${NC}${"".padEnd(W - content.length - 3)}${GOLD}\u2502${NC}`);
401
- }
402
- lines.push(`${GOLD}\u2502${NC} ${DIM}${descContent}${NC}${"".padEnd(W - descContent.length - 3)}${GOLD}\u2502${NC}`);
403
- }
404
-
405
- if (earned > 0 && earned === ACHIEVEMENTS.length) {
406
- lines.push(`${GOLD}${sep}${NC}`);
407
- const complete = "\u2728 ALL ACHIEVEMENTS UNLOCKED! \u2728";
408
- lines.push(`${GOLD}\u2502${NC} ${BOLD}${complete}${NC}${"".padEnd(W - complete.length - 4)}${GOLD}\u2502${NC}`);
409
- }
410
-
411
- lines.push(`${GOLD}\u2570${hr}\u256f${NC}`);
412
-
413
- return lines.join("\n");
414
- }
415
-
416
- export function renderAchievementsCardMarkdown(): string {
417
- const unlocked = loadUnlocked();
418
- const unlockedIds = new Set(unlocked.map((u) => u.id));
419
- const total = ACHIEVEMENTS.length;
420
- const earned = unlockedIds.size;
421
-
422
- const barFilled = total > 0 ? Math.round((earned / total) * 20) : 0;
423
- const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(20 - barFilled);
424
-
425
- const parts: string[] = [];
426
- parts.push(`### \ud83c\udfc6 Achievements \u2014 ${earned}/${total}`);
427
- parts.push("");
428
- parts.push(`\`${bar}\``);
429
- parts.push("");
430
-
431
- for (const ach of ACHIEVEMENTS) {
432
- if (ach.secret && !unlockedIds.has(ach.id)) continue;
433
- const done = unlockedIds.has(ach.id);
434
- const status = done ? "\u2705" : "\u2610";
435
- const line = `${ach.icon}${status} **${ach.name}** \u2014 ${ach.description}`;
436
- parts.push(line);
437
- }
438
-
439
- if (earned > 0 && earned === ACHIEVEMENTS.length) {
440
- parts.push("");
441
- parts.push("\u2728 **ALL ACHIEVEMENTS UNLOCKED!** \u2728");
442
- }
443
-
444
- return parts.join("\n");
445
- }
File without changes
File without changes
File without changes
File without changes