@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
@@ -0,0 +1,180 @@
1
+ /**
2
+ * buddy_train tool — Train your active Pokemon's coding stats.
3
+ * Has a 30-minute cooldown. Awards +3 to a stat and +5 XP.
4
+ */
5
+
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { z } from "zod";
8
+ import type { 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 } from "../../engine/constants.js";
12
+ import { addXp } from "../../engine/xp.js";
13
+ import { applyStatBoost, renderStatBar } from "../../engine/stats.js";
14
+ import { StateManager } from "../../state/state-manager.js";
15
+
16
+ /** Cooldown: 30 minutes in milliseconds. */
17
+ const TRAIN_COOLDOWN_MS = 1_800_000;
18
+
19
+ /** Stat boost per training session. */
20
+ const TRAIN_STAT_BOOST = 3;
21
+
22
+ /** XP awarded per training session. */
23
+ const TRAIN_XP_AWARD = 5;
24
+
25
+ /** Pick a random element from a non-empty array. */
26
+ function randomPick<T>(arr: readonly T[]): T {
27
+ const index = Math.floor(Math.random() * arr.length);
28
+ return arr[index] as T;
29
+ }
30
+
31
+ /** Format remaining cooldown as "Xm Ys". */
32
+ function formatCooldownRemaining(remainingMs: number): string {
33
+ const totalSeconds = Math.ceil(remainingMs / 1000);
34
+ const minutes = Math.floor(totalSeconds / 60);
35
+ const seconds = totalSeconds % 60;
36
+ if (minutes > 0 && seconds > 0) {
37
+ return `${minutes}m ${seconds}s`;
38
+ }
39
+ if (minutes > 0) {
40
+ return `${minutes}m`;
41
+ }
42
+ return `${seconds}s`;
43
+ }
44
+
45
+ /** Registers the buddy_train tool on the MCP server. */
46
+ export function registerTrainTool(server: McpServer): void {
47
+ server.tool(
48
+ "buddy_train",
49
+ "Train your active Pokemon's coding stats. Optionally specify a stat (debugging, stability, velocity, wisdom, stamina). 30-minute cooldown.",
50
+ {
51
+ stat: z
52
+ .string()
53
+ .optional()
54
+ .describe(
55
+ "Stat to train: debugging, stability, velocity, wisdom, or stamina. Random if omitted.",
56
+ ),
57
+ },
58
+ async ({ stat: statInput }) => {
59
+ const stateManager = StateManager.getInstance();
60
+ const state = await stateManager.load();
61
+
62
+ if (!state || state.party.length === 0) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text" as const,
67
+ text: "You don't have a Pokemon yet! Use buddy_starter to pick your first companion.",
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ const active = stateManager.getActivePokemon();
74
+ if (!active) {
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text" as const,
79
+ text: "No active Pokemon found. Use buddy_party to set one active.",
80
+ },
81
+ ],
82
+ };
83
+ }
84
+
85
+ const species = POKEMON_BY_ID.get(active.pokemonId);
86
+ if (!species) {
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text" as const,
91
+ text: `Could not find species data for Pokemon ID ${active.pokemonId}.`,
92
+ },
93
+ ],
94
+ isError: true,
95
+ };
96
+ }
97
+
98
+ // Check cooldown
99
+ const now = Date.now();
100
+ const elapsed = now - state.lastTrainedAt;
101
+ if (elapsed < TRAIN_COOLDOWN_MS) {
102
+ const remaining = TRAIN_COOLDOWN_MS - elapsed;
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text" as const,
107
+ text: `Still resting from training! Try again in ${formatCooldownRemaining(remaining)}.`,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+
113
+ // Determine which stat to train
114
+ let targetStat: CodingStat;
115
+ if (statInput !== undefined && statInput !== "") {
116
+ const normalized = statInput.toLowerCase().trim();
117
+ if (!CODING_STATS.includes(normalized as CodingStat)) {
118
+ const validStats = CODING_STATS.map((s) => STAT_DISPLAY_NAMES[s].toLowerCase()).join(
119
+ ", ",
120
+ );
121
+ return {
122
+ content: [
123
+ {
124
+ type: "text" as const,
125
+ text: `Invalid stat "${statInput}". Valid stats: ${validStats}`,
126
+ },
127
+ ],
128
+ };
129
+ }
130
+ targetStat = normalized as CodingStat;
131
+ } else {
132
+ targetStat = randomPick(CODING_STATS);
133
+ }
134
+
135
+ const displayName = active.nickname ?? species.name;
136
+
137
+ // Record stat before boost for display
138
+ const statBefore = active.codingStats[targetStat];
139
+
140
+ // Apply stat boost
141
+ applyStatBoost(active, targetStat, TRAIN_STAT_BOOST);
142
+
143
+ const statAfter = active.codingStats[targetStat];
144
+
145
+ // Award XP
146
+ const levelUp = addXp(active, TRAIN_XP_AWARD, species);
147
+
148
+ // Set mood to energetic
149
+ state.mood = "energetic";
150
+ state.moodSetAt = now;
151
+
152
+ // Record train timestamp
153
+ state.lastTrainedAt = now;
154
+
155
+ // Save state and update status line
156
+ await stateManager.save();
157
+ await stateManager.writeStatus();
158
+
159
+ // Build response
160
+ const statDisplayName = STAT_DISPLAY_NAMES[targetStat];
161
+ const lines: string[] = [];
162
+
163
+ lines.push(`*${displayName} trains its ${statDisplayName}! +${TRAIN_STAT_BOOST}*`);
164
+ lines.push("");
165
+ lines.push(
166
+ `${statDisplayName}: ${statBefore} \u2192 ${statAfter} ${renderStatBar(statAfter)}`,
167
+ );
168
+ lines.push(`+${TRAIN_XP_AWARD} XP`);
169
+
170
+ if (levelUp) {
171
+ lines.push("");
172
+ lines.push(`*** ${displayName} grew to Lv.${levelUp.newLevel}! ***`);
173
+ }
174
+
175
+ return {
176
+ content: [{ type: "text" as const, text: lines.join("\n") }],
177
+ };
178
+ },
179
+ );
180
+ }
@@ -9,6 +9,7 @@ import {
9
9
  BADGE_TYPES,
10
10
  CODING_STATS,
11
11
  EVENT_COUNTER_KEYS,
12
+ MOOD_TYPES,
12
13
  } from "../engine/types.js";
