@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.
- package/README.md +164 -0
- package/bin/claudemon.js +52 -0
- package/bunfig.toml +2 -0
- package/cli/doctor.ts +334 -0
- package/cli/index.ts +42 -0
- package/cli/install.ts +248 -0
- package/cli/shared.ts +102 -0
- package/cli/uninstall.ts +155 -0
- package/cli/update.ts +318 -0
- package/hooks/post-tool-use.sh +127 -0
- package/hooks/stop.sh +49 -0
- package/hooks/user-prompt-submit.sh +73 -0
- package/package.json +68 -0
- package/scripts/download-colorscripts.ts +311 -0
- package/skills/buddy/SKILL.md +47 -0
- package/sprites/colorscripts/small/1-bulbasaur.txt +11 -0
- package/sprites/colorscripts/small/10-caterpie.txt +9 -0
- package/sprites/colorscripts/small/100-voltorb.txt +8 -0
- package/sprites/colorscripts/small/101-electrode.txt +9 -0
- package/sprites/colorscripts/small/102-exeggcute.txt +10 -0
- package/sprites/colorscripts/small/103-exeggutor.txt +23 -0
- package/sprites/colorscripts/small/104-cubone.txt +11 -0
- package/sprites/colorscripts/small/105-marowak.txt +16 -0
- package/sprites/colorscripts/small/106-hitmonlee.txt +16 -0
- package/sprites/colorscripts/small/107-hitmonchan.txt +19 -0
- package/sprites/colorscripts/small/108-lickitung.txt +10 -0
- package/sprites/colorscripts/small/109-koffing.txt +14 -0
- package/sprites/colorscripts/small/11-metapod.txt +10 -0
- package/sprites/colorscripts/small/110-weezing.txt +23 -0
- package/sprites/colorscripts/small/111-rhyhorn.txt +11 -0
- package/sprites/colorscripts/small/112-rhydon.txt +20 -0
- package/sprites/colorscripts/small/113-chansey.txt +11 -0
- package/sprites/colorscripts/small/114-tangela.txt +10 -0
- package/sprites/colorscripts/small/115-kangaskhan.txt +18 -0
- package/sprites/colorscripts/small/116-horsea.txt +10 -0
- package/sprites/colorscripts/small/117-seadra.txt +11 -0
- package/sprites/colorscripts/small/118-goldeen.txt +11 -0
- package/sprites/colorscripts/small/119-seaking.txt +16 -0
- package/sprites/colorscripts/small/12-butterfree.txt +20 -0
- package/sprites/colorscripts/small/120-staryu.txt +10 -0
- package/sprites/colorscripts/small/121-starmie.txt +17 -0
- package/sprites/colorscripts/small/122-mr-mime.txt +18 -0
- package/sprites/colorscripts/small/123-scyther.txt +21 -0
- package/sprites/colorscripts/small/124-jynx.txt +18 -0
- package/sprites/colorscripts/small/125-electabuzz.txt +19 -0
- package/sprites/colorscripts/small/126-magmar.txt +19 -0
- package/sprites/colorscripts/small/127-pinsir.txt +19 -0
- package/sprites/colorscripts/small/128-tauros.txt +20 -0
- package/sprites/colorscripts/small/129-magikarp.txt +13 -0
- package/sprites/colorscripts/small/13-weedle.txt +10 -0
- package/sprites/colorscripts/small/130-gyarados.txt +21 -0
- package/sprites/colorscripts/small/131-lapras.txt +19 -0
- package/sprites/colorscripts/small/132-ditto.txt +8 -0
- package/sprites/colorscripts/small/133-eevee.txt +10 -0
- package/sprites/colorscripts/small/134-vaporeon.txt +16 -0
- package/sprites/colorscripts/small/135-jolteon.txt +17 -0
- package/sprites/colorscripts/small/136-flareon.txt +18 -0
- package/sprites/colorscripts/small/137-porygon.txt +10 -0
- package/sprites/colorscripts/small/138-omanyte.txt +10 -0
- package/sprites/colorscripts/small/139-omastar.txt +18 -0
- package/sprites/colorscripts/small/14-kakuna.txt +10 -0
- package/sprites/colorscripts/small/140-kabuto.txt +8 -0
- package/sprites/colorscripts/small/141-kabutops.txt +17 -0
- package/sprites/colorscripts/small/142-aerodactyl.txt +17 -0
- package/sprites/colorscripts/small/143-snorlax.txt +21 -0
- package/sprites/colorscripts/small/144-articuno.txt +24 -0
- package/sprites/colorscripts/small/145-zapdos.txt +20 -0
- package/sprites/colorscripts/small/146-moltres.txt +23 -0
- package/sprites/colorscripts/small/147-dratini.txt +10 -0
- package/sprites/colorscripts/small/148-dragonair.txt +12 -0
- package/sprites/colorscripts/small/149-dragonite.txt +21 -0
- package/sprites/colorscripts/small/15-beedrill.txt +13 -0
- package/sprites/colorscripts/small/150-mewtwo.txt +22 -0
- package/sprites/colorscripts/small/151-mew.txt +14 -0
- package/sprites/colorscripts/small/16-pidgey.txt +10 -0
- package/sprites/colorscripts/small/17-pidgeotto.txt +11 -0
- package/sprites/colorscripts/small/18-pidgeot.txt +18 -0
- package/sprites/colorscripts/small/19-rattata.txt +12 -0
- package/sprites/colorscripts/small/2-ivysaur.txt +11 -0
- package/sprites/colorscripts/small/20-raticate.txt +12 -0
- package/sprites/colorscripts/small/21-spearow.txt +9 -0
- package/sprites/colorscripts/small/22-fearow.txt +12 -0
- package/sprites/colorscripts/small/23-ekans.txt +12 -0
- package/sprites/colorscripts/small/24-arbok.txt +16 -0
- package/sprites/colorscripts/small/25-pikachu.txt +11 -0
- package/sprites/colorscripts/small/26-raichu.txt +19 -0
- package/sprites/colorscripts/small/27-sandshrew.txt +10 -0
- package/sprites/colorscripts/small/28-sandslash.txt +16 -0
- package/sprites/colorscripts/small/29-nidoran-f.txt +11 -0
- package/sprites/colorscripts/small/3-venusaur.txt +21 -0
- package/sprites/colorscripts/small/30-nidorina.txt +12 -0
- package/sprites/colorscripts/small/31-nidoqueen.txt +19 -0
- package/sprites/colorscripts/small/32-nidoran-m.txt +11 -0
- package/sprites/colorscripts/small/33-nidorino.txt +12 -0
- package/sprites/colorscripts/small/34-nidoking.txt +18 -0
- package/sprites/colorscripts/small/35-clefairy.txt +11 -0
- package/sprites/colorscripts/small/36-clefable.txt +17 -0
- package/sprites/colorscripts/small/37-vulpix.txt +11 -0
- package/sprites/colorscripts/small/38-ninetales.txt +18 -0
- package/sprites/colorscripts/small/39-jigglypuff.txt +11 -0
- package/sprites/colorscripts/small/4-charmander.txt +11 -0
- package/sprites/colorscripts/small/40-wigglytuff.txt +20 -0
- package/sprites/colorscripts/small/41-zubat.txt +11 -0
- package/sprites/colorscripts/small/42-golbat.txt +18 -0
- package/sprites/colorscripts/small/43-oddish.txt +11 -0
- package/sprites/colorscripts/small/44-gloom.txt +12 -0
- package/sprites/colorscripts/small/45-vileplume.txt +17 -0
- package/sprites/colorscripts/small/46-paras.txt +11 -0
- package/sprites/colorscripts/small/47-parasect.txt +12 -0
- package/sprites/colorscripts/small/48-venonat.txt +14 -0
- package/sprites/colorscripts/small/49-venomoth.txt +19 -0
- package/sprites/colorscripts/small/5-charmeleon.txt +13 -0
- package/sprites/colorscripts/small/50-diglett.txt +8 -0
- package/sprites/colorscripts/small/51-dugtrio.txt +18 -0
- package/sprites/colorscripts/small/52-meowth.txt +12 -0
- package/sprites/colorscripts/small/53-persian.txt +20 -0
- package/sprites/colorscripts/small/54-psyduck.txt +12 -0
- package/sprites/colorscripts/small/55-golduck.txt +17 -0
- package/sprites/colorscripts/small/56-mankey.txt +11 -0
- package/sprites/colorscripts/small/57-primeape.txt +13 -0
- package/sprites/colorscripts/small/58-growlithe.txt +12 -0
- package/sprites/colorscripts/small/59-arcanine.txt +20 -0
- package/sprites/colorscripts/small/6-charizard.txt +21 -0
- package/sprites/colorscripts/small/60-poliwag.txt +9 -0
- package/sprites/colorscripts/small/61-poliwhirl.txt +11 -0
- package/sprites/colorscripts/small/62-poliwrath.txt +17 -0
- package/sprites/colorscripts/small/63-abra.txt +12 -0
- package/sprites/colorscripts/small/64-kadabra.txt +14 -0
- package/sprites/colorscripts/small/65-alakazam.txt +19 -0
- package/sprites/colorscripts/small/66-machop.txt +11 -0
- package/sprites/colorscripts/small/67-machoke.txt +12 -0
- package/sprites/colorscripts/small/68-machamp.txt +19 -0
- package/sprites/colorscripts/small/69-bellsprout.txt +9 -0
- package/sprites/colorscripts/small/7-squirtle.txt +10 -0
- package/sprites/colorscripts/small/70-weepinbell.txt +11 -0
- package/sprites/colorscripts/small/71-victreebel.txt +17 -0
- package/sprites/colorscripts/small/72-tentacool.txt +12 -0
- package/sprites/colorscripts/small/73-tentacruel.txt +20 -0
- package/sprites/colorscripts/small/74-geodude.txt +9 -0
- package/sprites/colorscripts/small/75-graveler.txt +12 -0
- package/sprites/colorscripts/small/76-golem.txt +18 -0
- package/sprites/colorscripts/small/77-ponyta.txt +13 -0
- package/sprites/colorscripts/small/78-rapidash.txt +18 -0
- package/sprites/colorscripts/small/79-slowpoke.txt +12 -0
- package/sprites/colorscripts/small/8-wartortle.txt +12 -0
- package/sprites/colorscripts/small/80-slowbro.txt +18 -0
- package/sprites/colorscripts/small/81-magnemite.txt +9 -0
- package/sprites/colorscripts/small/82-magneton.txt +18 -0
- package/sprites/colorscripts/small/83-farfetchd.txt +12 -0
- package/sprites/colorscripts/small/84-doduo.txt +10 -0
- package/sprites/colorscripts/small/85-dodrio.txt +17 -0
- package/sprites/colorscripts/small/86-seel.txt +13 -0
- package/sprites/colorscripts/small/87-dewgong.txt +20 -0
- package/sprites/colorscripts/small/88-grimer.txt +10 -0
- package/sprites/colorscripts/small/89-muk.txt +14 -0
- package/sprites/colorscripts/small/9-blastoise.txt +20 -0
- package/sprites/colorscripts/small/90-shellder.txt +10 -0
- package/sprites/colorscripts/small/91-cloyster.txt +18 -0
- package/sprites/colorscripts/small/92-gastly.txt +12 -0
- package/sprites/colorscripts/small/93-haunter.txt +14 -0
- package/sprites/colorscripts/small/94-gengar.txt +19 -0
- package/sprites/colorscripts/small/95-onix.txt +22 -0
- package/sprites/colorscripts/small/96-drowzee.txt +12 -0
- package/sprites/colorscripts/small/97-hypno.txt +19 -0
- package/sprites/colorscripts/small/98-krabby.txt +12 -0
- package/sprites/colorscripts/small/99-kingler.txt +20 -0
- package/src/engine/constants.ts +121 -0
- package/src/engine/encounter-pool.ts +71 -0
- package/src/engine/encounters.ts +308 -0
- package/src/engine/evolution-data.ts +535 -0
- package/src/engine/evolution.ts +310 -0
- package/src/engine/pokemon-data.ts +1838 -0
- package/src/engine/reactions.ts +877 -0
- package/src/engine/starter-pool.ts +47 -0
- package/src/engine/stats.ts +97 -0
- package/src/engine/types.ts +312 -0
- package/src/engine/xp.ts +135 -0
- package/src/gamification/achievements.ts +204 -0
- package/src/gamification/legendary-quests.ts +265 -0
- package/src/gamification/milestones.ts +86 -0
- package/src/hooks/award-xp.ts +131 -0
- package/src/hooks/increment-counter.ts +27 -0
- package/src/server/index.ts +78 -0
- package/src/server/instructions.ts +194 -0
- package/src/server/tools/achievements.ts +118 -0
- package/src/server/tools/catch.ts +295 -0
- package/src/server/tools/display-helpers.ts +35 -0
- package/src/server/tools/evolve.ts +236 -0
- package/src/server/tools/legendary.ts +78 -0
- package/src/server/tools/party.ts +251 -0
- package/src/server/tools/pet.ts +124 -0
- package/src/server/tools/pokedex.ts +286 -0
- package/src/server/tools/rename.ts +63 -0
- package/src/server/tools/show.ts +136 -0
- package/src/server/tools/starter.ts +175 -0
- package/src/server/tools/stats.ts +123 -0
- package/src/server/tools/visibility.ts +65 -0
- package/src/sprites/index.ts +45 -0
- package/src/state/io.ts +91 -0
- package/src/state/schemas.ts +131 -0
- package/src/state/state-manager.ts +321 -0
- package/statusline/buddy-status.sh +233 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buddy_catch tool — Attempt to catch a wild Pokemon from a pending encounter.
|
|
3
|
+
* Preview mode checks requirements only; confirm mode rolls against catch rate.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import type { OwnedPokemon, CodingStat, WildEncounter } from "../../engine/types.js";
|
|
9
|
+
import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
|
|
10
|
+
import { BELL, MAX_PARTY_SIZE, STAT_DISPLAY_NAMES } from "../../engine/constants.js";
|
|
11
|
+
import { initCodingStats } from "../../engine/stats.js";
|
|
12
|
+
import { canCatch } from "../../engine/encounters.js";
|
|
13
|
+
import { checkNewAchievements, unlockAchievement } from "../../gamification/achievements.js";
|
|
14
|
+
import { StateManager } from "../../state/state-manager.js";
|
|
15
|
+
import { formatTypes, pad } from "./display-helpers.js";
|
|
16
|
+
|
|
17
|
+
/** Check only the stat/level requirements without the catch-rate roll. */
|
|
18
|
+
function meetsRequirements(
|
|
19
|
+
encounter: WildEncounter,
|
|
20
|
+
activePokemon: OwnedPokemon,
|
|
21
|
+
): { met: boolean; reason: string } {
|
|
22
|
+
const species = POKEMON_BY_ID.get(encounter.pokemonId);
|
|
23
|
+
if (!species) return { met: false, reason: "Unknown Pokemon" };
|
|
24
|
+
|
|
25
|
+
const { requiredStat, minStatValue, requiredLevel } = encounter.catchCondition;
|
|
26
|
+
|
|
27
|
+
if (activePokemon.level < requiredLevel) {
|
|
28
|
+
return {
|
|
29
|
+
met: false,
|
|
30
|
+
reason: `Need level ${requiredLevel} to catch ${species.rarity} Pokemon (currently level ${activePokemon.level})`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (requiredStat !== null) {
|
|
35
|
+
const currentStat = activePokemon.codingStats[requiredStat as CodingStat];
|
|
36
|
+
if (currentStat < minStatValue) {
|
|
37
|
+
return {
|
|
38
|
+
met: false,
|
|
39
|
+
reason: `Need ${requiredStat.toUpperCase()} at ${minStatValue} to catch this Pokemon (currently ${currentStat})`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { met: true, reason: "" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Registers the buddy_catch tool on the MCP server. */
|
|
48
|
+
export function registerCatchTool(server: McpServer): void {
|
|
49
|
+
server.tool(
|
|
50
|
+
"buddy_catch",
|
|
51
|
+
"Attempt to catch a wild Pokemon from a pending encounter.",
|
|
52
|
+
{ confirm: z.boolean().optional() },
|
|
53
|
+
async (params) => {
|
|
54
|
+
const stateManager = StateManager.getInstance();
|
|
55
|
+
const state = await stateManager.load();
|
|
56
|
+
|
|
57
|
+
if (!state || state.party.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text" as const,
|
|
62
|
+
text: "You don't have a Pokemon yet! Use buddy_starter to pick your first partner.",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const encounter = state.pendingEncounter;
|
|
70
|
+
if (!encounter) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text" as const,
|
|
75
|
+
text: "No wild Pokemon nearby! Keep coding and one will appear.",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const species = POKEMON_BY_ID.get(encounter.pokemonId);
|
|
82
|
+
if (!species) {
|
|
83
|
+
stateManager.clearPendingEncounter();
|
|
84
|
+
await stateManager.save();
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text" as const,
|
|
89
|
+
text: "The wild Pokemon vanished into the tall grass...",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const active = stateManager.getActivePokemon();
|
|
96
|
+
if (!active) {
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text" as const,
|
|
101
|
+
text: "No active Pokemon found in your party.",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const typeStr = formatTypes(species.types);
|
|
109
|
+
const { catchCondition } = encounter;
|
|
110
|
+
|
|
111
|
+
// Build condition description
|
|
112
|
+
const conditionParts: string[] = [];
|
|
113
|
+
if (catchCondition.requiredLevel > 1) {
|
|
114
|
+
conditionParts.push(`Level ${catchCondition.requiredLevel}+`);
|
|
115
|
+
}
|
|
116
|
+
if (catchCondition.requiredStat !== null) {
|
|
117
|
+
const statName = STAT_DISPLAY_NAMES[catchCondition.requiredStat as CodingStat];
|
|
118
|
+
conditionParts.push(`${statName} >= ${catchCondition.minStatValue}`);
|
|
119
|
+
}
|
|
120
|
+
const conditionStr = conditionParts.length > 0 ? conditionParts.join(", ") : "None";
|
|
121
|
+
|
|
122
|
+
// Check stat/level requirements (does NOT roll catch rate)
|
|
123
|
+
const reqCheck = meetsRequirements(encounter, active);
|
|
124
|
+
|
|
125
|
+
// Requirements not met — Pokemon flees
|
|
126
|
+
if (!reqCheck.met) {
|
|
127
|
+
const W = 42;
|
|
128
|
+
const border = "\u2500".repeat(W);
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
|
|
131
|
+
lines.push(`\u250c${border}\u2510`);
|
|
132
|
+
lines.push(`\u2502 ${pad(`Wild ${species.name} appeared!`, W - 2)}\u2502`);
|
|
133
|
+
lines.push(`\u2502 ${pad(`Type: ${typeStr} Lv.${encounter.level}`, W - 2)}\u2502`);
|
|
134
|
+
lines.push(`\u2502 ${pad(`Catch req: ${conditionStr}`, W - 2)}\u2502`);
|
|
135
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
136
|
+
lines.push(`\u2502 ${pad(reqCheck.reason, W - 2)}\u2502`);
|
|
137
|
+
lines.push(`\u2502 ${pad("It fled! Keep leveling up.", W - 2)}\u2502`);
|
|
138
|
+
lines.push(`\u2514${border}\u2518`);
|
|
139
|
+
|
|
140
|
+
// Clear encounter on flee
|
|
141
|
+
stateManager.clearPendingEncounter();
|
|
142
|
+
await stateManager.save();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Requirements met — show preview (no confirm) or attempt catch (confirm=true)
|
|
150
|
+
if (!params.confirm) {
|
|
151
|
+
const W = 42;
|
|
152
|
+
const border = "\u2500".repeat(W);
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
|
|
155
|
+
lines.push(`\u250c${border}\u2510`);
|
|
156
|
+
lines.push(`\u2502 ${pad(`Wild ${species.name} appeared!`, W - 2)}\u2502`);
|
|
157
|
+
lines.push(`\u2502 ${pad(`Type: ${typeStr} Lv.${encounter.level}`, W - 2)}\u2502`);
|
|
158
|
+
lines.push(`\u2502 ${pad(`Catch req: ${conditionStr}`, W - 2)}\u2502`);
|
|
159
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
160
|
+
lines.push(`\u2502 ${pad(`Rarity: ${species.rarity}`, W - 2)}\u2502`);
|
|
161
|
+
lines.push(`\u2502 ${pad(`"${species.description}"`, W - 2)}\u2502`);
|
|
162
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
163
|
+
lines.push(`\u2502 ${pad("Use /buddy catch confirm to throw a Pokeball!", W - 2)}\u2502`);
|
|
164
|
+
lines.push(`\u2514${border}\u2518`);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Confirm — roll against catch rate via canCatch()
|
|
172
|
+
const catchResult = canCatch(encounter, active);
|
|
173
|
+
|
|
174
|
+
if (!catchResult.success) {
|
|
175
|
+
// Catch rate roll failed — broke free
|
|
176
|
+
const W = 42;
|
|
177
|
+
const border = "\u2500".repeat(W);
|
|
178
|
+
const lines: string[] = [];
|
|
179
|
+
|
|
180
|
+
lines.push(`\u250c${border}\u2510`);
|
|
181
|
+
lines.push(`\u2502 ${pad(`You threw a Pokeball at ${species.name}!`, W - 2)}\u2502`);
|
|
182
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
183
|
+
lines.push(`\u2502 ${pad(catchResult.reason, W - 2)}\u2502`);
|
|
184
|
+
lines.push(`\u2502 ${pad("Better luck next time...", W - 2)}\u2502`);
|
|
185
|
+
lines.push(`\u2514${border}\u2518`);
|
|
186
|
+
|
|
187
|
+
// Clear encounter on failed catch
|
|
188
|
+
stateManager.clearPendingEncounter();
|
|
189
|
+
await stateManager.save();
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Catch succeeded — create OwnedPokemon
|
|
197
|
+
const now = new Date().toISOString();
|
|
198
|
+
const newPokemon: OwnedPokemon = {
|
|
199
|
+
id: crypto.randomUUID(),
|
|
200
|
+
pokemonId: encounter.pokemonId,
|
|
201
|
+
nickname: null,
|
|
202
|
+
level: encounter.level,
|
|
203
|
+
currentXp: 0,
|
|
204
|
+
totalXp: 0,
|
|
205
|
+
codingStats: initCodingStats(species.baseStats),
|
|
206
|
+
happiness: 70,
|
|
207
|
+
caughtAt: now,
|
|
208
|
+
evolvedAt: null,
|
|
209
|
+
isActive: false,
|
|
210
|
+
personality: null,
|
|
211
|
+
shiny: false,
|
|
212
|
+
isStarter: false,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Add to party or PC box
|
|
216
|
+
let placedIn: "party" | "pcBox";
|
|
217
|
+
if (state.party.length < MAX_PARTY_SIZE) {
|
|
218
|
+
state.party.push(newPokemon);
|
|
219
|
+
placedIn = "party";
|
|
220
|
+
} else {
|
|
221
|
+
state.pcBox.push(newPokemon);
|
|
222
|
+
placedIn = "pcBox";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Update pokedex
|
|
226
|
+
const existingEntry = state.pokedex.entries[encounter.pokemonId];
|
|
227
|
+
if (existingEntry) {
|
|
228
|
+
if (!existingEntry.caught) {
|
|
229
|
+
existingEntry.caught = true;
|
|
230
|
+
existingEntry.firstCaught = now;
|
|
231
|
+
state.pokedex.totalCaught += 1;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
state.pokedex.entries[encounter.pokemonId] = {
|
|
235
|
+
seen: true,
|
|
236
|
+
caught: true,
|
|
237
|
+
firstSeen: now,
|
|
238
|
+
firstCaught: now,
|
|
239
|
+
};
|
|
240
|
+
state.pokedex.totalSeen += 1;
|
|
241
|
+
state.pokedex.totalCaught += 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Clear pending encounter
|
|
245
|
+
stateManager.clearPendingEncounter();
|
|
246
|
+
|
|
247
|
+
// Check for new achievements
|
|
248
|
+
const newAchievements = checkNewAchievements(state);
|
|
249
|
+
for (const achievement of newAchievements) {
|
|
250
|
+
state.achievements.push(unlockAchievement(achievement.id));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Save state
|
|
254
|
+
await stateManager.save();
|
|
255
|
+
await stateManager.writeStatus();
|
|
256
|
+
|
|
257
|
+
// Build celebration message
|
|
258
|
+
const W = 42;
|
|
259
|
+
const border = "\u2500".repeat(W);
|
|
260
|
+
const lines: string[] = [];
|
|
261
|
+
|
|
262
|
+
lines.push(`\u250c${border}\u2510`);
|
|
263
|
+
lines.push(`\u2502 ${pad(`Gotcha! ${species.name} was caught!`, W - 2)}\u2502`);
|
|
264
|
+
lines.push(`\u2502 ${pad(`Type: ${typeStr} Lv.${encounter.level}`, W - 2)}\u2502`);
|
|
265
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
266
|
+
|
|
267
|
+
if (placedIn === "party") {
|
|
268
|
+
lines.push(`\u2502 ${pad(`${species.name} joined your party!`, W - 2)}\u2502`);
|
|
269
|
+
} else {
|
|
270
|
+
lines.push(`\u2502 ${pad(`Party full! Sent to PC Box.`, W - 2)}\u2502`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
lines.push(`\u2502 ${pad(`Pokedex: ${state.pokedex.totalCaught}/151 caught`, W - 2)}\u2502`);
|
|
274
|
+
lines.push(`\u2514${border}\u2518`);
|
|
275
|
+
|
|
276
|
+
// Show new achievements
|
|
277
|
+
if (newAchievements.length > 0) {
|
|
278
|
+
lines.push("");
|
|
279
|
+
for (const achievement of newAchievements) {
|
|
280
|
+
lines.push(`*** Achievement unlocked: ${achievement.name}! ***`);
|
|
281
|
+
lines.push(` ${achievement.description}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Terminal bell
|
|
286
|
+
if (state.config.bellEnabled) {
|
|
287
|
+
process.stderr.write(BELL);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display helper functions used across multiple tool files.
|
|
3
|
+
* Centralizes formatting logic to eliminate duplication.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CodingStat, BaseStats } from "../../engine/types.js";
|
|
7
|
+
import { BASE_STAT_TO_CODING } from "../../engine/types.js";
|
|
8
|
+
|
|
9
|
+
/** Format a Pokemon type array for display (e.g., "Fire/Flying"). */
|
|
10
|
+
export function formatTypes(types: readonly [string, string?]): string {
|
|
11
|
+
return types.filter(Boolean).join("/");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Right-pad a string to a fixed width, truncating if needed. */
|
|
15
|
+
export function pad(str: string, width: number): string {
|
|
16
|
+
if (str.length >= width) {
|
|
17
|
+
return str.slice(0, width);
|
|
18
|
+
}
|
|
19
|
+
return str + " ".repeat(width - str.length);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Render a compact XP progress bar using Unicode block characters. */
|
|
23
|
+
export function renderXpBar(percent: number, width: number = 20): string {
|
|
24
|
+
const filled = Math.round((percent / 100) * width);
|
|
25
|
+
const empty = width - filled;
|
|
26
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Inverted BASE_STAT_TO_CODING map: coding stat -> base stat key.
|
|
31
|
+
* Used for stat comparison during evolution preview and Pokedex display.
|
|
32
|
+
*/
|
|
33
|
+
export const CODING_TO_BASE: Record<CodingStat, keyof BaseStats> = Object.fromEntries(
|
|
34
|
+
Object.entries(BASE_STAT_TO_CODING).map(([base, coding]) => [coding, base]),
|
|
35
|
+
) as Record<CodingStat, keyof BaseStats>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buddy_evolve tool — Evolve the active Pokemon when eligible.
|
|
3
|
+
* Shows a preview with stat comparison, or applies the evolution on confirm.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import type { EvolutionMethod, CodingStat } from "../../engine/types.js";
|
|
9
|
+
import { CODING_STATS } from "../../engine/types.js";
|
|
10
|
+
import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
|
|
11
|
+
import { STAT_DISPLAY_NAMES, BELL, BADGES } from "../../engine/constants.js";
|
|
12
|
+
import { renderStatBar } from "../../engine/stats.js";
|
|
13
|
+
import {
|
|
14
|
+
checkEvolution,
|
|
15
|
+
applyEvolution,
|
|
16
|
+
getEvolutionLinks,
|
|
17
|
+
getNewlyEarnedBadges,
|
|
18
|
+
} from "../../engine/evolution.js";
|
|
19
|
+
import { StateManager } from "../../state/state-manager.js";
|
|
20
|
+
import { formatTypes, pad, CODING_TO_BASE } from "./display-helpers.js";
|
|
21
|
+
|
|
22
|
+
/** Describe an evolution method in human-readable text. */
|
|
23
|
+
function describeMethod(method: EvolutionMethod): string {
|
|
24
|
+
switch (method.type) {
|
|
25
|
+
case "level":
|
|
26
|
+
return `Reach Lv.${method.level}`;
|
|
27
|
+
case "badge": {
|
|
28
|
+
const badgeDef = BADGES.find((b) => b.type === method.badge);
|
|
29
|
+
const badgeName = badgeDef ? badgeDef.name : method.badge;
|
|
30
|
+
return `Earn the ${badgeName}`;
|
|
31
|
+
}
|
|
32
|
+
case "collaboration":
|
|
33
|
+
return "Merge 10 PRs (collaboration evolution)";
|
|
34
|
+
case "stat":
|
|
35
|
+
return `Reach Lv.25 with ${STAT_DISPLAY_NAMES[method.stat]} >= ${method.minValue}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Registers the buddy_evolve tool on the MCP server. */
|
|
40
|
+
export function registerEvolveTool(server: McpServer): void {
|
|
41
|
+
server.tool(
|
|
42
|
+
"buddy_evolve",
|
|
43
|
+
"Evolve your active Pokemon if it meets the evolution requirements.",
|
|
44
|
+
{ confirm: z.boolean().optional() },
|
|
45
|
+
async (params) => {
|
|
46
|
+
const stateManager = StateManager.getInstance();
|
|
47
|
+
const state = await stateManager.load();
|
|
48
|
+
|
|
49
|
+
if (!state || state.party.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text" as const,
|
|
54
|
+
text: "You don't have a Pokemon yet! Use buddy_starter to pick your first partner.",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const active = stateManager.getActivePokemon();
|
|
62
|
+
if (!active) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text" as const,
|
|
67
|
+
text: "No active Pokemon found in your party.",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const species = POKEMON_BY_ID.get(active.pokemonId);
|
|
75
|
+
if (!species) {
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text" as const,
|
|
80
|
+
text: `Could not find species data for Pokemon ID ${active.pokemonId}.`,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for newly earned badges before evolution check
|
|
88
|
+
const newBadges = getNewlyEarnedBadges(state);
|
|
89
|
+
const badgeLines: string[] = [];
|
|
90
|
+
if (newBadges.length > 0) {
|
|
91
|
+
for (const badge of newBadges) {
|
|
92
|
+
state.badges.push(badge);
|
|
93
|
+
const badgeDef = BADGES.find((b) => b.type === badge);
|
|
94
|
+
const badgeName = badgeDef ? badgeDef.name : badge;
|
|
95
|
+
badgeLines.push(`*** New badge earned: ${badgeName}! ***`);
|
|
96
|
+
}
|
|
97
|
+
await stateManager.save();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check evolution eligibility
|
|
101
|
+
const eligibleLink = checkEvolution(active, state);
|
|
102
|
+
|
|
103
|
+
if (!eligibleLink) {
|
|
104
|
+
// Not eligible — explain what's needed
|
|
105
|
+
const links = getEvolutionLinks(active.pokemonId);
|
|
106
|
+
const displayName = active.nickname ?? species.name;
|
|
107
|
+
const lines: string[] = [];
|
|
108
|
+
|
|
109
|
+
if (badgeLines.length > 0) {
|
|
110
|
+
lines.push(...badgeLines, "");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (links.length === 0) {
|
|
114
|
+
lines.push(`${displayName} does not evolve.`);
|
|
115
|
+
} else {
|
|
116
|
+
lines.push(`${displayName} is not ready to evolve yet.`);
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push("Evolution requirements:");
|
|
119
|
+
for (const link of links) {
|
|
120
|
+
const target = POKEMON_BY_ID.get(link.to);
|
|
121
|
+
const targetName = target ? target.name : `#${link.to}`;
|
|
122
|
+
lines.push(` -> ${targetName}: ${describeMethod(link.method)}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push(`Current level: ${active.level}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const targetSpecies = POKEMON_BY_ID.get(eligibleLink.to);
|
|
134
|
+
if (!targetSpecies) {
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: "text" as const,
|
|
139
|
+
text: `Could not find target species data for Pokemon ID ${eligibleLink.to}.`,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
isError: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const displayName = active.nickname ?? species.name;
|
|
147
|
+
|
|
148
|
+
// Preview mode: show what the evolution will look like
|
|
149
|
+
if (!params.confirm) {
|
|
150
|
+
const W = 42;
|
|
151
|
+
const border = "\u2500".repeat(W);
|
|
152
|
+
const lines: string[] = [];
|
|
153
|
+
|
|
154
|
+
if (badgeLines.length > 0) {
|
|
155
|
+
lines.push(...badgeLines, "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lines.push(`\u250c${border}\u2510`);
|
|
159
|
+
lines.push(`\u2502 ${pad(`What? ${displayName} is evolving!`, W - 2)}\u2502`);
|
|
160
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
161
|
+
lines.push(`\u2502 ${pad(`${species.name} \u2192 ${targetSpecies.name}`, W - 2)}\u2502`);
|
|
162
|
+
lines.push(
|
|
163
|
+
`\u2502 ${pad(`${formatTypes(species.types)} ${formatTypes(targetSpecies.types)}`, W - 2)}\u2502`,
|
|
164
|
+
);
|
|
165
|
+
lines.push(`\u2502 ${pad(`Lv.${active.level}`, W - 2)}\u2502`);
|
|
166
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
167
|
+
lines.push(`\u2502 ${pad("New base stats:", W - 2)}\u2502`);
|
|
168
|
+
|
|
169
|
+
// Show stat comparison (new base vs current)
|
|
170
|
+
for (const stat of CODING_STATS) {
|
|
171
|
+
const currentValue = active.codingStats[stat as CodingStat];
|
|
172
|
+
const label = STAT_DISPLAY_NAMES[stat as CodingStat].padEnd(10);
|
|
173
|
+
// Calculate what new stat would be after evolution (preview)
|
|
174
|
+
const oldBaseContrib = Math.floor(
|
|
175
|
+
species.baseStats[CODING_TO_BASE[stat as CodingStat]] * 0.5,
|
|
176
|
+
);
|
|
177
|
+
const activityBonus = Math.max(0, currentValue - oldBaseContrib);
|
|
178
|
+
const newBaseContrib = Math.floor(
|
|
179
|
+
targetSpecies.baseStats[CODING_TO_BASE[stat as CodingStat]] * 0.5,
|
|
180
|
+
);
|
|
181
|
+
const newValue = newBaseContrib + activityBonus;
|
|
182
|
+
const bar = renderStatBar(newValue);
|
|
183
|
+
const delta = newValue !== currentValue ? ` (was ${currentValue})` : "";
|
|
184
|
+
lines.push(
|
|
185
|
+
`\u2502 ${pad(`${label} ${bar} ${String(newValue).padStart(3)}${delta}`, W - 2)}\u2502`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
190
|
+
lines.push(`\u2502 ${pad("Use /buddy evolve confirm to proceed", W - 2)}\u2502`);
|
|
191
|
+
lines.push(`\u2502 ${pad("(or don't -- like pressing B!)", W - 2)}\u2502`);
|
|
192
|
+
lines.push(`\u2514${border}\u2518`);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Confirm mode: apply evolution
|
|
200
|
+
const { newName, newTypes } = applyEvolution(active, eligibleLink.to);
|
|
201
|
+
|
|
202
|
+
// Save state
|
|
203
|
+
await stateManager.save();
|
|
204
|
+
await stateManager.writeStatus();
|
|
205
|
+
|
|
206
|
+
// Build celebration message
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
|
|
209
|
+
if (badgeLines.length > 0) {
|
|
210
|
+
lines.push(...badgeLines, "");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
lines.push(`Congratulations! ${displayName} evolved into ${newName}!`);
|
|
214
|
+
lines.push(`Type: ${formatTypes(newTypes as readonly [string, string?])}`);
|
|
215
|
+
lines.push("");
|
|
216
|
+
|
|
217
|
+
// Show new stats
|
|
218
|
+
lines.push("Updated stats:");
|
|
219
|
+
for (const stat of CODING_STATS) {
|
|
220
|
+
const value = active.codingStats[stat as CodingStat];
|
|
221
|
+
const label = STAT_DISPLAY_NAMES[stat as CodingStat].padEnd(10);
|
|
222
|
+
const bar = renderStatBar(value);
|
|
223
|
+
lines.push(` ${label} ${bar} ${String(value).padStart(3)}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Terminal bell
|
|
227
|
+
if (state.config.bellEnabled) {
|
|
228
|
+
process.stderr.write(BELL);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buddy_legendary tool — Display progress on all 5 legendary quest chains.
|
|
3
|
+
* Each quest has multi-step conditions tied to sustained coding activity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
|
|
8
|
+
import { getQuestProgress } from "../../gamification/legendary-quests.js";
|
|
9
|
+
import { StateManager } from "../../state/state-manager.js";
|
|
10
|
+
import { pad } from "./display-helpers.js";
|
|
11
|
+
|
|
12
|
+
/** Registers the buddy_legendary tool on the MCP server. */
|
|
13
|
+
export function registerLegendaryTool(server: McpServer): void {
|
|
14
|
+
server.tool(
|
|
15
|
+
"buddy_legendary",
|
|
16
|
+
"View progress on all legendary Pokemon quest chains.",
|
|
17
|
+
{},
|
|
18
|
+
async () => {
|
|
19
|
+
const stateManager = StateManager.getInstance();
|
|
20
|
+
const state = await stateManager.load();
|
|
21
|
+
|
|
22
|
+
if (!state) {
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: "text" as const,
|
|
27
|
+
text: "You don't have a Pokemon yet! Use buddy_starter to begin your journey.",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
isError: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const progress = getQuestProgress(state);
|
|
35
|
+
|
|
36
|
+
const W = 48;
|
|
37
|
+
const border = "\u2500".repeat(W);
|
|
38
|
+
const lines: string[] = [];
|
|
39
|
+
|
|
40
|
+
lines.push("L E G E N D A R Y Q U E S T S");
|
|
41
|
+
lines.push("");
|
|
42
|
+
|
|
43
|
+
for (const entry of progress) {
|
|
44
|
+
const { quest, stepsCompleted, totalSteps } = entry;
|
|
45
|
+
const species = POKEMON_BY_ID.get(quest.pokemonId);
|
|
46
|
+
const pokemonName = species?.name ?? `#${quest.pokemonId}`;
|
|
47
|
+
|
|
48
|
+
lines.push(
|
|
49
|
+
`\u250c\u2500 LEGENDARY QUEST: ${pokemonName} ${"\u2500".repeat(Math.max(0, W - 24 - pokemonName.length))}\u2510`,
|
|
50
|
+
);
|
|
51
|
+
lines.push(`\u2502 ${pad(quest.name, W - 2)}\u2502`);
|
|
52
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < quest.steps.length; i++) {
|
|
55
|
+
const step = quest.steps[i]!;
|
|
56
|
+
const completed = i < stepsCompleted;
|
|
57
|
+
const marker = completed ? "\u2713" : "\u25cb";
|
|
58
|
+
const stepText = `Step ${i + 1}: ${marker} ${step.description}`;
|
|
59
|
+
lines.push(`\u2502 ${pad(stepText, W - 2)}\u2502`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lines.push(`\u2502${" ".repeat(W)}\u2502`);
|
|
63
|
+
|
|
64
|
+
const allDone = stepsCompleted === totalSteps;
|
|
65
|
+
const progressStr = allDone
|
|
66
|
+
? `COMPLETE! ${pokemonName} awaits you!`
|
|
67
|
+
: `Progress: ${stepsCompleted}/${totalSteps}`;
|
|
68
|
+
lines.push(`\u2502 ${pad(progressStr, W - 2)}\u2502`);
|
|
69
|
+
lines.push(`\u2514${border}\u2518`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
}
|