@ramarivera/coding-buddy 0.4.0-alpha.8 → 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 +9 -7
  3. package/LICENSE +1 -1
  4. package/README.md +102 -72
  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 +59 -44
  22. package/cli/biomes.ts +309 -0
  23. package/cli/buddy-shell.ts +818 -0
  24. package/{adapters/claude/install → cli}/disable.ts +14 -32
  25. package/{adapters/claude/install → cli}/doctor.ts +55 -57
  26. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  27. package/cli/index.ts +36 -25
  28. package/{adapters/claude/install → cli}/install.ts +100 -145
  29. package/{adapters/claude/install → cli}/pick.ts +111 -50
  30. package/{adapters/claude/install → cli}/settings.ts +2 -2
  31. package/{adapters/claude/install → cli}/show.ts +4 -4
  32. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  33. package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
  34. package/cli/tui.tsx +2333 -0
  35. package/{adapters/claude/install → cli}/uninstall.ts +23 -38
  36. package/cli/upgrade.ts +213 -0
  37. package/cli/validate-species.ts +209 -0
  38. package/cli/verify.ts +3 -3
  39. package/core/achievements.ts +12 -20
  40. package/core/art-data.ts +26 -0
  41. package/core/command-service.ts +99 -97
  42. package/core/engine.ts +83 -1
  43. package/core/ports.ts +10 -0
  44. package/core/reactions.ts +533 -95
  45. package/core/slot-slug.ts +22 -0
  46. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
  47. package/hooks/file-type-react.sh +401 -0
  48. package/hooks/hooks.json +52 -0
  49. package/hooks/mood-react.sh +208 -0
  50. package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
  51. package/hooks/react.sh +1368 -0
  52. package/hooks/suggest.sh +55 -0
  53. package/package.json +113 -59
  54. package/scripts/gen-emoji-widths.ts +34 -0
  55. package/scripts/paths.sh +56 -0
  56. package/scripts/postinstall.cjs +42 -0
  57. package/scripts/snapshot-statusline.sh +54 -0
  58. package/server/achievements.ts +1419 -0
  59. package/server/art.ts +450 -0
  60. package/server/award-xp.ts +48 -0
  61. package/server/check-suggestions.ts +102 -0
  62. package/server/consolidate.ts +33 -0
  63. package/server/index.ts +1340 -0
  64. package/server/mcp-launcher.sh +26 -0
  65. package/server/memory.ts +404 -0
  66. package/server/mood.ts +254 -0
  67. package/server/path.ts +82 -0
  68. package/server/shift-mood.ts +43 -0
  69. package/{adapters/claude/storage → server}/state.ts +200 -7
  70. package/server/suggestions.ts +335 -0
  71. package/server/theme.ts +53 -0
  72. package/server/xp.ts +435 -0
  73. package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
  74. package/statusline/buddy-status.sh +397 -0
  75. package/statusline/combined-status.sh +134 -0
  76. package/statusline/emoji-widths.data +4 -0
  77. package/adapters/claude/hooks/hooks.json +0 -35
  78. package/adapters/claude/hooks/react.sh +0 -204
  79. package/adapters/claude/popup/buddy-popup.sh +0 -92
  80. package/adapters/claude/popup/buddy-render.sh +0 -540
  81. package/adapters/claude/popup/popup-manager.sh +0 -355
  82. package/adapters/claude/rendering/art.ts +0 -264
  83. package/adapters/claude/server/index.ts +0 -752
  84. package/adapters/claude/server/instructions.ts +0 -24
  85. package/adapters/claude/server/resources.ts +0 -38
  86. package/adapters/claude/statusline/buddy-status.sh +0 -389
  87. package/adapters/claude/storage/achievements.ts +0 -253
  88. package/adapters/claude/storage/identity.ts +0 -13
  89. package/adapters/claude/storage/paths.ts +0 -24
  90. package/adapters/claude/storage/settings.ts +0 -41
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
@@ -1,10 +1,19 @@
1
+ export function mergeAchievements(first: Achievement[], second: Achievement[]): Achievement[] {
2
+ const merged = new Map<string, Achievement>();
3
+ for (const achievement of [...first, ...second]) {
4
+ merged.set(achievement.id, achievement);
5
+ }
6
+ return [...merged.values()];
7
+ }
8
+
1
9
  import {
2
10
  generateBones,
3
11
  RARITY_STARS,
4
12
  type Companion,
5
13
  } from "./engine.ts";