13
14
 
14
15
  // ---- Shared Primitives ----
@@ -69,6 +70,8 @@ export const BuddyConfigSchema = z.object({
69
70
  reactionCooldownMs: z.number().int().min(0).default(30_000),
70
71
  statusLineEnabled: z.boolean().default(true),
71
72
  bellEnabled: z.boolean().default(true),
73
+ encounterSpeed: z.enum(["fast", "normal", "slow"]).default("normal"),
74
+ xpSharePercent: z.number().min(0).max(100).default(25),
72
75
  });
73
76
 
74
77
  // ---- Pokedex ----
@@ -93,6 +96,15 @@ export const UnlockedAchievementSchema = z.object({
93
96
  unlockedAt: z.string(),
94
97
  });
95
98
 
99
+ // ---- Pending Quiz ----
100
+
101
+ export const PendingQuizSchema = z.object({
102
+ type: z.enum(["type_matchup", "stat_compare", "evolution", "pokedex_trivia"]),
103
+ question: z.string(),
104
+ options: z.array(z.string()),
105
+ correctAnswer: z.number().int().min(1).max(4),
106
+ });
107
+
96
108
  // ---- Catch Condition ----
97
109
 
98
110
  export const CatchConditionSchema = z.object({
@@ -127,5 +139,13 @@ export const PlayerStateSchema = z.object({
127
139
  totalSessions: z.number().int().min(0).default(0),
128
140
  pendingEncounter: WildEncounterSchema.nullable().default(null),
129
141
  xpSinceLastEncounter: z.number().int().min(0).default(0),
142
+ recentToolTypes: z.array(z.string()).default([]),
143
+ lastEncounterTime: z.number().int().min(0).default(0),
144
+ mood: z.enum(MOOD_TYPES).default("neutral"),
145
+ moodSetAt: z.number().default(0),
146
+ lastFedAt: z.number().default(0),
147
+ lastTrainedAt: z.number().default(0),
148
+ lastPlayedAt: z.number().default(0),
149
+ pendingQuiz: PendingQuizSchema.nullable().default(null),
130
150
  });
131
151
 
@@ -13,6 +13,7 @@ import type {
13
13
  BuddyConfig,
14
14
  PokedexState,
15
15
  WildEncounter,
16
+ MoodType,
16
17
  } from "../engine/types.js";
17
18
  import { EVENT_COUNTER_KEYS } from "../engine/types.js";
18
19
  import { atomicWrite, safeRead, ensureDir, backupCorrupted, withLock } from "./io.js";
@@ -25,6 +26,7 @@ interface StatusPayload {
25
26
  xpPercent: number;
26
27
  speciesId: number;
27
28
  evolutionReady: boolean;
29
+ mood: MoodType;
28
30
  }
29
31
 
30
32
  /** Build a zeroed-out EventCounters record */
@@ -53,6 +55,8 @@ function defaultConfig(): BuddyConfig {
53
55
  reactionCooldownMs: 30_000,
54
56
  statusLineEnabled: true,
55
57
  bellEnabled: true,
58
+ encounterSpeed: "normal",
59
+ xpSharePercent: 25,
56
60
  };
57
61
  }
58
62
 
@@ -188,6 +192,14 @@ export class StateManager {
188
192
  totalSessions: 0,
189
193
  pendingEncounter: null,
190
194
  xpSinceLastEncounter: 0,
195
+ recentToolTypes: [],
196
+ lastEncounterTime: 0,
197
+ mood: "neutral",
198
+ moodSetAt: 0,
199
+ lastFedAt: 0,
200
+ lastTrainedAt: 0,
201
+ lastPlayedAt: 0,
202
+ pendingQuiz: null,
191
203
  };
192
204
 
193
205
  this.state = state;
@@ -210,19 +222,21 @@ export class StateManager {
210
222
  await this.save();
211
223
  }
212
224
 
213
- /** Update daily streak based on today vs lastActiveDate, then save */
225
+ /**
226
+ * Update daily streak with weekend grace period.
227
+ * Allows up to 2 days off without breaking the streak (covers weekends).
228
+ * Streak counts "coding days" not "consecutive calendar days".
229
+ */
214
230
  async updateStreak(): Promise<void> {
215
231
  const state = this.getState();
216
232
  const today = todayDateString();
217
233
  const { streak } = state;
218
234
 
219
235
  if (streak.lastActiveDate === today) {
220
- // Already recorded today, nothing to do
221
236
  return;
222
237
  }
223
238
 
224
239
  if (streak.lastActiveDate === null) {
225
- // First ever activity
226
240
  streak.currentStreak = 1;
227
241
  streak.totalDaysActive = 1;
228
242
  } else {
@@ -231,11 +245,14 @@ export class StateManager {
231
245
  const diffMs = now.getTime() - last.getTime();
232
246
  const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
233
247
 
234
- if (diffDays === 1) {
235
- // Consecutive day
248
+ // Grace period: up to 2 days off (covers weekends)
249
+ // 1 day gap = next day (consecutive) ✓
250
+ // 2 day gap = skipped 1 day (e.g., Friday → Sunday) ✓
251
+ // 3 day gap = skipped 2 days (e.g., Friday → Monday) ✓
252
+ // 4+ day gap = streak broken
253
+ if (diffDays <= 3) {
236
254
  streak.currentStreak += 1;
237
255
  } else {
238
- // Streak broken
239
256
  streak.currentStreak = 1;
240
257
  }
241
258
  streak.totalDaysActive += 1;
@@ -256,6 +273,8 @@ export class StateManager {
256
273
  return;
257
274
  }
258
275
 
276
+ const state = this.getState();
277
+
259
278
  // XP percent is currentXp as a rough percentage toward next level
260
279
  // Exact formula depends on exp group; use a simple ratio for the status line
261
280
  const xpPercent = active.level >= 100 ? 100 : Math.min(100, Math.floor((active.currentXp / Math.max(1, active.currentXp + 50)) * 100));
@@ -274,6 +293,7 @@ export class StateManager {
274
293
  xpPercent,
275
294
  speciesId: active.pokemonId,
276
295
  evolutionReady,
296
+ mood: state.mood ?? "neutral",
277
297
  };
278
298
 
279
299
  const stateDir = getStateDir();
@@ -56,6 +56,7 @@ SPECIES_ID=$(echo "$STATUS" | jq -r '.speciesId // 0')
56
56
  EVOLVING=$(echo "$STATUS" | jq -r '.evolutionReady // false')
57
57
  REACTION=$(echo "$STATUS" | jq -r '.reaction // empty')
58
58
  ENCOUNTER=$(echo "$STATUS" | jq -r '.encounter // empty')
59
+ MOOD=$(echo "$STATUS" | jq -r '.mood // "neutral"')
59
60
 
60
61
  # ── Colors ──────────────────────────────────────────────────
61
62
  NC=$'\033[0m'
@@ -158,69 +159,83 @@ if [ -n "$ENCOUNTER" ]; then
158
159
  elif [ -n "$REACTION" ]; then
159
160
  SPEECH="$REACTION"
160
161
  else
162
+ # Mood-based speeches — pick from mood-specific arrays
161
163
  NOW=$(date +%s)
162
- IDX=$(( (NOW / 30) % 60 ))
163
- SPEECHES=(
164
- "*${NAME} looks at your code curiously*"
165
- ""
166
- "*${NAME} nods along as you type*"
167
- ""
168
- "*${NAME} is watching closely*"
169
- ""
170
- "*${NAME} hums softly*"
171
- ""
172
- "*${NAME} stretches and yawns*"
173
- ""
174
- "*${NAME} bounces excitedly*"
175
- ""
176
- "*${NAME} waits patiently*"
177
- ""
178
- "*${NAME} tilts head at the screen*"
179
- ""
180
- "*${NAME} chirps encouragingly*"
181
- ""
182
- "*${NAME} peers at a variable name*"
183
- ""
184
- "*${NAME} sniffs at a function*"
185
- ""
186
- "*${NAME} sits on the keyboard*"
187
- ""
188
- "*${NAME} chases the cursor*"
189
- ""
190
- "*${NAME} judges your indentation*"
191
- ""
192
- "*${NAME} found a semicolon!*"
193
- ""
194
- "*${NAME} debugs alongside you*"
195
- ""
196
- "*${NAME} spots a typo... maybe*"
197
- ""
198
- "*${NAME} celebrates a clean build*"
199
- ""
200
- "*${NAME} is impressed by that refactor*"
201
- ""
202
- "*${NAME} blinks at the test results*"
203
- ""
204
- "*${NAME} dreams of evolution*"
205
- ""
206
- "*${NAME} wants to learn new moves*"
207
- ""
208
- "*${NAME} is proud of your progress*"
209
- ""
210
- "*${NAME} snoozes between commits*"
211
- ""
212
- "*${NAME} practices its type moves*"
213
- ""
214
- "*${NAME} stares at the linter output*"
215
- ""
216
- "*${NAME} wonders about that TODO*"
217
- ""
218
- "*${NAME} approves of that commit msg*"
219
- ""
220
- "*${NAME} is ready for action!*"
221
- ""
222
- )
223
- SPEECH="${SPEECHES[$IDX]}"
164
+
165
+ case "$MOOD" in
166
+ happy)
167
+ MOOD_SPEECHES=(
168
+ "*${NAME} is beaming with pride!*"
169
+ "*${NAME} does a little victory dance*"
170
+ "*${NAME} radiates positive energy*"
171
+ "*${NAME} bounces happily*"
172
+ "*${NAME} gives you a thumbs up*"
173
+ )
174
+ ;;
175
+ worried)
176
+ MOOD_SPEECHES=(
177
+ "*${NAME} looks concerned...*"
178
+ "*${NAME} nervously watches the errors*"
179
+ "*${NAME} hides behind the terminal*"
180
+ "*${NAME} paces back and forth*"
181
+ "*${NAME} offers you a virtual hug*"
182
+ )
183
+ ;;
184
+ sleepy)
185
+ MOOD_SPEECHES=(
186
+ "*${NAME} yawns widely*"
187
+ "*${NAME} dozes off... zzz*"
188
+ "*${NAME} rubs its eyes*"
189
+ "*${NAME} curls up near the keyboard*"
190
+ "*${NAME} mumbles in its sleep*"
191
+ )
192
+ ;;
193
+ energetic)
194
+ MOOD_SPEECHES=(
195
+ "*${NAME} is fired up! Let's go!*"
196
+ "*${NAME} bounces off the walls*"
197
+ "*${NAME} can't sit still!*"
198
+ "*${NAME} is ready to code all day!*"
199
+ "*${NAME} stretches and flexes*"
200
+ )
201
+ ;;
202
+ proud)
203
+ MOOD_SPEECHES=(
204
+ "*${NAME} puffs up with pride*"
205
+ "*${NAME} strikes a victory pose*"
206
+ "*${NAME} shows off to everyone*"
207
+ "*${NAME} earned bragging rights!*"
208
+ "*${NAME} stands tall and proud*"
209
+ )
210
+ ;;
211
+ *)
212
+ # neutral / default — use the original idle speeches
213
+ MOOD_SPEECHES=(
214
+ "*${NAME} looks at your code curiously*"
215
+ "*${NAME} nods along as you type*"
216
+ "*${NAME} is watching closely*"
217
+ "*${NAME} hums softly*"
218
+ "*${NAME} waits patiently*"
219
+ "*${NAME} tilts head at the screen*"
220
+ "*${NAME} chirps encouragingly*"
221
+ "*${NAME} peers at a variable name*"
222
+ "*${NAME} sniffs at a function*"
223
+ "*${NAME} sits on the keyboard*"
224
+ "*${NAME} chases the cursor*"
225
+ "*${NAME} judges your indentation*"
226
+ "*${NAME} found a semicolon!*"
227
+ "*${NAME} debugs alongside you*"
228
+ "*${NAME} spots a typo... maybe*"
229
+ "*${NAME} celebrates a clean build*"
230
+ "*${NAME} dreams of evolution*"
231
+ "*${NAME} is ready for action!*"
232
+ )
233
+ ;;
234
+ esac
235
+
236
+ MOOD_COUNT=${#MOOD_SPEECHES[@]}
237
+ IDX=$(( (NOW / 30) % MOOD_COUNT ))
238
+ SPEECH="${MOOD_SPEECHES[$IDX]}"
224
239
  fi
225
240
  if [ -n "$ENCOUNTER" ]; then
226
241
  # Bright yellow for encounter alerts