@umang-boss/claudemon 1.3.0 → 1.4.1

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 (43) hide show
  1. package/cli/doctor.ts +18 -2
  2. package/cli/index.ts +26 -8
  3. package/dist/cli/doctor.js +19 -2
  4. package/dist/cli/index.js +26 -9
  5. package/dist/src/engine/constants.js +9 -3
  6. package/dist/src/engine/encounters.js +50 -10
  7. package/dist/src/engine/mood.js +187 -0
  8. package/dist/src/engine/types.js +2 -0
  9. package/dist/src/gamification/achievements.js +3 -3
  10. package/dist/src/gamification/legendary-quests.js +4 -4
  11. package/dist/src/hooks/award-xp.js +75 -5
  12. package/dist/src/server/index.js +8 -0
  13. package/dist/src/server/instructions.js +23 -0
  14. package/dist/src/server/tools/catch.js +3 -0
  15. package/dist/src/server/tools/evolve.js +3 -0
  16. package/dist/src/server/tools/feed.js +120 -0
  17. package/dist/src/server/tools/play.js +310 -0
  18. package/dist/src/server/tools/settings.js +116 -0
  19. package/dist/src/server/tools/show.js +5 -0
  20. package/dist/src/server/tools/train.js +144 -0
  21. package/dist/src/state/schemas.js +18 -1
  22. package/dist/src/state/state-manager.js +23 -6
  23. package/package.json +1 -1
  24. package/skills/buddy/SKILL.md +16 -0
  25. package/src/engine/constants.ts +12 -3
  26. package/src/engine/encounters.ts +65 -9
  27. package/src/engine/mood.ts +220 -0
  28. package/src/engine/types.ts +24 -0
  29. package/src/gamification/achievements.ts +3 -3
  30. package/src/gamification/legendary-quests.ts +4 -4
  31. package/src/hooks/award-xp.ts +97 -5
  32. package/src/server/index.ts +8 -0
  33. package/src/server/instructions.ts +25 -0
  34. package/src/server/tools/catch.ts +4 -0
  35. package/src/server/tools/evolve.ts +4 -0
  36. package/src/server/tools/feed.ts +145 -0
  37. package/src/server/tools/play.ts +378 -0
  38. package/src/server/tools/settings.ts +142 -0
  39. package/src/server/tools/show.ts +7 -0
  40. package/src/server/tools/train.ts +180 -0
  41. package/src/state/schemas.ts +20 -0
  42. package/src/state/state-manager.ts +26 -6
  43. package/statusline/buddy-status.sh +77 -62
package/cli/doctor.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { access, stat, unlink, readdir, readFile } from "node:fs/promises";
9
- import { constants as fsConstants } from "node:fs";
9
+ import { constants as fsConstants, existsSync } from "node:fs";
10
10
  import { resolve, dirname } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { spawnSync } from "node:child_process";
@@ -32,7 +32,20 @@ const LOCK_MAX_AGE_MS = 5000;
32
32
  const EXPECTED_SPRITE_COUNT = 151;
33
33
  const __filename = fileURLToPath(import.meta.url);
34
34
  const __dirname = dirname(__filename);
35
- const COLORSCRIPT_DIR = resolve(dirname(__dirname), "sprites/colorscripts/small");
35
+ // Sprite dir: check multiple candidate paths (works from source, dist, and npm global)
36
+ function findColorscriptDir(): string | null {
37
+ const candidates = [
38
+ resolve(dirname(__dirname), "sprites", "colorscripts", "small"),
39
+ resolve(dirname(__dirname), "..", "sprites", "colorscripts", "small"),
40
+ resolve(__dirname, "..", "sprites", "colorscripts", "small"),
41
+ resolve(__dirname, "..", "..", "sprites", "colorscripts", "small"),
42
+ ];
43
+ for (const c of candidates) {
44
+ if (existsSync(c)) return c;
45
+ }
46
+ return null;
47
+ }
48
+ const COLORSCRIPT_DIR = findColorscriptDir();
36
49
 
37
50
  // ── Helpers ──────────────────────────────────────────────────