6
- import { getUnlockedAchievements, type Achievement } from "./achievements.ts";
14
+ import { ACHIEVEMENTS, getCreationAchievements, getUnlockedAchievements, type Achievement } from "./achievements.ts";
7
15
  import { generateFallbackName, getReaction } from "./reactions.ts";
16
+ import { slugifySlot } from "./slot-slug.ts";
8
17
  import type {
9
18
  BuddyConfig,
10
19
  ReactionState,
@@ -86,40 +95,27 @@ export class BuddyCommandService {
86
95
  }
87
96
 
88
97
  ensureCompanion(): EnsureCompanionResult {
89
- const active = this.deps.buddies.loadActive();
90
- if (active) {
91
- return {
92
- companion: active,
93
- slot: this.getActiveSlot(),
94
- created: false,
95
- achievements: [],
96
- };
97
- }
98
-
99
- const saved = this.deps.buddies.listSlots();
100
- if (saved.length > 0) {
101
- const rescued = saved[0];
102
- this.deps.buddies.saveActiveSlot(rescued.slot);
103
- return {
104
- companion: rescued.companion,
105
- slot: rescued.slot,
106
- created: false,
107
- achievements: [],
108
- };
109
- }
110
-
111
98
  const userId = this.getStableUserId();
112
- const companion = this.createCompanion(userId);
113
- const slot = slugify(companion.name);
114
- this.deps.buddies.saveSlot(slot, companion);
115
- this.deps.buddies.saveActiveSlot(slot);
99
+ const { companion, slot, created } = this.deps.buddies.ensureCompanion(
100
+ userId,
101
+ (existingSlots) => this.createCompanion(userId, existingSlots),
102
+ );
103
+ const achievements = created
104
+ ? this.deps.events.unlockAchievements(slot, getCreationAchievements)
105
+ : [];
106
+ return { companion, slot, created, achievements };
107
+ }
108
+
109
+ startSession(): EnsureCompanionResult {
110
+ const result = this.ensureCompanion();
116
111
  this.deps.events.trackActiveDay();
117
112
  this.deps.events.increment("sessions", 1);
118
- const achievements = this.unlockAchievements(slot);
113
+ const sessionAchievements = this.unlockNewAchievements(result.slot);
119
114
 
120
- return { companion, slot, created: true, achievements };
115
+ return { ...result, achievements: mergeAchievements(result.achievements, sessionAchievements) };
121
116
  }
122
117
 
118
+
123
119
  petBuddy(scope?: string): ReactionResult {
124
120
  const { companion, slot } = this.ensureCompanion();
125
121
  this.deps.events.increment("pets", 1, slot);
@@ -129,31 +125,45 @@ export class BuddyCommandService {
129
125
  renameBuddy(name: string): Companion {
130
126
  const trimmed = name.trim();
131
127
  if (!trimmed) throw new Error("Buddy name cannot be empty.");
132
- const { companion } = this.ensureCompanion();
133
- const updated: Companion = { ...companion, name: trimmed.slice(0, 14) };
134
- this.deps.buddies.saveActive(updated);
135
- return updated;
128
+ this.ensureCompanion();
129
+ return this.deps.buddies.updateActive((companion) => ({
130
+ ...companion,
131
+ name: trimmed.slice(0, 14),
132
+ }));
136
133
  }
137
134
 
138
135
  setPersonality(personality: string): Companion {
139
136
  const trimmed = personality.trim();
140
137
  if (!trimmed) throw new Error("Buddy personality cannot be empty.");
141
- const { companion } = this.ensureCompanion();
142
- const updated: Companion = { ...companion, personality: trimmed };
143
- this.deps.buddies.saveActive(updated);
144
- return updated;
138
+ this.ensureCompanion();
139
+ return this.deps.buddies.updateActive((companion) => ({
140
+ ...companion,
141
+ personality: trimmed,
142
+ }));
145
143
  }
146
144
 
147
145
  saveBuddy(slot?: string): SaveBuddyResult {
148
- const { companion } = this.ensureCompanion();
149
- const targetSlot = slugify(slot?.trim() || companion.name);
150
- this.deps.buddies.saveSlot(targetSlot, companion);
151
- this.deps.buddies.saveActiveSlot(targetSlot);
152
- return { companion, slot: targetSlot };
146
+ const { companion, slot: activeSlot } = this.ensureCompanion();
147
+ if (slot?.trim()) {
148
+ const targetSlot = slugifySlot(slot.trim());
149
+ if (targetSlot !== activeSlot && this.deps.buddies.loadSlot(targetSlot)) {
150
+ throw new Error(`Slot "${targetSlot}" already exists.`);
151
+ }
152
+ if (targetSlot === activeSlot) {
153
+ this.deps.buddies.saveActive(companion);
154
+ } else {
155
+ this.deps.buddies.saveSlot(targetSlot, companion);
156
+ }
157
+ this.deps.buddies.saveActiveSlot(targetSlot);
158
+ return { companion, slot: targetSlot };
159
+ }
160
+
161
+ this.deps.buddies.saveActive(companion);
162
+ return { companion, slot: activeSlot };
153
163
  }
154
164
 
155
165
  summonBuddy(slot?: string): SummonBuddyResult | null {
156
- const targetSlot = slot ? slugify(slot) : undefined;
166
+ const targetSlot = slot ? slugifySlot(slot) : undefined;
157
167
 
158
168
  if (!targetSlot) {
159
169
  const companions = this.deps.buddies.listSlots();
@@ -171,7 +181,7 @@ export class BuddyCommandService {
171
181
  }
172
182
 
173
183
  dismissBuddy(slot: string): SaveBuddyResult {
174
- const targetSlot = slugify(slot);
184
+ const targetSlot = slugifySlot(slot);
175
185
  const activeSlot = this.getActiveSlot();
176
186
  if (targetSlot === activeSlot) {
177
187
  throw new Error("Cannot dismiss the active buddy.");
@@ -208,6 +218,23 @@ export class BuddyCommandService {
208
218
  this.deps.events.increment("large_diffs", 1);
209
219
  return this.saveReaction("large-diff", companion, slot, scope, { lines });
210
220
  }
221
+ trackToolError(scope?: string, line?: number): Achievement[] {
222
+ const { slot } = this.ensureCompanion();
223
+ this.deps.events.increment("errors_seen", 1, slot);
224
+ return this.unlockNewAchievements(slot);
225
+ }
226
+
227
+ trackTestFailure(scope?: string, count?: number): Achievement[] {
228
+ const { slot } = this.ensureCompanion();
229
+ this.deps.events.increment("tests_failed", 1, slot);
230
+ return this.unlockNewAchievements(slot);
231
+ }
232
+
233
+ trackLargeDiff(lines: number, scope?: string): Achievement[] {
234
+ const { slot } = this.ensureCompanion();
235
+ this.deps.events.increment("large_diffs", 1, slot);
236
+ return this.unlockNewAchievements(slot);
237
+ }
211
238
 
212
239
  recordTurn(scope?: string): ReactionResult {
213
240
  const { companion, slot } = this.ensureCompanion();
@@ -218,7 +245,7 @@ export class BuddyCommandService {
218
245
  recordTurnOnly(): ProgressResult {
219
246
  const { companion, slot } = this.ensureCompanion();
220
247
  this.deps.events.increment("turns", 1);
221
- const achievements = this.unlockAchievements(slot);
248
+ const achievements = this.unlockNewAchievements(slot);
222
249
  return { companion, slot, achievements };
223
250
  }
224
251
 
@@ -232,10 +259,10 @@ export class BuddyCommandService {
232
259
  const { companion, slot } = this.ensureCompanion();
233
260
  return this.saveCustomReaction(trimmed, reason, companion, slot, scope);
234
261
  }
235
-
236
262
  incrementCommandsRun(): Achievement[] {
237
- this.deps.events.increment("commands_run", 1, this.getActiveSlotOrUndefined());
238
- return this.unlockAchievements(this.getActiveSlotOrUndefined());
263
+ const slot = this.getActiveSlotOrUndefined();
264
+ this.deps.events.increment("commands_run", 1, slot);
265
+ return this.unlockNewAchievements(slot);
239
266
  }
240
267
 
241
268
  getAchievementProgress(slot?: string): {
@@ -245,15 +272,10 @@ export class BuddyCommandService {
245
272
  const unlocked = this.deps.events.loadUnlocked();
246
273
  const unlockedMap = new Map(unlocked.map((entry) => [entry.id, entry]));
247
274
 
248
- const unlockedAchievements = getUnlockedAchievements(
249
- this.deps.events.loadCounters(slot),
250
- new Set<string>(),
251
- ).filter((achievement) => unlockedMap.has(achievement.id));
252
-
253
- const remaining = getUnlockedAchievements(
254
- this.deps.events.loadCounters(slot),
255
- new Set(unlocked.map((entry) => entry.id)),
275
+ const unlockedAchievements = ACHIEVEMENTS.filter((achievement) =>
276
+ unlockedMap.has(achievement.id)
256
277
  );
278
+ const remaining = ACHIEVEMENTS.filter((achievement) => !unlockedMap.has(achievement.id));
257
279
 
258
280
  return {
259
281
  unlocked: unlockedAchievements.map((achievement) => ({
@@ -279,23 +301,30 @@ export class BuddyCommandService {
279
301
  return slot || undefined;
280
302
  }
281
303
 
282
- private createCompanion(userId: string): Companion {
304
+ private createCompanion(
305
+ userId: string,
306
+ existingSlots: string[],
307
+ ): { companion: Companion; slot: string } {
283
308
  const bones = generateBones(userId);
284
- const name = this.generateUnusedName();
309
+ const name = this.generateUnusedName(existingSlots);
310
+ const slot = slugifySlot(name);
285
311
  return {
286
- bones,
287
- name,
288
- personality: `A ${bones.rarity} ${bones.species} who watches code with quiet intensity.`,
289
- hatchedAt: Date.now(),
290
- userId,
312
+ companion: {
313
+ bones,
314
+ name,
315
+ personality: `A ${bones.rarity} ${bones.species} who watches code with quiet intensity.`,
316
+ hatchedAt: Date.now(),
317
+ userId,
318
+ },
319
+ slot,
291
320
  };
292
321
  }
293
322
 
294
- private generateUnusedName(): string {
295
- const taken = new Set(this.deps.buddies.listSlots().map(({ slot }) => slot));
323
+ private generateUnusedName(existingSlots: string[]): string {
324
+ const taken = new Set(existingSlots);
296
325
  for (let i = 0; i < 50; i++) {
297
326
  const candidate = generateFallbackName();
298
- if (!taken.has(slugify(candidate))) return candidate;
327
+ if (!taken.has(slugifySlot(candidate))) return candidate;
299
328
  }
300
329
 
301
330
  let suffix = 0;
@@ -314,6 +343,7 @@ export class BuddyCommandService {
314
343
  reason,
315
344
  companion.bones.species,
316
345
  companion.bones.rarity,
346
+ companion.bones.stats,
317
347
  context,
318
348
  );
319
349
  return this.saveCustomReaction(reaction, reason, companion, slot, scope);
@@ -333,40 +363,12 @@ export class BuddyCommandService {
333
363
  };
334
364
  this.deps.reactions.saveLatest(state, scope);
335
365
  this.deps.events.increment("reactions_given", 1, slot);
336
- const achievements = this.unlockAchievements(slot);
366
+ const achievements = this.unlockNewAchievements(slot);
337
367
  return { companion, slot, reaction, state, achievements };
338
368
  }
339
369
 
340
- private unlockAchievements(slot?: string): Achievement[] {
341
- const unlocked = this.deps.events.loadUnlocked();
342
- const unlockedIds = new Set(unlocked.map((entry) => entry.id));
343
- const newlyUnlocked = getUnlockedAchievements(
344
- this.deps.events.loadCounters(slot),
345
- unlockedIds,
346
- );
347
-
348
- if (newlyUnlocked.length === 0) return [];
349
-
350
- this.deps.events.saveUnlocked([
351
- ...unlocked,
352
- ...newlyUnlocked.map((achievement) => ({
353
- id: achievement.id,
354
- unlockedAt: Date.now(),
355
- slot,
356
- })),
357
- ]);
358
-
359
- return newlyUnlocked;
370
+ private unlockNewAchievements(slot?: string): Achievement[] {
371
+ return this.deps.events.unlockAchievements(slot, getUnlockedAchievements);
360
372
  }
361
- }
362
373
 
363
- function slugify(name: string): string {
364
- return (
365
- name
366
- .toLowerCase()
367
- .replace(/[^a-z0-9-]/g, "-")
368
- .replace(/-+/g, "-")
369
- .replace(/^-|-$/g, "")
370
- .slice(0, 14) || "buddy"
371
- );
372
374
  }
package/core/engine.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * claude-buddy engine — deterministic companion generation
2
+ * coding-buddy engine — deterministic companion generation
3
3
  * Matches Claude Code's exact algorithm: wyhash → mulberry32 → species/stats
4
4
  */
5
5
 
@@ -24,6 +24,8 @@ export const SPECIES = [
24
24
  "rabbit",
25
25
  "mushroom",
26
26
  "chonk",
27
+ "wyvern",
28
+ "pikachu",
27
29
  ] as const;
28
30
 
29
31
  export type Species = (typeof SPECIES)[number];
@@ -296,6 +298,84 @@ export function generateBones(userId: string, salt: string = SALT): BuddyBones {
296
298
  return { rarity, species, eye, hat, shiny, stats, peak, dump };
297
299
  }
298
300
 
301
+ // ─── Personality ────────────────────────────────────────────────────────────
302
+
303
+ const PEAK_PHRASES: Record<StatName, string[]> = {
304
+ DEBUGGING: [
305
+ "spots segfaults before the stack unwinds",
306
+ "can read a backtrace like a map",
307
+ "finds the off-by-one before the tests do",
308
+ ],
309
+ PATIENCE: [
310
+ "outlasts any flaky test suite",
311
+ "waits for the slow CI build without complaint",
312
+ "never merges before the green check",
313
+ ],
314
+ CHAOS: [
315
+ "treats breaking changes as a love language",
316
+ "rewrites the Makefile on a Tuesday for fun",
317
+ "thrives wherever the incident channel is loudest",
318
+ ],
319
+ WISDOM: [
320
+ "has seen this bug before — in three other repos",
321
+ "quotes the relevant RFC from memory",
322
+ "recognizes the abstraction that will outlive its author",
323
+ ],
324
+ SNARK: [
325
+ "leaves code review comments that linger",
326
+ "finds the edge case you forgot and mentions it twice",
327
+ "names things with uncomfortable accuracy",
328
+ ],
329
+ };
330
+
331
+ const DUMP_PHRASES: Record<StatName, string[]> = {
332
+ DEBUGGING: [
333
+ "occasionally ships the workaround instead of the fix",
334
+ "skips the repro step",
335
+ "trusts the logs a little too much",
336
+ ],
337
+ PATIENCE: [
338
+ "starts the rebase before the review is done",
339
+ "has been known to close slow issues as stale",
340
+ "has been known to force-push main",
341
+ ],
342
+ CHAOS: [
343
+ "prefers everything to stay exactly where it is",
344
+ "dislikes surprise refactors",
345
+ "writes very thorough migration guides",
346
+ ],
347
+ WISDOM: [
348
+ "sometimes reinvents the wheel with enthusiasm",
349
+ "skips the existing prior art",
350
+ "learns by doing, not by reading",
351
+ ],
352
+ SNARK: [
353
+ "only leaves encouraging comments",
354
+ "approves PRs with genuine warmth",
355
+ "never says what it actually thinks",
356
+ ],
357
+ };
358
+
359
+ const RARITY_CLOSER: Record<string, string[]> = {
360
+ common: ["Gets the job done.", "Reliable, if unassuming."],
361
+ uncommon: ["Has a few tricks up its sleeve.", "Worth keeping around."],
362
+ rare: ["Not to be underestimated.", "Earns its keep."],
363
+ epic: ["Commands quiet respect.", "The kind of companion repos are built around."],
364
+ legendary: ["The kind you find once, if you're lucky.", "Leaves every codebase better than it found it."],
365
+ };
366
+
367
+ export function generatePersonality(bones: BuddyBones, userId: string): string {
368
+ const seed = parseInt(userId.slice(0, 8), 16);
369
+ const pickPhrase = <T>(arr: T[], salt: number = 0): T => arr[(seed + salt) % arr.length];
370
+
371
+ const shiny = bones.shiny ? " Shimmers faintly in dark mode." : "";
372
+ const peak = pickPhrase(PEAK_PHRASES[bones.peak]);
373
+ const dump = pickPhrase(DUMP_PHRASES[bones.dump], 1);
374
+ const closer = pickPhrase(RARITY_CLOSER[bones.rarity] ?? ["Gets the job done."], 2);
375
+
376
+ return `A ${bones.rarity} ${bones.species} that ${peak}.${shiny} ${closer} Though it ${dump}.`;
377
+ }
378
+
299
379
  // ─── ASCII Art ──────────────────────────────────────────────────────────────
300
380
 
301
381
  const FACE_TEMPLATES: Record<Species, string> = {
@@ -317,6 +397,8 @@ const FACE_TEMPLATES: Record<Species, string> = {
317
397
  rabbit: "({E}..{E})",
318
398
  mushroom: "|{E} {E}|",
319
399
  chonk: "({E}.{E})",
400
+ wyvern: "\\ {E}' '{E} /",
401
+ pikachu: "({E}ω{E})",
320
402
  };
321
403
 
322
404
  export function renderFace(species: Species, eye: Eye): string {
package/core/ports.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  ReactionState,
6
6
  UnlockedAchievement,
7
7
  } from "./model.ts";
8
+ import type { Achievement } from "./achievements.ts";
8
9
 
9
10
  export interface IdentityProvider {
10
11
  getStableUserId(): string;
@@ -13,12 +14,17 @@ export interface IdentityProvider {
13
14
  export interface BuddyRepository {
14
15
  loadActive(): Companion | null;
15
16
  saveActive(companion: Companion): void;
17
+ updateActive(transform: (companion: Companion) => Companion): Companion;
16
18
  loadSlot(slot: string): Companion | null;
17
19
  saveSlot(slot: string, companion: Companion): void;
18
20
  deleteSlot(slot: string): void;
19
21
  listSlots(): Array<{ slot: string; companion: Companion }>;
20
22
  loadActiveSlot(): string | null;
21
23
  saveActiveSlot(slot: string): void;
24
+ ensureCompanion(
25
+ userId: string,
26
+ create: (existingSlots: string[]) => { companion: Companion; slot: string },
27
+ ): { companion: Companion; slot: string; created: boolean };
22
28
  }
23
29
 
24
30
  export interface ReactionRepository {
@@ -37,4 +43,8 @@ export interface BuddyEventRepository {
37
43
  loadUnlocked(): UnlockedAchievement[];
38
44
  saveUnlocked(unlocked: UnlockedAchievement[]): void;
39
45
  trackActiveDay(): void;
46
+ unlockAchievements(
47
+ slot: string | undefined,
48
+ evaluate: (counters: EventCounters, unlockedIds: Set<string>) => Achievement[],
49
+ ): Achievement[];
40
50
  }