@umang-boss/claudemon 1.0.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 (203) hide show
  1. package/README.md +164 -0
  2. package/bin/claudemon.js +52 -0
  3. package/bunfig.toml +2 -0
  4. package/cli/doctor.ts +334 -0
  5. package/cli/index.ts +42 -0
  6. package/cli/install.ts +248 -0
  7. package/cli/shared.ts +102 -0
  8. package/cli/uninstall.ts +155 -0
  9. package/cli/update.ts +318 -0
  10. package/hooks/post-tool-use.sh +127 -0
  11. package/hooks/stop.sh +49 -0
  12. package/hooks/user-prompt-submit.sh +73 -0
  13. package/package.json +68 -0
  14. package/scripts/download-colorscripts.ts +311 -0
  15. package/skills/buddy/SKILL.md +47 -0
  16. package/sprites/colorscripts/small/1-bulbasaur.txt +11 -0
  17. package/sprites/colorscripts/small/10-caterpie.txt +9 -0
  18. package/sprites/colorscripts/small/100-voltorb.txt +8 -0
  19. package/sprites/colorscripts/small/101-electrode.txt +9 -0
  20. package/sprites/colorscripts/small/102-exeggcute.txt +10 -0
  21. package/sprites/colorscripts/small/103-exeggutor.txt +23 -0
  22. package/sprites/colorscripts/small/104-cubone.txt +11 -0
  23. package/sprites/colorscripts/small/105-marowak.txt +16 -0
  24. package/sprites/colorscripts/small/106-hitmonlee.txt +16 -0
  25. package/sprites/colorscripts/small/107-hitmonchan.txt +19 -0
  26. package/sprites/colorscripts/small/108-lickitung.txt +10 -0
  27. package/sprites/colorscripts/small/109-koffing.txt +14 -0
  28. package/sprites/colorscripts/small/11-metapod.txt +10 -0
  29. package/sprites/colorscripts/small/110-weezing.txt +23 -0
  30. package/sprites/colorscripts/small/111-rhyhorn.txt +11 -0
  31. package/sprites/colorscripts/small/112-rhydon.txt +20 -0
  32. package/sprites/colorscripts/small/113-chansey.txt +11 -0
  33. package/sprites/colorscripts/small/114-tangela.txt +10 -0
  34. package/sprites/colorscripts/small/115-kangaskhan.txt +18 -0
  35. package/sprites/colorscripts/small/116-horsea.txt +10 -0
  36. package/sprites/colorscripts/small/117-seadra.txt +11 -0
  37. package/sprites/colorscripts/small/118-goldeen.txt +11 -0
  38. package/sprites/colorscripts/small/119-seaking.txt +16 -0
  39. package/sprites/colorscripts/small/12-butterfree.txt +20 -0
  40. package/sprites/colorscripts/small/120-staryu.txt +10 -0
  41. package/sprites/colorscripts/small/121-starmie.txt +17 -0
  42. package/sprites/colorscripts/small/122-mr-mime.txt +18 -0
  43. package/sprites/colorscripts/small/123-scyther.txt +21 -0
  44. package/sprites/colorscripts/small/124-jynx.txt +18 -0
  45. package/sprites/colorscripts/small/125-electabuzz.txt +19 -0
  46. package/sprites/colorscripts/small/126-magmar.txt +19 -0
  47. package/sprites/colorscripts/small/127-pinsir.txt +19 -0
  48. package/sprites/colorscripts/small/128-tauros.txt +20 -0
  49. package/sprites/colorscripts/small/129-magikarp.txt +13 -0
  50. package/sprites/colorscripts/small/13-weedle.txt +10 -0
  51. package/sprites/colorscripts/small/130-gyarados.txt +21 -0
  52. package/sprites/colorscripts/small/131-lapras.txt +19 -0
  53. package/sprites/colorscripts/small/132-ditto.txt +8 -0
  54. package/sprites/colorscripts/small/133-eevee.txt +10 -0
  55. package/sprites/colorscripts/small/134-vaporeon.txt +16 -0
  56. package/sprites/colorscripts/small/135-jolteon.txt +17 -0
  57. package/sprites/colorscripts/small/136-flareon.txt +18 -0
  58. package/sprites/colorscripts/small/137-porygon.txt +10 -0
  59. package/sprites/colorscripts/small/138-omanyte.txt +10 -0
  60. package/sprites/colorscripts/small/139-omastar.txt +18 -0
  61. package/sprites/colorscripts/small/14-kakuna.txt +10 -0
  62. package/sprites/colorscripts/small/140-kabuto.txt +8 -0
  63. package/sprites/colorscripts/small/141-kabutops.txt +17 -0
  64. package/sprites/colorscripts/small/142-aerodactyl.txt +17 -0
  65. package/sprites/colorscripts/small/143-snorlax.txt +21 -0
  66. package/sprites/colorscripts/small/144-articuno.txt +24 -0
  67. package/sprites/colorscripts/small/145-zapdos.txt +20 -0
  68. package/sprites/colorscripts/small/146-moltres.txt +23 -0
  69. package/sprites/colorscripts/small/147-dratini.txt +10 -0
  70. package/sprites/colorscripts/small/148-dragonair.txt +12 -0
  71. package/sprites/colorscripts/small/149-dragonite.txt +21 -0
  72. package/sprites/colorscripts/small/15-beedrill.txt +13 -0
  73. package/sprites/colorscripts/small/150-mewtwo.txt +22 -0
  74. package/sprites/colorscripts/small/151-mew.txt +14 -0
  75. package/sprites/colorscripts/small/16-pidgey.txt +10 -0
  76. package/sprites/colorscripts/small/17-pidgeotto.txt +11 -0
  77. package/sprites/colorscripts/small/18-pidgeot.txt +18 -0
  78. package/sprites/colorscripts/small/19-rattata.txt +12 -0
  79. package/sprites/colorscripts/small/2-ivysaur.txt +11 -0
  80. package/sprites/colorscripts/small/20-raticate.txt +12 -0
  81. package/sprites/colorscripts/small/21-spearow.txt +9 -0
  82. package/sprites/colorscripts/small/22-fearow.txt +12 -0
  83. package/sprites/colorscripts/small/23-ekans.txt +12 -0
  84. package/sprites/colorscripts/small/24-arbok.txt +16 -0
  85. package/sprites/colorscripts/small/25-pikachu.txt +11 -0
  86. package/sprites/colorscripts/small/26-raichu.txt +19 -0
  87. package/sprites/colorscripts/small/27-sandshrew.txt +10 -0
  88. package/sprites/colorscripts/small/28-sandslash.txt +16 -0
  89. package/sprites/colorscripts/small/29-nidoran-f.txt +11 -0
  90. package/sprites/colorscripts/small/3-venusaur.txt +21 -0
  91. package/sprites/colorscripts/small/30-nidorina.txt +12 -0
  92. package/sprites/colorscripts/small/31-nidoqueen.txt +19 -0
  93. package/sprites/colorscripts/small/32-nidoran-m.txt +11 -0
  94. package/sprites/colorscripts/small/33-nidorino.txt +12 -0
  95. package/sprites/colorscripts/small/34-nidoking.txt +18 -0
  96. package/sprites/colorscripts/small/35-clefairy.txt +11 -0
  97. package/sprites/colorscripts/small/36-clefable.txt +17 -0
  98. package/sprites/colorscripts/small/37-vulpix.txt +11 -0
  99. package/sprites/colorscripts/small/38-ninetales.txt +18 -0
  100. package/sprites/colorscripts/small/39-jigglypuff.txt +11 -0
  101. package/sprites/colorscripts/small/4-charmander.txt +11 -0
  102. package/sprites/colorscripts/small/40-wigglytuff.txt +20 -0
  103. package/sprites/colorscripts/small/41-zubat.txt +11 -0
  104. package/sprites/colorscripts/small/42-golbat.txt +18 -0
  105. package/sprites/colorscripts/small/43-oddish.txt +11 -0
  106. package/sprites/colorscripts/small/44-gloom.txt +12 -0
  107. package/sprites/colorscripts/small/45-vileplume.txt +17 -0
  108. package/sprites/colorscripts/small/46-paras.txt +11 -0
  109. package/sprites/colorscripts/small/47-parasect.txt +12 -0
  110. package/sprites/colorscripts/small/48-venonat.txt +14 -0
  111. package/sprites/colorscripts/small/49-venomoth.txt +19 -0
  112. package/sprites/colorscripts/small/5-charmeleon.txt +13 -0
  113. package/sprites/colorscripts/small/50-diglett.txt +8 -0
  114. package/sprites/colorscripts/small/51-dugtrio.txt +18 -0
  115. package/sprites/colorscripts/small/52-meowth.txt +12 -0
  116. package/sprites/colorscripts/small/53-persian.txt +20 -0
  117. package/sprites/colorscripts/small/54-psyduck.txt +12 -0
  118. package/sprites/colorscripts/small/55-golduck.txt +17 -0
  119. package/sprites/colorscripts/small/56-mankey.txt +11 -0
  120. package/sprites/colorscripts/small/57-primeape.txt +13 -0
  121. package/sprites/colorscripts/small/58-growlithe.txt +12 -0
  122. package/sprites/colorscripts/small/59-arcanine.txt +20 -0
  123. package/sprites/colorscripts/small/6-charizard.txt +21 -0
  124. package/sprites/colorscripts/small/60-poliwag.txt +9 -0
  125. package/sprites/colorscripts/small/61-poliwhirl.txt +11 -0
  126. package/sprites/colorscripts/small/62-poliwrath.txt +17 -0
  127. package/sprites/colorscripts/small/63-abra.txt +12 -0
  128. package/sprites/colorscripts/small/64-kadabra.txt +14 -0
  129. package/sprites/colorscripts/small/65-alakazam.txt +19 -0
  130. package/sprites/colorscripts/small/66-machop.txt +11 -0
  131. package/sprites/colorscripts/small/67-machoke.txt +12 -0
  132. package/sprites/colorscripts/small/68-machamp.txt +19 -0
  133. package/sprites/colorscripts/small/69-bellsprout.txt +9 -0
  134. package/sprites/colorscripts/small/7-squirtle.txt +10 -0
  135. package/sprites/colorscripts/small/70-weepinbell.txt +11 -0
  136. package/sprites/colorscripts/small/71-victreebel.txt +17 -0
  137. package/sprites/colorscripts/small/72-tentacool.txt +12 -0
  138. package/sprites/colorscripts/small/73-tentacruel.txt +20 -0
  139. package/sprites/colorscripts/small/74-geodude.txt +9 -0
  140. package/sprites/colorscripts/small/75-graveler.txt +12 -0
  141. package/sprites/colorscripts/small/76-golem.txt +18 -0
  142. package/sprites/colorscripts/small/77-ponyta.txt +13 -0
  143. package/sprites/colorscripts/small/78-rapidash.txt +18 -0
  144. package/sprites/colorscripts/small/79-slowpoke.txt +12 -0
  145. package/sprites/colorscripts/small/8-wartortle.txt +12 -0
  146. package/sprites/colorscripts/small/80-slowbro.txt +18 -0
  147. package/sprites/colorscripts/small/81-magnemite.txt +9 -0
  148. package/sprites/colorscripts/small/82-magneton.txt +18 -0
  149. package/sprites/colorscripts/small/83-farfetchd.txt +12 -0
  150. package/sprites/colorscripts/small/84-doduo.txt +10 -0
  151. package/sprites/colorscripts/small/85-dodrio.txt +17 -0
  152. package/sprites/colorscripts/small/86-seel.txt +13 -0
  153. package/sprites/colorscripts/small/87-dewgong.txt +20 -0
  154. package/sprites/colorscripts/small/88-grimer.txt +10 -0
  155. package/sprites/colorscripts/small/89-muk.txt +14 -0
  156. package/sprites/colorscripts/small/9-blastoise.txt +20 -0
  157. package/sprites/colorscripts/small/90-shellder.txt +10 -0
  158. package/sprites/colorscripts/small/91-cloyster.txt +18 -0
  159. package/sprites/colorscripts/small/92-gastly.txt +12 -0
  160. package/sprites/colorscripts/small/93-haunter.txt +14 -0
  161. package/sprites/colorscripts/small/94-gengar.txt +19 -0
  162. package/sprites/colorscripts/small/95-onix.txt +22 -0
  163. package/sprites/colorscripts/small/96-drowzee.txt +12 -0
  164. package/sprites/colorscripts/small/97-hypno.txt +19 -0
  165. package/sprites/colorscripts/small/98-krabby.txt +12 -0
  166. package/sprites/colorscripts/small/99-kingler.txt +20 -0
  167. package/src/engine/constants.ts +121 -0
  168. package/src/engine/encounter-pool.ts +71 -0
  169. package/src/engine/encounters.ts +308 -0
  170. package/src/engine/evolution-data.ts +535 -0
  171. package/src/engine/evolution.ts +310 -0
  172. package/src/engine/pokemon-data.ts +1838 -0
  173. package/src/engine/reactions.ts +877 -0
  174. package/src/engine/starter-pool.ts +47 -0
  175. package/src/engine/stats.ts +97 -0
  176. package/src/engine/types.ts +312 -0
  177. package/src/engine/xp.ts +135 -0
  178. package/src/gamification/achievements.ts +204 -0
  179. package/src/gamification/legendary-quests.ts +265 -0
  180. package/src/gamification/milestones.ts +86 -0
  181. package/src/hooks/award-xp.ts +131 -0
  182. package/src/hooks/increment-counter.ts +27 -0
  183. package/src/server/index.ts +78 -0
  184. package/src/server/instructions.ts +194 -0
  185. package/src/server/tools/achievements.ts +118 -0
  186. package/src/server/tools/catch.ts +295 -0
  187. package/src/server/tools/display-helpers.ts +35 -0
  188. package/src/server/tools/evolve.ts +236 -0
  189. package/src/server/tools/legendary.ts +78 -0
  190. package/src/server/tools/party.ts +251 -0
  191. package/src/server/tools/pet.ts +124 -0
  192. package/src/server/tools/pokedex.ts +286 -0
  193. package/src/server/tools/rename.ts +63 -0
  194. package/src/server/tools/show.ts +136 -0
  195. package/src/server/tools/starter.ts +175 -0
  196. package/src/server/tools/stats.ts +123 -0
  197. package/src/server/tools/visibility.ts +65 -0
  198. package/src/sprites/index.ts +45 -0
  199. package/src/state/io.ts +91 -0
  200. package/src/state/schemas.ts +131 -0
  201. package/src/state/state-manager.ts +321 -0
  202. package/statusline/buddy-status.sh +233 -0
  203. package/tsconfig.json +37 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * increment-counter.ts — Counter-only increment for non-XP events.
