@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.
Files changed (94) hide show
  1. package/.claude-plugin/marketplace.json +45 -0
  2. package/.claude-plugin/plugin.json +30 -0
  3. package/LICENSE +1 -1
  4. package/README.md +96 -45
  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 +136 -57
  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 +46 -13
  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 +22 -9
  46. package/hooks/file-type-react.sh +403 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +210 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
  50. package/hooks/react.sh +1364 -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 +33 -2
  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 +552 -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 +402 -0
  74. package/statusline/combined-status.sh +135 -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/plugin/marketplace.json +0 -40
  80. package/adapters/claude/plugin/plugin.json +0 -26
  81. package/adapters/claude/rendering/art.ts +0 -264
  82. package/adapters/claude/server/instructions.ts +0 -24
  83. package/adapters/claude/server/path.ts +0 -12
  84. package/adapters/claude/server/resources.ts +0 -38
  85. package/adapters/claude/statusline/buddy-status.sh +0 -408
  86. package/adapters/claude/storage/achievements.ts +0 -253
  87. package/adapters/claude/storage/identity.ts +0 -13
  88. package/adapters/claude/storage/paths.ts +0 -33
  89. package/adapters/claude/storage/settings.ts +0 -91
  90. package/adapters/claude/storage/state.ts +0 -365
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
  93. package/server/engine.ts +0 -1
  94. package/server/reactions.ts +0 -1