38
51
 
@@ -236,6 +249,9 @@ async function checkStaleLock(): Promise<CheckResult> {
236
249
 
237
250
  async function checkSpriteCount(): Promise<CheckResult> {
238
251
  try {
252
+ if (!COLORSCRIPT_DIR) {
253
+ return { label: "Sprites", passed: false, detail: "colorscripts directory not found" };
254
+ }
239
255
  await access(COLORSCRIPT_DIR, fsConstants.F_OK);
240
256
  const entries = await readdir(COLORSCRIPT_DIR);
241
257
  const spriteFiles = entries.filter((f) => f.endsWith(".txt"));
package/cli/index.ts CHANGED
@@ -1,19 +1,36 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * Claudemon CLI entry point.
4
- * Routes to install, uninstall, update, or doctor based on first argument.
5
- *
6
- * Usage:
7
- * npx claudemon install
8
- * npx claudemon uninstall
9
- * npx claudemon update
10
- * npx claudemon doctor
4
+ * Routes to install, uninstall, update, doctor, or --version.
11
5
  */
12
6
 
7
+ import { readFileSync } from "node:fs";
8
+ import { resolve, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
13
11
  export {};
14
12
  const command = process.argv[2];
15
13
 
16
14
  switch (command) {
15
+ case "--version":
16
+ case "-v":
17
+ case "version": {
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const pkgPaths = [
20
+ resolve(__dirname, "..", "package.json"),
21
+ resolve(__dirname, "..", "..", "package.json"),
22
+ ];
23
+ for (const p of pkgPaths) {
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(p, "utf-8")) as { version: string };
26
+ console.log(`claudemon v${pkg.version}`);
27
+ break;
28
+ } catch {
29
+ continue;
30
+ }
31
+ }
32
+ break;
33
+ }
17
34
  case "install":
18
35
  await import("./install.js");
19
36
  break;
@@ -28,13 +45,14 @@ switch (command) {
28
45
  break;
29
46
  default:
30
47
  console.log(`
31
- Claudemon — Pokemon Gen 1 coding companion for Claude Code
48
+ Claudemon — Pokemon coding companion for Claude Code
32
49
 
33
50
  Usage:
34
51
  claudemon install Set up Claudemon (MCP server, hooks, skill, status line)
35
52
  claudemon uninstall Remove Claudemon from Claude Code
36
53
  claudemon update Re-register everything (preserves save data)
37
54
  claudemon doctor Run diagnostics
55
+ claudemon --version Show version
38
56
 
39
57
  After install, start a new Claude Code session and type /buddy
40
58
  `);
@@ -5,7 +5,7 @@
5
5
  * Usage: bun run cli/doctor.ts
6
6
  */
7
7
  import { access, stat, unlink, readdir, readFile } from "node:fs/promises";
8
- import { constants as fsConstants } from "node:fs";
8
+ import { constants as fsConstants, existsSync } from "node:fs";
9
9
  import { resolve, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { spawnSync } from "node:child_process";
@@ -19,7 +19,21 @@ const LOCK_MAX_AGE_MS = 5000;
19
19
  const EXPECTED_SPRITE_COUNT = 151;
20
20
  const __filename = fileURLToPath(import.meta.url);
21
21
  const __dirname = dirname(__filename);
22
- const COLORSCRIPT_DIR = resolve(dirname(__dirname), "sprites/colorscripts/small");
22
+ // Sprite dir: check multiple candidate paths (works from source, dist, and npm global)
23
+ function findColorscriptDir() {
24
+ const candidates = [
25
+ resolve(dirname(__dirname), "sprites", "colorscripts", "small"),
26
+ resolve(dirname(__dirname), "..", "sprites", "colorscripts", "small"),
27
+ resolve(__dirname, "..", "sprites", "colorscripts", "small"),
28
+ resolve(__dirname, "..", "..", "sprites", "colorscripts", "small"),
29
+ ];
30
+ for (const c of candidates) {
31
+ if (existsSync(c))
32
+ return c;
33
+ }
34
+ return null;
35
+ }
36
+ const COLORSCRIPT_DIR = findColorscriptDir();
23
37
  function formatCheck(result) {
24
38
  const icon = result.passed ? "\u2713" : "\u2717";
25
39
  return `[${icon}] ${result.label}: ${result.detail}`;
@@ -188,6 +202,9 @@ async function checkStaleLock() {
188
202
  // ── Check 10: Sprite Count ──────────────────────────────────
189
203
  async function checkSpriteCount() {
190
204
  try {
205
+ if (!COLORSCRIPT_DIR) {
206
+ return { label: "Sprites", passed: false, detail: "colorscripts directory not found" };
207
+ }
191
208
  await access(COLORSCRIPT_DIR, fsConstants.F_OK);
192
209
  const entries = await readdir(COLORSCRIPT_DIR);
193
210
  const spriteFiles = entries.filter((f) => f.endsWith(".txt"));
package/dist/cli/index.js CHANGED
@@ -1,16 +1,33 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * Claudemon CLI entry point.
4
- * Routes to install, uninstall, update, or doctor based on first argument.
5
- *
6
- * Usage:
7
- * npx claudemon install
8
- * npx claudemon uninstall
9
- * npx claudemon update
10
- * npx claudemon doctor
4
+ * Routes to install, uninstall, update, doctor, or --version.
11
5
  */
6
+ import { readFileSync } from "node:fs";
7
+ import { resolve, dirname } from "node:path";
8
+ import { fileURLToPath } from "node:url";
12
9
  const command = process.argv[2];
13
10
  switch (command) {
11
+ case "--version":
12
+ case "-v":
13
+ case "version": {
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const pkgPaths = [
16
+ resolve(__dirname, "..", "package.json"),
17
+ resolve(__dirname, "..", "..", "package.json"),
18
+ ];
19
+ for (const p of pkgPaths) {
20
+ try {
21
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
22
+ console.log(`claudemon v${pkg.version}`);
23
+ break;
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ }
29
+ break;
30
+ }
14
31
  case "install":
15
32
  await import("./install.js");
16
33
  break;
@@ -25,16 +42,16 @@ switch (command) {
25
42
  break;
26
43
  default:
27
44
  console.log(`
28
- Claudemon — Pokemon Gen 1 coding companion for Claude Code
45
+ Claudemon — Pokemon coding companion for Claude Code
29
46
 
30
47
  Usage:
31
48
  claudemon install Set up Claudemon (MCP server, hooks, skill, status line)
32
49
  claudemon uninstall Remove Claudemon from Claude Code
33
50
  claudemon update Re-register everything (preserves save data)
34
51
  claudemon doctor Run diagnostics
52
+ claudemon --version Show version
35
53
 
36
54
  After install, start a new Claude Code session and type /buddy
37
55
  `);
38
56
  break;
39
57
  }
40
- export {};
@@ -33,8 +33,14 @@ export const STAT_DISPLAY_NAMES = {
33
33
  wisdom: "WISDOM",
34
34
  };
35
35
  // ── Encounter Rate ─────────────────────────────────────────
36
- /** XP earned between wild encounters */
37
- export const XP_PER_ENCOUNTER = 500;
36
+ /** XP thresholds for encounter triggers by speed setting */
37
+ export const ENCOUNTER_THRESHOLDS = {
38
+ fast: 100,
39
+ normal: 250,
40
+ slow: 500,
41
+ };
42
+ /** Default encounter speed */
43
+ export const DEFAULT_ENCOUNTER_SPEED = "normal";
38
44
  // ── Reaction Cooldown ──────────────────────────────────────
39
45
  export const DEFAULT_REACTION_COOLDOWN_MS = 30_000;
40
46
  // ── Trainer Titles (by highest Pokemon level) ──────────────
@@ -73,7 +79,7 @@ export const BADGES = [
73
79
  {
74
80
  type: "lunar",
75
81
  name: "Lunar Badge",
76
- description: "Maintain a 30-day coding streak — unlocks Moon Stone evolutions",
82
+ description: "Code for 30 days (weekends off OK) — unlocks Moon Stone evolutions",
77
83
  condition: { type: "streak", minDays: 30 },
78
84
  },
79
85
  {
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Wild encounter system.
3
- * Pokemon appear based on coding activity type and are catchable
4
- * based on the active Pokemon's stats and level.
3
+ * Pokemon appear based on coding activity type, streak bonuses,
4
+ * tool diversity, and time-of-day biases. Catch eligibility is
5
+ * determined by the active Pokemon's stats and level.
5
6
  */
6
- import { XP_PER_ENCOUNTER } from "./constants.js";
7
+ import { ENCOUNTER_THRESHOLDS } from "./constants.js";
7
8
  import { POKEMON_BY_ID } from "./pokemon-data.js";
8
9
  import { TYPE_POOLS } from "./encounter-pool.js";
9
10
  // ── Activity to Pokemon Type Mapping ──────────────────────────
@@ -28,11 +29,40 @@ export function getEncounterTypes(eventType) {
28
29
  }
29
30
  // ── Encounter Trigger ─────────────────────────────────────────
30
31
  /**
31
- * Check if a wild encounter should trigger based on XP earned since last encounter.
32
- * Returns true roughly every XP_PER_ENCOUNTER (500) XP.
32
+ * Check if a wild encounter should trigger based on XP earned,
33
+ * encounter speed setting, and streak bonus.
34
+ * Streak bonus: 7+ day streak halves the threshold.
33
35
  */
34
- export function shouldTriggerEncounter(xpSinceLastEncounter) {
35
- return xpSinceLastEncounter >= XP_PER_ENCOUNTER;
36
+ export function shouldTriggerEncounter(ctx) {
37
+ const threshold = ENCOUNTER_THRESHOLDS[ctx.encounterSpeed];
38
+ // Streak bonus: 7+ day streak = halve the threshold
39
+ const streakMultiplier = ctx.currentStreak >= 7 ? 0.5 : 1;
40
+ const effectiveThreshold = Math.floor(threshold * streakMultiplier);
41
+ if (ctx.xpSinceLastEncounter < effectiveThreshold)
42
+ return false;
43
+ return true;
44
+ }
45
+ /** Check for bonus encounter (10% chance after a regular encounter). */
46
+ export function shouldBonusEncounter() {
47
+ return Math.random() < 0.1;
48
+ }
49
+ /** Check for activity diversity bonus (3+ unique tool types in recent history). */
50
+ export function shouldDiversityBonus(recentToolTypes) {
51
+ const uniqueTypes = new Set(recentToolTypes);
52
+ return uniqueTypes.size >= 3;
53
+ }
54
+ // ── Time-of-Day Bias ──────────────────────────────────────────
55
+ /** Get time-of-day type biases for encounter generation. */
56
+ export function getTimeOfDayBias(hour) {
57
+ if (hour >= 22 || hour < 5)
58
+ return ["Ghost", "Poison"]; // Night: Ghost types
59
+ if (hour >= 5 && hour < 9)
60
+ return ["Grass", "Bug"]; // Morning: Grass types
61
+ if (hour >= 12 && hour < 14)
62
+ return ["Fire", "Rock"]; // Midday: Fire types
63
+ if (hour >= 17 && hour < 20)
64
+ return ["Water", "Flying"]; // Evening: Water types
65
+ return []; // No bias
36
66
  }
37
67
  // ── Rarity Weights ────────────────────────────────────────────
38
68
  /** Relative weights for rarity-based selection. Higher = more likely to appear. */
@@ -186,11 +216,21 @@ function determineEncounterLevel(state, seed) {
186
216
  /**
187
217
  * Generate a wild encounter based on the activity type.
188
218
  * Picks a Pokemon from the matching type pool, weighted by rarity.
189
- * Excludes Pokemon already in the player's party/box (unless duplicates are common tier).
219
+ * If time-of-day bias types are provided, there is a 40% chance to
220
+ * use those types instead of the activity-based types.
221
+ * Excludes Pokemon already in the player's party/box (unless common tier).
190
222
  * Returns null if no eligible Pokemon found.
191
223
  */
192
- export function generateEncounter(eventType, state) {
193
- const types = getEncounterTypes(eventType);
224
+ export function generateEncounter(eventType, state, timeOfDayTypes) {
225
+ let types = getEncounterTypes(eventType);
226
+ // 40% chance to use time-of-day biased types if available
227
+ if (timeOfDayTypes && timeOfDayTypes.length > 0) {
228
+ const seed = Math.floor(Date.now() / 1000);
229
+ const biasRoll = seededRandom(seed + 42);
230
+ if (biasRoll < 0.4) {
231
+ types = timeOfDayTypes;
232
+ }
233
+ }
194
234
  const candidates = buildCandidatePool(types, state);
195
235
  if (candidates.length === 0)
196
236
  return null;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Mood engine for Claudemon.
3
+ * Pure functions that calculate mood based on recent events, time of day,
4
+ * and special triggers (evolution, achievements, catches).
5
+ */
6
+ // ── Mood Decay Durations (milliseconds) ───────────────────
7
+ /** How long each mood lasts before decaying back to neutral */
8
+ const MOOD_DECAY_MS = {
9
+ happy: 600_000, // 10 minutes
10
+ worried: 300_000, // 5 minutes
11
+ sleepy: Infinity, // Resets based on time-of-day, not duration
12
+ energetic: 900_000, // 15 minutes
13
+ proud: 600_000, // 10 minutes
14
+ neutral: Infinity, // Never decays (it IS the default)
15
+ };
16
+ // ── XP Event Types That Trigger Moods ─────────────────────
17
+ const POSITIVE_EVENTS = new Set(["test_pass", "build_success", "commit"]);
18
+ const NEGATIVE_EVENTS = new Set(["test_fail", "build_fail", "error"]);
19
+ // ── Mood Calculation ──────────────────────────────────────
20
+ /**
21
+ * Calculate the current mood based on recent events, time, and special triggers.
22
+ *
23
+ * Priority order:
24
+ * 1. Sleepy (midnight to 5 AM)
25
+ * 2. Proud (just evolved/achieved/caught)
26
+ * 3. Worried (recent negative event)
27
+ * 4. Happy (recent positive event)
28
+ * 5. Energetic (morning + active streak)
29
+ * 6. Keep current mood if it hasn't decayed
30
+ * 7. Neutral (default fallback)
31
+ *
32
+ * @param recentEvent - The last XP event type, or null
33
+ * @param counters - Current event counters (for context)
34
+ * @param currentHour - Hour of day (0-23)
35
+ * @param lastMood - The previous mood
36
+ * @param moodSetAt - Timestamp when last mood was set
37
+ * @param hadEvolution - Whether the Pokemon just evolved
38
+ * @param hadAchievement - Whether the player just unlocked an achievement
39
+ * @param hadCatch - Whether the player just caught a Pokemon
40
+ * @returns The calculated mood
41
+ */
42
+ export function calculateMood(recentEvent, counters, currentHour, lastMood, moodSetAt, hadEvolution, hadAchievement, hadCatch) {
43
+ // 1. Sleepy: midnight to 5 AM (hours 0-4)
44
+ if (currentHour >= 0 && currentHour < 5) {
45
+ return "sleepy";
46
+ }
47
+ // 2. Proud: just evolved, achieved, or caught a Pokemon
48
+ if (hadEvolution || hadAchievement || hadCatch) {
49
+ return "proud";
50
+ }
51
+ // 3. Worried: recent negative event
52
+ if (recentEvent !== null && NEGATIVE_EVENTS.has(recentEvent)) {
53
+ return "worried";
54
+ }
55
+ // 4. Happy: recent positive event
56
+ if (recentEvent !== null && POSITIVE_EVENTS.has(recentEvent)) {
57
+ return "happy";
58
+ }
59
+ // 5. Energetic: morning coding (5 AM - 10 AM) with an active streak
60
+ if (currentHour >= 5 && currentHour < 10) {
61
+ // Use a simple heuristic: if there have been any sessions, consider it an active streak
62
+ const hasStreak = counters.sessions > 0;
63
+ if (hasStreak) {
64
+ return "energetic";
65
+ }
66
+ }
67
+ // 6. Keep current mood if it hasn't decayed
68
+ if (!hasMoodDecayed(lastMood, moodSetAt, Date.now())) {
69
+ return lastMood;
70
+ }
71
+ // 7. Default fallback
72
+ return "neutral";
73
+ }
74
+ /**
75
+ * Check whether a mood has expired based on its decay duration.
76
+ *
77
+ * @param mood - The mood to check
78
+ * @param setAt - Timestamp when the mood was set
79
+ * @param now - Current timestamp
80
+ * @returns true if the mood has decayed (expired)
81
+ */
82
+ export function hasMoodDecayed(mood, setAt, now) {
83
+ const duration = MOOD_DECAY_MS[mood];
84
+ if (duration === Infinity) {
85
+ // Sleepy decays when it's no longer midnight-5 AM (handled in calculateMood)
86
+ // Neutral never decays
87
+ return false;
88
+ }
89
+ return now - setAt >= duration;
90
+ }
91
+ // ── Mood Speeches ─────────────────────────────────────────
92
+ /** Mood-specific speech lines for the status line. Name placeholder {name} is replaced at call time. */
93
+ const MOOD_SPEECHES = {
94
+ happy: [
95
+ "*{name} is beaming with pride!*",
96
+ "*{name} does a little victory dance*",
97
+ "*{name} radiates positive energy*",
98
+ "*{name} bounces happily*",
99
+ "*{name} gives you a thumbs up*",
100
+ ],
101
+ worried: [
102
+ "*{name} looks concerned...*",
103
+ "*{name} nervously watches the errors*",
104
+ "*{name} hides behind the terminal*",
105
+ "*{name} paces back and forth*",
106
+ "*{name} offers you a virtual hug*",
107
+ ],
108
+ sleepy: [
109
+ "*{name} yawns widely*",
110
+ "*{name} dozes off... zzz*",
111
+ "*{name} rubs its eyes*",
112
+ "*{name} curls up near the keyboard*",
113
+ "*{name} mumbles in its sleep*",
114
+ ],
115
+ energetic: [
116
+ "*{name} is fired up! Let's go!*",
117
+ "*{name} bounces off the walls*",
118
+ "*{name} can't sit still!*",
119
+ "*{name} is ready to code all day!*",
120
+ "*{name} stretches and flexes*",
121
+ ],
122
+ proud: [
123
+ "*{name} puffs up with pride*",
124
+ "*{name} strikes a victory pose*",
125
+ "*{name} shows off to everyone*",
126
+ "*{name} earned bragging rights!*",
127
+ "*{name} stands tall and proud*",
128
+ ],
129
+ neutral: [
130
+ "*{name} looks at your code curiously*",
131
+ "*{name} nods along as you type*",
132
+ "*{name} is watching closely*",
133
+ "*{name} hums softly*",
134
+ "*{name} waits patiently*",
135
+ "*{name} tilts head at the screen*",
136
+ "*{name} chirps encouragingly*",
137
+ "*{name} peers at a variable name*",
138
+ ],
139
+ };
140
+ /**
141
+ * Get mood-specific speech messages with the Pokemon's name filled in.
142
+ *
143
+ * @param name - The Pokemon's display name
144
+ * @param mood - The current mood
145
+ * @returns Array of speech strings with the name interpolated
146
+ */
147
+ export function getMoodSpeeches(name, mood) {
148
+ const templates = MOOD_SPEECHES[mood];
149
+ return templates.map((t) => t.replace("{name}", name));
150
+ }
151
+ // ── Mood Display Helpers ──────────────────────────────────
152
+ /** Emoji representation for each mood */
153
+ const MOOD_EMOJIS = {
154
+ happy: "\u{1F60A}", // 😊
155
+ worried: "\u{1F61F}", // 😟
156
+ sleepy: "\u{1F634}", // 😴
157
+ energetic: "\u{26A1}", // ⚡
158
+ proud: "\u{1F451}", // 👑
159
+ neutral: "\u{1F610}", // 😐
160
+ };
161
+ /** Human-readable mood descriptions */
162
+ const MOOD_DESCRIPTIONS = {
163
+ happy: "Happy",
164
+ worried: "Worried",
165
+ sleepy: "Sleepy",
166
+ energetic: "Energetic",
167
+ proud: "Proud",
168
+ neutral: "Neutral",
169
+ };
170
+ /**
171
+ * Get the emoji for a mood.
172
+ *
173
+ * @param mood - The mood type
174
+ * @returns The emoji string
175
+ */
176
+ export function getMoodEmoji(mood) {
177
+ return MOOD_EMOJIS[mood];
178
+ }
179
+ /**
180
+ * Get a human-readable description for a mood.
181
+ *
182
+ * @param mood - The mood type
183
+ * @returns The description string (e.g. "Happy")
184
+ */
185
+ export function getMoodDescription(mood) {
186
+ return MOOD_DESCRIPTIONS[mood];
187
+ }
@@ -2,6 +2,8 @@
2
2
  * Core type definitions for Claudemon.
3
3
  * Single source of truth for all shared interfaces and types.
4
4
  */
5
+ // ── Mood Types ────────────────────────────────────────────
6
+ export const MOOD_TYPES = ["happy", "worried", "sleepy", "energetic", "proud", "neutral"];
5
7
  // ── Pokemon Types ──────────────────────────────────────────
6
8
  export const POKEMON_TYPES = [
7
9
  "Normal",
@@ -93,21 +93,21 @@ export const ACHIEVEMENTS = [
93
93
  {
94
94
  id: "iron_coder",
95
95
  name: "Iron Coder",
96
- description: "Maintain a 7-day coding streak",
96
+ description: "Code for 7 days (weekends off OK)",
97
97
  category: "coding",
98
98
  condition: { type: "streak", minDays: 7 },
99
99
  },
100
100
  {
101
101
  id: "marathon",
102
102
  name: "Marathon",
103
- description: "Maintain a 30-day coding streak",
103
+ description: "Code for 30 days (weekends off OK)",
104
104
  category: "coding",
105
105
  condition: { type: "streak", minDays: 30 },
106
106
  },
107
107
  {
108
108
  id: "centurion",
109
109
  name: "Centurion",
110
- description: "Maintain a 100-day coding streak",
110
+ description: "Code for 100 days (weekends off OK)",
111
111
  category: "coding",
112
112
  condition: { type: "streak", minDays: 100 },
113
113
  },
@@ -12,7 +12,7 @@ export const LEGENDARY_QUESTS = [
12
12
  name: "The Ice Bird of Endurance",
13
13
  steps: [
14
14
  {
15
- description: "Maintain a 30-day coding streak",
15
+ description: "Code for 30 days (weekends off OK)",
16
16
  condition: { type: "streak", minDays: 30 },
17
17
  },
18
18
  {
@@ -21,7 +21,7 @@ export const LEGENDARY_QUESTS = [
21
21
  condition: { type: "pokedex", minCaught: 3 },
22
22
  },
23
23
  {
24
- description: "Maintain a 100-day coding streak",
24
+ description: "Code for 100 days (weekends off OK)",
25
25
  condition: { type: "streak", minDays: 100 },
26
26
  },
27
27
  {
@@ -105,7 +105,7 @@ export const LEGENDARY_QUESTS = [
105
105
  name: "The Myth",
106
106
  steps: [
107
107
  {
108
- description: "Maintain a 100-day coding streak",
108
+ description: "Code for 100 days (weekends off OK)",
109
109
  condition: { type: "streak", minDays: 100 },
110
110
  },
111
111
  {
@@ -121,7 +121,7 @@ export const LEGENDARY_QUESTS = [
121
121
  condition: { type: "pokedex", minCaught: 149 },
122
122
  },
123
123
  {
124
- description: "Maintain a 365-day coding streak",
124
+ description: "Code for 365 days (weekends off OK)",
125
125
  condition: { type: "streak", minDays: 365 },
126
126
  },
127
127
  ],