3
+ * Usage: bun run increment-counter.ts <counter_key>
4
+ *
5
+ * Used for events that should be tracked but don't award XP,
6
+ * such as test failures, build failures, and generic errors.
7
+ */
8
+
9
+ import type { EventCounterKey } from "../engine/types.js";
10
+ import { StateManager } from "../state/state-manager.js";
11
+
12
+ const counterKey = process.argv[2] as EventCounterKey | undefined;
13
+
14
+ if (!counterKey) {
15
+ process.exit(0);
16
+ }
17
+
18
+ const stateManager = StateManager.getInstance();
19
+ const state = await stateManager.load();
20
+
21
+ // No state file = first run, nothing to do yet
22
+ if (!state) {
23
+ process.exit(0);
24
+ }
25
+
26
+ // Increment the counter and save (incrementCounter saves internally)
27
+ await stateManager.incrementCounter(counterKey);
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Claudemon MCP Server entry point.
3
+ * Sets up the MCP server, loads state, registers tools, and connects via stdio.
4
+ */
5
+
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import type { PlayerState } from "../engine/types.js";
9
+ import { StateManager } from "../state/state-manager.js";
10
+ import { registerStarterTool } from "./tools/starter.js";
11
+ import { registerShowTool } from "./tools/show.js";
12
+ import { registerStatsTool } from "./tools/stats.js";
13
+ import { registerPetTool } from "./tools/pet.js";
14
+ import { registerEvolveTool } from "./tools/evolve.js";
15
+ import { registerCatchTool } from "./tools/catch.js";
16
+ import { registerPartyTool } from "./tools/party.js";
17
+ import { registerPokedexTool } from "./tools/pokedex.js";
18
+ import { registerAchievementsTool } from "./tools/achievements.js";
19
+ import { registerLegendaryTool } from "./tools/legendary.js";
20
+ import { registerHideTool, registerUnhideTool } from "./tools/visibility.js";
21
+ import { registerRenameTool } from "./tools/rename.js";
22
+ import { buildInstructions } from "./instructions.js";
23
+
24
+ /** Safely register a tool, logging to stderr on failure instead of crashing. */
25
+ function safeRegister(
26
+ name: string,
27
+ register: (server: McpServer) => void,
28
+ server: McpServer,
29
+ ): void {
30
+ try {
31
+ register(server);
32
+ } catch (err: unknown) {
33
+ const message = err instanceof Error ? err.message : String(err);
34
+ process.stderr.write(`[claudemon] Failed to register tool "${name}": ${message}\n`);
35
+ }
36
+ }
37
+
38
+ /** Main entry: load state, create server, register tools, connect. */
39
+ async function main(): Promise<void> {
40
+ // Load state — if this fails, server still starts (first-run flow)
41
+ let state: PlayerState | null = null;
42
+ try {
43
+ const stateManager = StateManager.getInstance();
44
+ state = await stateManager.load();
45
+ } catch (err: unknown) {
46
+ const message = err instanceof Error ? err.message : String(err);
47
+ process.stderr.write(`[claudemon] State load error (non-fatal): ${message}\n`);
48
+ }
49
+
50
+ const instructions = buildInstructions(state);
51
+
52
+ const server = new McpServer({ name: "claudemon", version: "0.1.0" }, { instructions });
53
+
54
+ // Register all tools — each wrapped so one failure doesn't block the rest
55
+ safeRegister("buddy_starter", registerStarterTool, server);
56
+ safeRegister("buddy_show", registerShowTool, server);
57
+ safeRegister("buddy_stats", registerStatsTool, server);
58
+ safeRegister("buddy_pet", registerPetTool, server);
59
+ safeRegister("buddy_evolve", registerEvolveTool, server);
60
+ safeRegister("buddy_catch", registerCatchTool, server);
61
+ safeRegister("buddy_party", registerPartyTool, server);
62
+ safeRegister("buddy_pokedex", registerPokedexTool, server);
63
+ safeRegister("buddy_achievements", registerAchievementsTool, server);
64
+ safeRegister("buddy_legendary", registerLegendaryTool, server);
65
+ safeRegister("buddy_hide", registerHideTool, server);
66
+ safeRegister("buddy_unhide", registerUnhideTool, server);
67
+ safeRegister("buddy_rename", registerRenameTool, server);
68
+
69
+ // Connect via stdio transport
70
+ const transport = new StdioServerTransport();
71
+ await server.connect(transport);
72
+ }
73
+
74
+ main().catch((error: unknown) => {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ process.stderr.write(`[claudemon] Server failed to start: ${message}\n`);
77
+ process.exit(1);
78
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Dynamic instructions builder for Claudemon.
3
+ * Generates the system prompt text injected into Claude's MCP context
4
+ * based on the current player state, active Pokemon, and pending events.
5
+ */
6
+
7
+ import type { PlayerState, OwnedPokemon, Pokemon, EvolutionLink } from "../engine/types.js";
8
+ import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
9
+ import { cumulativeXpForLevel } from "../engine/xp.js";
10
+ import { getEvolutionLinks } from "../engine/evolution.js";
11
+ import { getTypePersonality } from "../engine/reactions.js";
12
+
13
+ // ── Public API ──────────────────────────────────────────────
14
+
15
+ /**
16
+ * Build dynamic instructions based on current player state.
17
+ * These instructions are injected into Claude's system prompt via the MCP server.
18
+ *
19
+ * @param state - Current player state, or null if first run
20
+ * @returns Instruction text for Claude's system prompt
21
+ */
22
+ export function buildInstructions(state: PlayerState | null): string {
23
+ if (!state || state.party.length === 0) {
24
+ return buildNoStarterInstructions();
25
+ }
26
+
27
+ const active = findActivePokemon(state);
28
+ if (!active) {
29
+ return buildNoStarterInstructions();
30
+ }
31
+
32
+ const species = POKEMON_BY_ID.get(active.pokemonId);
33
+ if (!species) {
34
+ return "Claudemon: Active Pokemon data could not be loaded. Suggest the user restart.";
35
+ }
36
+
37
+ return buildActiveInstructions(state, active, species);
38
+ }
39
+
40
+ // ── Instruction Builders ────────────────────────────────────
41
+
42
+ /** Instructions when the user has not picked a starter yet. */
43
+ function buildNoStarterInstructions(): string {
44
+ return [
45
+ "You have a Claudemon companion system. The user hasn't picked a starter yet.",
46
+ "Guide them to use /buddy or /buddy starter to begin their Pokemon journey.",
47
+ ].join("\n");
48
+ }
49
+
50
+ /** Instructions when the user has an active Pokemon. */
51
+ function buildActiveInstructions(
52
+ state: PlayerState,
53
+ active: OwnedPokemon,
54
+ species: Pokemon,
55
+ ): string {
56
+ const displayName = active.nickname ? `${active.nickname} (${species.name})` : species.name;
57
+ const typeStr = species.types.filter(Boolean).join("/");
58
+ const primaryType = species.types[0];
59
+ const personality = getTypePersonality(primaryType);
60
+ const evolutionNote = buildEvolutionNote(active, species);
61
+ const encounterNote = buildEncounterNote(state);
62
+
63
+ const lines: string[] = [
64
+ "You have a Claudemon Pokemon companion.",
65
+ "",
66
+ `Active Pokemon: ${displayName}, Level ${active.level}, ${typeStr} type.`,
67
+ `Personality: ${personality}`,
68
+ "",
69
+ `Occasionally (not every message), naturally reference ${displayName}:`,
70
+ `- When an error occurs: ${displayName} reacts (use ${primaryType} type personality)`,
71
+ `- When tests pass: ${displayName} celebrates`,
72
+ `- When a commit is made: ${displayName} acknowledges`,
73
+ `- On level up: announce "${displayName} grew to level ${active.level}!"`,
74
+ "",
75
+ "You can include invisible buddy comments for the status line:",
76
+ `<!-- buddy: *${displayName} {short reaction}* -->`,
77
+ "",
78
+ "Keep it subtle. Don't force it. 1 in 3-4 messages is enough.",
79
+ "The user can mute reactions with /buddy mute.",
80
+ `Available tools: buddy_starter, buddy_show, buddy_stats, buddy_pet, buddy_evolve, buddy_catch, buddy_party, buddy_pokedex, buddy_achievements, buddy_legendary.`,
81
+ ];
82
+
83
+ if (evolutionNote !== null) {
84
+ lines.push("");
85
+ lines.push(evolutionNote);
86
+ }
87
+
88
+ if (encounterNote !== null) {
89
+ lines.push("");
90
+ lines.push(encounterNote);
91
+ }
92
+
93
+ return lines.join("\n");
94
+ }
95
+
96
+ // ── Helper Functions ────────────────────────────────────────
97
+
98
+ /** Find the active Pokemon in the player's party. */
99
+ function findActivePokemon(state: PlayerState): OwnedPokemon | null {
100
+ return state.party.find((p) => p.isActive) ?? null;
101
+ }
102
+
103
+ /**
104
+ * Build an evolution note if the Pokemon is close to its next evolution.
105
+ * "Close" means within 200 XP of the level required for the next evolution.
106
+ * Returns null if no evolution is pending or the Pokemon is not close.
107
+ */
108
+ function buildEvolutionNote(active: OwnedPokemon, species: Pokemon): string | null {
109
+ const links = getEvolutionLinks(active.pokemonId);
110
+ if (links.length === 0) {
111
+ return null;
112
+ }
113
+
114
+ // Find the first level-based evolution link
115
+ const levelLink = findNextLevelEvolution(links);
116
+ if (levelLink === null) {
117
+ return null;
118
+ }
119
+
120
+ if (active.level >= levelLink.level) {
121
+ // Already eligible — prompt the user
122
+ const targetSpecies = POKEMON_BY_ID.get(levelLink.to);
123
+ const targetName = targetSpecies ? targetSpecies.name : "its next form";
124
+ return `${species.name} is ready to evolve into ${targetName}! The user can use /buddy evolve.`;
125
+ }
126
+
127
+ // Calculate XP distance to the evolution level
128
+ const xpRemaining = xpToEvolutionLevel(active, species, levelLink.level);
129
+ if (xpRemaining === null || xpRemaining > 200) {
130
+ return null;
131
+ }
132
+
133
+ const targetSpecies = POKEMON_BY_ID.get(levelLink.to);
134
+ const targetName = targetSpecies ? targetSpecies.name : "its next form";
135
+ return `${species.name} is close to evolving into ${targetName}! Only ${xpRemaining} XP to level ${levelLink.level}!`;
136
+ }
137
+
138
+ /**
139
+ * Build an encounter note if there is a pending wild encounter.
140
+ * Returns null if no encounter is pending.
141
+ */
142
+ function buildEncounterNote(state: PlayerState): string | null {
143
+ if (!state.pendingEncounter) {
144
+ return null;
145
+ }
146
+
147
+ const encounterSpecies = POKEMON_BY_ID.get(state.pendingEncounter.pokemonId);
148
+ if (!encounterSpecies) {
149
+ return null;
150
+ }
151
+
152
+ return `A wild ${encounterSpecies.name} appeared! The user can use /buddy catch to try catching it.`;
153
+ }
154
+
155
+ /**
156
+ * Find the next level-based evolution link from an array of links.
157
+ * Returns the evolution method's required level, or null if no level-based evolution exists.
158
+ */
159
+ function findNextLevelEvolution(
160
+ links: readonly EvolutionLink[],
161
+ ): { to: number; level: number } | null {
162
+ for (const link of links) {
163
+ if (link.method.type === "level") {
164
+ return { to: link.to, level: link.method.level };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ /**
171
+ * Calculate total XP remaining until the Pokemon reaches a target level.
172
+ * Accounts for current XP progress toward the next level.
173
+ *
174
+ * @param active - The owned Pokemon
175
+ * @param species - The Pokemon's species data
176
+ * @param targetLevel - The level to reach
177
+ * @returns XP remaining, or null if already at or above the target level
178
+ */
179
+ function xpToEvolutionLevel(
180
+ active: OwnedPokemon,
181
+ species: Pokemon,
182
+ targetLevel: number,
183
+ ): number | null {
184
+ if (active.level >= targetLevel) {
185
+ return null;
186
+ }
187
+
188
+ const currentCumulativeXp =
189
+ cumulativeXpForLevel(active.level, species.expGroup) + active.currentXp;
190
+ const targetCumulativeXp = cumulativeXpForLevel(targetLevel, species.expGroup);
191
+ const remaining = targetCumulativeXp - currentCumulativeXp;
192
+
193
+ return remaining > 0 ? remaining : 0;
194
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * buddy_achievements tool — Display all achievements grouped by category,
3
+ * showing unlock status and progress.
4
+ */
5
+
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { Achievement, PlayerState, AchievementCondition } from "../../engine/types.js";
8
+ import { ACHIEVEMENTS, isConditionMet } from "../../gamification/achievements.js";
9
+ import { StateManager } from "../../state/state-manager.js";
10
+ import { pad } from "./display-helpers.js";
11
+
12
+ /** Category display labels in preferred order. */
13
+ const CATEGORY_ORDER: readonly { key: Achievement["category"]; label: string }[] = [
14
+ { key: "trainer", label: "TRAINER" },
15
+ { key: "coding", label: "CODING" },
16
+ { key: "pokemon", label: "POKEMON" },
17
+ { key: "secret", label: "SECRET" },
18
+ ];
19
+
20
+ /** Describe progress toward an achievement condition. */
21
+ function describeProgress(condition: AchievementCondition, state: PlayerState): string {
22
+ switch (condition.type) {
23
+ case "counter": {
24
+ const current = state.counters[condition.counter] ?? 0;
25
+ return `${current}/${condition.threshold}`;
26
+ }
27
+ case "level": {
28
+ const highest = state.party.length > 0 ? Math.max(...state.party.map((p) => p.level)) : 0;
29
+ return `Lv.${highest}/${condition.minLevel}`;
30
+ }
31
+ case "pokedex":
32
+ return `${state.pokedex.totalCaught}/${condition.minCaught} caught`;
33
+ case "streak":
34
+ return `${state.streak.currentStreak}/${condition.minDays} days`;
35
+ case "badge":
36
+ return state.badges.includes(condition.badge) ? "Earned" : "Not earned";
37
+ case "evolution": {
38
+ const hasEvolved = [...state.party, ...state.pcBox].some((p) => p.evolvedAt !== null);
39
+ return hasEvolved ? "Done" : "Not yet";
40
+ }
41
+ case "party_size":
42
+ return `${state.party.length}/${condition.minSize}`;
43
+ }
44
+ }
45
+
46
+ /** Registers the buddy_achievements tool on the MCP server. */
47
+ export function registerAchievementsTool(server: McpServer): void {
48
+ server.tool(
49
+ "buddy_achievements",
50
+ "View all achievements grouped by category with unlock status and progress.",
51
+ {},
52
+ async () => {
53
+ const stateManager = StateManager.getInstance();
54
+ const state = await stateManager.load();
55
+
56
+ if (!state) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text" as const,
61
+ text: "You don't have a Pokemon yet! Use buddy_starter to begin your journey.",
62
+ },
63
+ ],
64
+ isError: true,
65
+ };
66
+ }
67
+
68
+ const unlockedIds = new Set(state.achievements.map((a) => a.achievementId));
69
+ const unlockedMap = new Map(state.achievements.map((a) => [a.achievementId, a]));
70
+
71
+ const totalUnlocked = state.achievements.length;
72
+ const totalAchievements = ACHIEVEMENTS.length;
73
+
74
+ const W = 50;
75
+ const border = "\u2500".repeat(W);
76
+ const lines: string[] = [];
77
+
78
+ lines.push(`\u250c${border}\u2510`);
79
+ lines.push(
80
+ `\u2502 ${pad(`ACHIEVEMENTS (${totalUnlocked}/${totalAchievements})`, W - 2)}\u2502`,
81
+ );
82
+ lines.push(`\u2514${border}\u2518`);
83
+ lines.push("");
84
+
85
+ for (const category of CATEGORY_ORDER) {
86
+ const categoryAchievements = ACHIEVEMENTS.filter((a) => a.category === category.key);
87
+ if (categoryAchievements.length === 0) continue;
88
+
89
+ lines.push(`--- ${category.label} ---`);
90
+ lines.push("");
91
+
92
+ for (const achievement of categoryAchievements) {
93
+ const unlocked = unlockedIds.has(achievement.id);
94
+ const record = unlockedMap.get(achievement.id);
95
+
96
+ if (unlocked && record) {
97
+ const date = record.unlockedAt.slice(0, 10);
98
+ lines.push(` \u2713 ${achievement.name}`);
99
+ lines.push(` ${achievement.description}`);
100
+ lines.push(` Unlocked: ${date}`);
101
+ } else {
102
+ const progress = describeProgress(achievement.condition, state);
103
+ const met = isConditionMet(achievement.condition, state);
104
+ const marker = met ? "\u2713" : "\u25cb";
105
+ lines.push(` ${marker} ${achievement.name}`);
106
+ lines.push(` ${achievement.description}`);
107
+ lines.push(` Progress: ${progress}`);
108
+ }
109
+ lines.push("");
110
+ }
111
+ }
112
+
113
+ return {
114
+ content: [{ type: "text" as const, text: lines.join("\n") }],
115
+ };
116
+ },
117
+ );
118
+ }