@@ -0,0 +1,526 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ rmdirSync,
8
+ rmSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import {
15
+ EMPTY_GLOBAL,
16
+ EMPTY_SLOT,
17
+ GLOBAL_KEYS,
18
+ SLOT_KEYS,
19
+ type Achievement,
20
+ type GlobalCounters,
21
+ type SlotCounters,
22
+ } from "../../core/achievements.ts";
23
+ import type {
24
+ BuddyConfig,
25
+ Companion,
26
+ EventCounters,
27
+ ReactionState,
28
+ UnlockedAchievement,
29
+ } from "../../core/model.ts";
30
+ import type {
31
+ BuddyConfigRepository,
32
+ BuddyEventRepository,
33
+ BuddyRepository,
34
+ ReactionRepository,
35
+ } from "../../core/ports.ts";
36
+
37
+ export interface FileBuddyConfig extends BuddyConfig {
38
+ muted: boolean;
39
+ }
40
+
41
+ interface MenagerieManifest {
42
+ active: string;
43
+ companions: Record<string, Companion>;
44
+ }
45
+
46
+ interface ActiveDayState {
47
+ lastDate: string;
48
+ totalDays: number;
49
+ }
50
+
51
+ export const DEFAULT_FILE_BUDDY_CONFIG: FileBuddyConfig = {
52
+ commentCooldown: 30,
53
+ reactionTTL: 0,
54
+ bubbleStyle: "classic",
55
+ bubblePosition: "top",
56
+ showRarity: true,
57
+ statusLineEnabled: true,
58
+ turnCommentModel: undefined,
59
+ muted: false,
60
+ };
61
+
62
+ function isSlotKey(key: keyof EventCounters): key is keyof SlotCounters {
63
+ return (SLOT_KEYS as readonly string[]).includes(key);
64
+ }
65
+
66
+ function isGlobalKey(key: keyof EventCounters): key is keyof GlobalCounters {
67
+ return (GLOBAL_KEYS as readonly string[]).includes(key);
68
+ }
69
+ function isNodeError(value: unknown): value is NodeJS.ErrnoException {
70
+ return (
71
+ typeof value === "object" &&
72
+ value !== null &&
73
+ "code" in value &&
74
+ typeof value.code === "string"
75
+ );
76
+ }
77
+ function sleepMs(ms: number): void {
78
+ const target = Date.now() + ms;
79
+ while (Date.now() < target) {}
80
+ }
81
+ const LOCK_STALE_MS = 3000;
82
+
83
+ class LockOwnershipError extends Error {
84
+ constructor(message: string) {
85
+ super(message);
86
+ this.name = "LockOwnershipError";
87
+ }
88
+ }
89
+
90
+ function parseOwner(content: string): { pid?: number; token?: string } {
91
+ const raw = content.trim();
92
+ if (!raw) return {};
93
+ try {
94
+ const parsed = JSON.parse(raw);
95
+ if (typeof parsed === "object" && parsed !== null) {
96
+ return {
97
+ pid: Number(parsed.pid),
98
+ token: typeof parsed.token === "string" ? parsed.token : undefined,
99
+ };
100
+ }
101
+ } catch {
102
+ // Legacy plain-number owner file.
103
+ }
104
+ const pid = Number(raw);
105
+ return Number.isFinite(pid) ? { pid } : {};
106
+ }
107
+
108
+ export class FileBuddyStorage
109
+ implements BuddyRepository, ReactionRepository, BuddyConfigRepository, BuddyEventRepository
110
+ {
111
+ constructor(
112
+ readonly stateDir: string,
113
+ private readonly defaultConfig: FileBuddyConfig = DEFAULT_FILE_BUDDY_CONFIG,
114
+ ) {}
115
+
116
+ private currentLockToken: string | null = null;
117
+
118
+
119
+ private path(name: string): string {
120
+ return join(this.stateDir, name);
121
+ }
122
+
123
+ private ensureDir(): void {
124
+ if (!existsSync(this.stateDir)) mkdirSync(this.stateDir, { recursive: true });
125
+ }
126
+
127
+ private atomicWrite(path: string, value: string): void {
128
+ this.ensureDir();
129
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
130
+ writeFileSync(temporaryPath, value, "utf8");
131
+ try {
132
+ if (this.currentLockToken) {
133
+ this.assertLockOwner();
134
+ }
135
+ renameSync(temporaryPath, path);
136
+ } catch (error) {
137
+ try {
138
+ rmSync(temporaryPath, { force: true });
139
+ } catch {
140
+ // Ignore cleanup failure.
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ private lockDir(): string {
147
+ return this.path(".lock");
148
+ }
149
+
150
+ private assertLockOwner(): void {
151
+ const token = this.currentLockToken;
152
+ if (!token) {
153
+ throw new LockOwnershipError("No active lock for atomic commit.");
154
+ }
155
+ const ownerPath = join(this.lockDir(), "owner");
156
+ let raw = "";
157
+ try {
158
+ raw = readFileSync(ownerPath, "utf8");
159
+ } catch {
160
+ throw new LockOwnershipError("Lock owner file missing during commit.");
161
+ }
162
+ const owner = parseOwner(raw);
163
+ if (owner.token !== token) {
164
+ throw new LockOwnershipError("Lock token mismatch during commit.");
165
+ }
166
+ }
167
+
168
+ private releaseLockIfOwner(): void {
169
+ const token = this.currentLockToken;
170
+ this.currentLockToken = null;
171
+ if (!token) return;
172
+ const lockDir = this.lockDir();
173
+ const ownerPath = join(lockDir, "owner");
174
+ try {
175
+ const owner = parseOwner(readFileSync(ownerPath, "utf8"));
176
+ if (owner.token !== token) {
177
+ return;
178
+ }
179
+ unlinkSync(ownerPath);
180
+ rmdirSync(lockDir);
181
+ } catch {
182
+ // Lock was already released or stolen; never remove someone else's lock.
183
+ }
184
+ }
185
+
186
+ private tryAcquireLock(): boolean {
187
+ const lockDir = this.lockDir();
188
+ const token = randomUUID();
189
+ try {
190
+ mkdirSync(lockDir, { mode: 0o700 });
191
+ writeFileSync(
192
+ join(lockDir, "owner"),
193
+ JSON.stringify({ pid: process.pid, token }),
194
+ { mode: 0o600 },
195
+ );
196
+ this.currentLockToken = token;
197
+ return true;
198
+ } catch (err) {
199
+ if (!isNodeError(err) || err.code !== "EEXIST") {
200
+ throw err;
201
+ }
202
+
203
+ let info;
204
+ try {
205
+ info = statSync(lockDir);
206
+ } catch {
207
+ return false;
208
+ }
209
+
210
+ if (Date.now() - info.mtime.getTime() <= LOCK_STALE_MS) {
211
+ return false;
212
+ }
213
+
214
+ // The lock directory is stale. Try to atomically rename it into a unique
215
+ // quarantine path. Only one contender can win the rename; the rest see
216
+ // ENOENT and retry mkdir on the now-empty path.
217
+ const quarantine = `${lockDir}.quarantine-${process.pid}-${randomUUID()}`;
218
+ try {
219
+ renameSync(lockDir, quarantine);
220
+ } catch (renameErr) {
221
+ if (isNodeError(renameErr) && renameErr.code === "ENOENT") {
222
+ return false;
223
+ }
224
+ return false;
225
+ }
226
+
227
+ // We won the rename. The quarantine must be our stale lock; remove it
228
+ // and let the outer loop retry mkdir on the vacated lock path.
229
+ try {
230
+ rmSync(quarantine, { recursive: true, force: true });
231
+ } catch {
232
+ // Quarantine may contain leftover files; ignore cleanup failures.
233
+ }
234
+ return false;
235
+ }
236
+ }
237
+
238
+ private withLock<T>(operation: () => T): T {
239
+ this.ensureDir();
240
+ const start = Date.now();
241
+ let lastError: Error | undefined;
242
+
243
+ while (Date.now() - start < 5000) {
244
+ if (this.tryAcquireLock()) {
245
+ try {
246
+ return operation();
247
+ } catch (error) {
248
+ if (error instanceof LockOwnershipError) {
249
+ lastError = error;
250
+ continue;
251
+ }
252
+ throw error;
253
+ } finally {
254
+ this.releaseLockIfOwner();
255
+ }
256
+ }
257
+ sleepMs(5);
258
+ }
259
+
260
+ throw lastError ?? new Error(`Could not acquire buddy lock for ${this.stateDir}`);
261
+ }
262
+
263
+ private readJson<T extends object>(path: string, fallback: T): T {
264
+ try {
265
+ return { ...fallback, ...JSON.parse(readFileSync(path, "utf8")) };
266
+ } catch {
267
+ return { ...fallback };
268
+ }
269
+ }
270
+
271
+ private loadManifest(): MenagerieManifest {
272
+ const manifest = this.readJson<MenagerieManifest>(this.path("menagerie.json"), {
273
+ active: "buddy",
274
+ companions: {},
275
+ });
276
+ if (!manifest.companions) manifest.companions = {};
277
+ if (!manifest.active) manifest.active = Object.keys(manifest.companions)[0] ?? "buddy";
278
+ return manifest;
279
+ }
280
+
281
+ private saveManifest(manifest: MenagerieManifest): void {
282
+ this.atomicWrite(this.path("menagerie.json"), JSON.stringify(manifest, null, 2));
283
+ }
284
+
285
+
286
+ private slotEventsFile(slot: string): string {
287
+ return this.path(`events.${slot}.json`);
288
+ }
289
+
290
+ loadActive(): Companion | null {
291
+ const manifest = this.loadManifest();
292
+ return manifest.companions[manifest.active] ?? null;
293
+ }
294
+
295
+ saveActive(companion: Companion): void {
296
+ this.withLock(() => {
297
+ const manifest = this.loadManifest();
298
+ manifest.companions[manifest.active] = companion;
299
+ this.saveManifest(manifest);
300
+ });
301
+ }
302
+
303
+ updateActive(transform: (companion: Companion) => Companion): Companion {
304
+ return this.withLock(() => {
305
+ const manifest = this.loadManifest();
306
+ const active = manifest.companions[manifest.active];
307
+ if (!active) throw new Error("No active companion.");
308
+ const updated = transform(active);
309
+ manifest.companions[manifest.active] = updated;
310
+ this.saveManifest(manifest);
311
+ return updated;
312
+ });
313
+ }
314
+
315
+ loadSlot(slot: string): Companion | null {
316
+ return this.loadManifest().companions[slot] ?? null;
317
+ }
318
+
319
+ saveSlot(slot: string, companion: Companion): void {
320
+ this.withLock(() => {
321
+ const manifest = this.loadManifest();
322
+ if (manifest.companions[slot]) throw new Error(`Slot "${slot}" already exists.`);
323
+ manifest.companions[slot] = companion;
324
+ this.saveManifest(manifest);
325
+ });
326
+ }
327
+
328
+ deleteSlot(slot: string): void {
329
+ this.withLock(() => {
330
+ const manifest = this.loadManifest();
331
+ delete manifest.companions[slot];
332
+ if (manifest.active === slot) {
333
+ manifest.active = Object.keys(manifest.companions)[0] ?? "buddy";
334
+ }
335
+ this.saveManifest(manifest);
336
+ });
337
+ }
338
+
339
+ listSlots(): Array<{ slot: string; companion: Companion }> {
340
+ return Object.entries(this.loadManifest().companions).map(([slot, companion]) => ({
341
+ slot,
342
+ companion,
343
+ }));
344
+ }
345
+
346
+ loadActiveSlot(): string | null {
347
+ const manifest = this.loadManifest();
348
+ if (manifest.active && manifest.companions[manifest.active]) return manifest.active;
349
+ return Object.keys(manifest.companions)[0] ?? null;
350
+ }
351
+
352
+ saveActiveSlot(slot: string): void {
353
+ this.withLock(() => {
354
+ const manifest = this.loadManifest();
355
+ manifest.active = slot;
356
+ this.saveManifest(manifest);
357
+ });
358
+ }
359
+
360
+ ensureCompanion(
361
+ userId: string,
362
+ create: (existingSlots: string[]) => { companion: Companion; slot: string },
363
+ ): { companion: Companion; slot: string; created: boolean } {
364
+ return this.withLock(() => {
365
+ const manifest = this.loadManifest();
366
+ const active = manifest.companions[manifest.active];
367
+ if (active) {
368
+ return { companion: active, slot: manifest.active, created: false };
369
+ }
370
+
371
+ const slots = Object.entries(manifest.companions);
372
+ if (slots.length > 0) {
373
+ const [slot, companion] = slots[0];
374
+ manifest.active = slot;
375
+ this.saveManifest(manifest);
376
+ return { companion, slot, created: false };
377
+ }
378
+
379
+ const existingSlots = Object.keys(manifest.companions);
380
+ const { companion, slot } = create(existingSlots);
381
+ manifest.companions[slot] = companion;
382
+ manifest.active = slot;
383
+ this.saveManifest(manifest);
384
+ return { companion, slot, created: true };
385
+ });
386
+ }
387
+
388
+ loadLatest(): ReactionState | null {
389
+ try {
390
+ const reaction = JSON.parse(readFileSync(this.path("reaction.json"), "utf8")) as ReactionState;
391
+ const ttl = this.loadHostConfig().reactionTTL;
392
+ if (ttl > 0 && Date.now() - reaction.timestamp > ttl * 1000) return null;
393
+ return reaction;
394
+ } catch {
395
+ return null;
396
+ }
397
+ }
398
+
399
+ saveLatest(reaction: ReactionState): void {
400
+ this.atomicWrite(this.path("reaction.json"), JSON.stringify(reaction, null, 2));
401
+ }
402
+
403
+ clearLatestReaction(): void {
404
+ this.saveLatest({ reaction: "", reason: "clear", timestamp: Date.now() });
405
+ }
406
+
407
+ loadConfig(): BuddyConfig {
408
+ return this.loadHostConfig();
409
+ }
410
+
411
+ saveConfig(config: Partial<BuddyConfig>): BuddyConfig {
412
+ return this.saveHostConfig(config);
413
+ }
414
+
415
+ loadHostConfig(): FileBuddyConfig {
416
+ return this.readJson<FileBuddyConfig>(this.path("config.json"), this.defaultConfig);
417
+ }
418
+
419
+ saveHostConfig(config: Partial<FileBuddyConfig>): FileBuddyConfig {
420
+ const merged = { ...this.loadHostConfig(), ...config };
421
+ this.atomicWrite(this.path("config.json"), JSON.stringify(merged, null, 2));
422
+ return merged;
423
+ }
424
+
425
+ isMuted(): boolean {
426
+ return this.loadHostConfig().muted;
427
+ }
428
+
429
+ setMuted(muted: boolean): FileBuddyConfig {
430
+ return this.saveHostConfig({ muted });
431
+ }
432
+
433
+ loadCounters(slot?: string): EventCounters {
434
+ const global = this.readJson<GlobalCounters>(this.path("events.json"), EMPTY_GLOBAL);
435
+ if (!slot) return { ...global, ...EMPTY_SLOT };
436
+ const slotCounters = this.readJson<SlotCounters>(this.slotEventsFile(slot), EMPTY_SLOT);
437
+ return { ...global, ...slotCounters };
438
+ }
439
+
440
+ increment(key: keyof EventCounters, amount = 1, slot?: string): EventCounters {
441
+ return this.withLock(() => {
442
+ if (isSlotKey(key) && slot) {
443
+ const slotCounters = this.readJson<SlotCounters>(this.slotEventsFile(slot), EMPTY_SLOT);
444
+ slotCounters[key] += amount;
445
+ this.atomicWrite(this.slotEventsFile(slot), JSON.stringify(slotCounters, null, 2));
446
+ } else if (isGlobalKey(key)) {
447
+ const global = this.readJson<GlobalCounters>(this.path("events.json"), EMPTY_GLOBAL);
448
+ global[key] += amount;
449
+ this.atomicWrite(this.path("events.json"), JSON.stringify(global, null, 2));
450
+ }
451
+ return this.loadCounters(slot);
452
+ });
453
+ }
454
+
455
+ loadUnlocked(): UnlockedAchievement[] {
456
+ try {
457
+ return JSON.parse(readFileSync(this.path("unlocked.json"), "utf8")) as UnlockedAchievement[];
458
+ } catch {
459
+ return [];
460
+ }
461
+ }
462
+
463
+ saveUnlocked(unlocked: UnlockedAchievement[]): void {
464
+ this.atomicWrite(this.path("unlocked.json"), JSON.stringify(unlocked, null, 2));
465
+ }
466
+
467
+ unlockAchievements(
468
+ slot: string | undefined,
469
+ evaluate: (counters: EventCounters, unlockedIds: Set<string>) => Achievement[],
470
+ ): Achievement[] {
471
+ return this.withLock(() => {
472
+ const counters = this.loadCounters(slot);
473
+ const unlocked = this.loadUnlocked();
474
+ const unlockedIds = new Set(unlocked.map((entry) => entry.id));
475
+ const newlyUnlocked = evaluate(counters, unlockedIds);
476
+ if (newlyUnlocked.length === 0) return [];
477
+ this.saveUnlocked([
478
+ ...unlocked,
479
+ ...newlyUnlocked.map((achievement) => ({
480
+ id: achievement.id,
481
+ unlockedAt: Date.now(),
482
+ slot,
483
+ })),
484
+ ]);
485
+ return newlyUnlocked;
486
+ });
487
+ }
488
+
489
+ trackActiveDay(): void {
490
+ this.withLock(() => {
491
+ const today = new Date().toISOString().slice(0, 10);
492
+ const current = this.readJson<ActiveDayState>(this.path("active_days.json"), {
493
+ lastDate: "",
494
+ totalDays: 0,
495
+ });
496
+ if (current.lastDate === today) return;
497
+
498
+ const next = { lastDate: today, totalDays: current.totalDays + 1 };
499
+ this.atomicWrite(this.path("active_days.json"), JSON.stringify(next, null, 2));
500
+
501
+ const global = this.readJson<GlobalCounters>(this.path("events.json"), EMPTY_GLOBAL);
502
+ global.days_active = next.totalDays;
503
+ this.atomicWrite(this.path("events.json"), JSON.stringify(global, null, 2));
504
+ });
505
+ }
506
+
507
+ ensureStableIdentity(): string {
508
+ return this.withLock(() => {
509
+ this.ensureDir();
510
+ try {
511
+ const parsed = JSON.parse(readFileSync(this.path("identity.json"), "utf8")) as {
512
+ userId?: string;
513
+ };
514
+ if (parsed.userId) return parsed.userId;
515
+ } catch {
516
+ // Create a stable identity below.
517
+ }
518
+
519
+ const userId = randomUUID();
520
+ this.atomicWrite(this.path("identity.json"), JSON.stringify({ userId }, null, 2));
521
+ return userId;
522
+ });
523
+ }
524
+ }
525
+
526
+ export { slugifySlot } from "../../core/slot-slug.ts";