@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,131 @@
1
+ /**
2
+ * Zod validation schemas for state files.
3
+ * Catches disk corruption early at load boundaries.
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ BADGE_TYPES,
10
+ CODING_STATS,
11
+ EVENT_COUNTER_KEYS,
12
+ } from "../engine/types.js";
13
+
14
+ // ---- Shared Primitives ----
15
+
16
+ const BadgeTypeSchema = z.enum(BADGE_TYPES);
17
+
18
+ // ---- Coding Stats ----
19
+
20
+ export const CodingStatsSchema = z.object({
21
+ stamina: z.number(),
22
+ debugging: z.number(),
23
+ stability: z.number(),
24
+ velocity: z.number(),
25
+ wisdom: z.number(),
26
+ });
27
+
28
+ // ---- Owned Pokemon ----
29
+
30
+ export const OwnedPokemonSchema = z.object({
31
+ id: z.string(),
32
+ pokemonId: z.number().int().min(1).max(151),
33
+ nickname: z.string().nullable(),
34
+ level: z.number().int().min(1).max(100),
35
+ currentXp: z.number().int().min(0),
36
+ totalXp: z.number().int().min(0),
37
+ codingStats: CodingStatsSchema,
38
+ happiness: z.number().int().min(0).max(255),
39
+ caughtAt: z.string(),
40
+ evolvedAt: z.string().nullable(),
41
+ isActive: z.boolean(),
42
+ personality: z.string().nullable(),
43
+ shiny: z.boolean().default(false),
44
+ isStarter: z.boolean().default(false),
45
+ });
46
+
47
+ // ---- Event Counters ----
48
+
49
+ // Cast needed: Object.fromEntries loses key specificity, but keys are guaranteed by EVENT_COUNTER_KEYS
50
+ export const EventCountersSchema = z.object(
51
+ Object.fromEntries(
52
+ EVENT_COUNTER_KEYS.map((key) => [key, z.number().int().min(0).default(0)])
53
+ ) as Record<(typeof EVENT_COUNTER_KEYS)[number], z.ZodDefault<z.ZodNumber>>
54
+ );
55
+
56
+ // ---- Streak Data ----
57
+
58
+ export const StreakDataSchema = z.object({
59
+ currentStreak: z.number().int().min(0).default(0),
60
+ longestStreak: z.number().int().min(0).default(0),
61
+ lastActiveDate: z.string().nullable().default(null),
62
+ totalDaysActive: z.number().int().min(0).default(0),
63
+ });
64
+
65
+ // ---- Buddy Config ----
66
+
67
+ export const BuddyConfigSchema = z.object({
68
+ muted: z.boolean().default(false),
69
+ reactionCooldownMs: z.number().int().min(0).default(30_000),
70
+ statusLineEnabled: z.boolean().default(true),
71
+ bellEnabled: z.boolean().default(true),
72
+ });
73
+
74
+ // ---- Pokedex ----
75
+
76
+ export const PokedexEntrySchema = z.object({
77
+ seen: z.boolean(),
78
+ caught: z.boolean(),
79
+ firstSeen: z.string().nullable(),
80
+ firstCaught: z.string().nullable(),
81
+ });
82
+
83
+ export const PokedexStateSchema = z.object({
84
+ entries: z.record(z.coerce.number(), PokedexEntrySchema).default({}),
85
+ totalSeen: z.number().int().min(0).default(0),
86
+ totalCaught: z.number().int().min(0).default(0),
87
+ });
88
+
89
+ // ---- Achievements ----
90
+
91
+ export const UnlockedAchievementSchema = z.object({
92
+ achievementId: z.string(),
93
+ unlockedAt: z.string(),
94
+ });
95
+
96
+ // ---- Catch Condition ----
97
+
98
+ export const CatchConditionSchema = z.object({
99
+ requiredStat: z.enum(CODING_STATS).nullable(),
100
+ minStatValue: z.number(),
101
+ requiredLevel: z.number(),
102
+ });
103
+
104
+ // ---- Wild Encounter ----
105
+
106
+ export const WildEncounterSchema = z.object({
107
+ pokemonId: z.number().int().min(1).max(151),
108
+ level: z.number().int().min(1).max(100),
109
+ catchCondition: CatchConditionSchema,
110
+ });
111
+
112
+ // ---- Player State (top-level) ----
113
+
114
+ export const PlayerStateSchema = z.object({
115
+ trainerId: z.string(),
116
+ trainerName: z.string(),
117
+ party: z.array(OwnedPokemonSchema).max(6),
118
+ pcBox: z.array(OwnedPokemonSchema).default([]),
119
+ pokedex: PokedexStateSchema,
120
+ badges: z.array(BadgeTypeSchema).default([]),
121
+ achievements: z.array(UnlockedAchievementSchema).default([]),
122
+ counters: EventCountersSchema,
123
+ streak: StreakDataSchema,
124
+ config: BuddyConfigSchema,
125
+ startedAt: z.string(),
126
+ totalXpEarned: z.number().int().min(0).default(0),
127
+ totalSessions: z.number().int().min(0).default(0),
128
+ pendingEncounter: WildEncounterSchema.nullable().default(null),
129
+ xpSinceLastEncounter: z.number().int().min(0).default(0),
130
+ });
131
+
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Singleton state manager for Claudemon.
3
+ * All state reads/writes flow through this class.
4
+ */
5
+
6
+ import { getStateDir, getStateFile, getStatusFile } from "../engine/constants.js";
7
+ import type {
8
+ PlayerState,
9
+ OwnedPokemon,
10
+ EventCounterKey,
11
+ EventCounters,
12
+ StreakData,
13
+ BuddyConfig,
14
+ PokedexState,
15
+ WildEncounter,
16
+ } from "../engine/types.js";
17
+ import { EVENT_COUNTER_KEYS } from "../engine/types.js";
18
+ import { atomicWrite, safeRead, ensureDir, backupCorrupted, withLock } from "./io.js";
19
+ import { PlayerStateSchema } from "./schemas.js";
20
+
21
+ /** Compact status payload for the shell status line script */
22
+ interface StatusPayload {
23
+ name: string;
24
+ level: number;
25
+ xpPercent: number;
26
+ speciesId: number;
27
+ evolutionReady: boolean;
28
+ }
29
+
30
+ /** Build a zeroed-out EventCounters record */
31
+ function emptyCounters(): EventCounters {
32
+ const counters = {} as Record<string, number>;
33
+ for (const key of EVENT_COUNTER_KEYS) {
34
+ counters[key] = 0;
35
+ }
36
+ return counters as EventCounters;
37
+ }
38
+
39
+ /** Build default streak data */
40
+ function emptyStreak(): StreakData {
41
+ return {
42
+ currentStreak: 0,
43
+ longestStreak: 0,
44
+ lastActiveDate: null,
45
+ totalDaysActive: 0,
46
+ };
47
+ }
48
+
49
+ /** Build default config */
50
+ function defaultConfig(): BuddyConfig {
51
+ return {
52
+ muted: false,
53
+ reactionCooldownMs: 30_000,
54
+ statusLineEnabled: true,
55
+ bellEnabled: true,
56
+ };
57
+ }
58
+
59
+ /** Build empty pokedex */
60
+ function emptyPokedex(): PokedexState {
61
+ return {
62
+ entries: {},
63
+ totalSeen: 0,
64
+ totalCaught: 0,
65
+ };
66
+ }
67
+
68
+ /** Get today's date as YYYY-MM-DD in local timezone */
69
+ function todayDateString(): string {
70
+ const now = new Date();
71
+ const year = now.getFullYear();
72
+ const month = String(now.getMonth() + 1).padStart(2, "0");
73
+ const day = String(now.getDate()).padStart(2, "0");
74
+ return `${year}-${month}-${day}`;
75
+ }
76
+
77
+ export class StateManager {
78
+ private static instance: StateManager | null = null;
79
+ private state: PlayerState | null = null;
80
+
81
+ private constructor() {}
82
+
83
+ /** Get the singleton StateManager instance */
84
+ static getInstance(): StateManager {
85
+ if (!StateManager.instance) {
86
+ StateManager.instance = new StateManager();
87
+ }
88
+ return StateManager.instance;
89
+ }
90
+
91
+ /** Load state from disk. Returns null on first run (no file) or if data is corrupted. */
92
+ async load(): Promise<PlayerState | null> {
93
+ await ensureDir(getStateDir());
94
+
95
+ const stateFile = getStateFile();
96
+ const fileExists = await Bun.file(stateFile).exists();
97
+ if (!fileExists) {
98
+ return null;
99
+ }
100
+
101
+ const raw = await safeRead<unknown>(stateFile);
102
+ if (raw === null) {
103
+ // File exists but safeRead returned null — empty or invalid JSON
104
+ const backupPath = await backupCorrupted(stateFile);
105
+ process.stderr.write(
106
+ `[claudemon] State file was corrupted and has been backed up to ${backupPath}\n` +
107
+ `[claudemon] You'll need to pick a new starter with buddy_starter.\n`,
108
+ );
109
+ return null;
110
+ }
111
+
112
+ try {
113
+ // Zod output is structurally compatible but not nominally identical to PlayerState
114
+ const parsed = PlayerStateSchema.parse(raw) as PlayerState;
115
+ this.state = parsed;
116
+ return this.state;
117
+ } catch (err: unknown) {
118
+ // Schema validation failed — data on disk doesn't match expected shape
119
+ const backupPath = await backupCorrupted(stateFile);
120
+ const message = err instanceof Error ? err.message : String(err);
121
+ process.stderr.write(
122
+ `[claudemon] State validation failed: ${message}\n` +
123
+ `[claudemon] Corrupted data backed up to ${backupPath}\n` +
124
+ `[claudemon] You'll need to pick a new starter with buddy_starter.\n`,
125
+ );
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /** Save current state to disk atomically, protected by file lock */
131
+ async save(): Promise<void> {
132
+ if (!this.state) {
133
+ throw new Error("Cannot save: state not loaded. Call load() or initializePlayer() first.");
134
+ }
135
+ const stateDir = getStateDir();
136
+ await ensureDir(stateDir);
137
+ const lockPath = `${stateDir}/state.lock`;
138
+ await withLock(lockPath, async () => {
139
+ const json = JSON.stringify(this.state, null, 2);
140
+ await atomicWrite(getStateFile(), json);
141
+ });
142
+ }
143
+
144
+ /** Get current state (throws if not loaded) */
145
+ getState(): PlayerState {
146
+ if (!this.state) {
147
+ throw new Error("State not loaded. Call load() first.");
148
+ }
149
+ return this.state;
150
+ }
151
+
152
+ /** Check if this is first run (no state file exists) */
153
+ async isFirstRun(): Promise<boolean> {
154
+ const file = Bun.file(getStateFile());
155
+ return !(await file.exists());
156
+ }
157
+
158
+ /** Initialize new player state after starter selection */
159
+ async initializePlayer(
160
+ trainerId: string,
161
+ trainerName: string,
162
+ starter: OwnedPokemon,
163
+ ): Promise<PlayerState> {
164
+ const now = new Date().toISOString();
165
+
166
+ const state: PlayerState = {
167
+ trainerId,
168
+ trainerName,
169
+ party: [starter],
170
+ pcBox: [],
171
+ pokedex: emptyPokedex(),
172
+ badges: [],
173
+ achievements: [],
174
+ counters: emptyCounters(),
175
+ streak: emptyStreak(),
176
+ config: defaultConfig(),
177
+ startedAt: now,
178
+ totalXpEarned: 0,
179
+ totalSessions: 0,
180
+ pendingEncounter: null,
181
+ xpSinceLastEncounter: 0,
182
+ };
183
+
184
+ this.state = state;
185
+ await this.save();
186
+ return this.state;
187
+ }
188
+
189
+ /** Get the active Pokemon from the party (isActive === true) */
190
+ getActivePokemon(): OwnedPokemon | null {
191
+ if (!this.state) {
192
+ return null;
193
+ }
194
+ return this.state.party.find((p) => p.isActive) ?? null;
195
+ }
196
+
197
+ /** Increment an event counter by amount (default 1) and save (lock-protected) */
198
+ async incrementCounter(key: EventCounterKey, amount: number = 1): Promise<void> {
199
+ const state = this.getState();
200
+ state.counters[key] += amount;
201
+ await this.save();
202
+ }
203
+
204
+ /** Update daily streak based on today vs lastActiveDate, then save */
205
+ async updateStreak(): Promise<void> {
206
+ const state = this.getState();
207
+ const today = todayDateString();
208
+ const { streak } = state;
209
+
210
+ if (streak.lastActiveDate === today) {
211
+ // Already recorded today, nothing to do
212
+ return;
213
+ }
214
+
215
+ if (streak.lastActiveDate === null) {
216
+ // First ever activity
217
+ streak.currentStreak = 1;
218
+ streak.totalDaysActive = 1;
219
+ } else {
220
+ const last = new Date(streak.lastActiveDate);
221
+ const now = new Date(today);
222
+ const diffMs = now.getTime() - last.getTime();
223
+ const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
224
+
225
+ if (diffDays === 1) {
226
+ // Consecutive day
227
+ streak.currentStreak += 1;
228
+ } else {
229
+ // Streak broken
230
+ streak.currentStreak = 1;
231
+ }
232
+ streak.totalDaysActive += 1;
233
+ }
234
+
235
+ if (streak.currentStreak > streak.longestStreak) {
236
+ streak.longestStreak = streak.currentStreak;
237
+ }
238
+
239
+ streak.lastActiveDate = today;
240
+ await this.save();
241
+ }
242
+
243
+ /** Write compact status JSON for the shell status line script. */
244
+ async writeStatus(evolutionReady: boolean = false): Promise<void> {
245
+ const active = this.getActivePokemon();
246
+ if (!active) {
247
+ return;
248
+ }
249
+
250
+ // XP percent is currentXp as a rough percentage toward next level
251
+ // Exact formula depends on exp group; use a simple ratio for the status line
252
+ const xpPercent = active.level >= 100 ? 100 : Math.min(100, Math.floor((active.currentXp / Math.max(1, active.currentXp + 50)) * 100));
253
+
254
+ // Look up species name if no nickname
255
+ let displayName = active.nickname ?? "";
256
+ if (!displayName) {
257
+ const { POKEMON_BY_ID } = await import("../engine/pokemon-data.js");
258
+ const species = POKEMON_BY_ID.get(active.pokemonId);
259
+ displayName = species?.name ?? `Pokemon #${active.pokemonId}`;
260
+ }
261
+
262
+ const payload: StatusPayload = {
263
+ name: displayName,
264
+ level: active.level,
265
+ xpPercent,
266
+ speciesId: active.pokemonId,
267
+ evolutionReady,
268
+ };
269
+
270
+ const stateDir = getStateDir();
271
+ await ensureDir(stateDir);
272
+ await atomicWrite(getStatusFile(), JSON.stringify(payload));
273
+ }
274
+
275
+ /** Set a pending wild encounter on state. */
276
+ setPendingEncounter(encounter: WildEncounter): void {
277
+ const state = this.getState();
278
+ state.pendingEncounter = encounter;
279
+ }
280
+
281
+ /** Clear the pending wild encounter from state. */
282
+ clearPendingEncounter(): void {
283
+ const state = this.getState();
284
+ state.pendingEncounter = null;
285
+ }
286
+
287
+ /** Write encounter notification to status JSON so status line can show it. */
288
+ async writeEncounterStatus(pokemonName: string): Promise<void> {
289
+ const active = this.getActivePokemon();
290
+ if (!active) return;
291
+
292
+ const xpPercent =
293
+ active.level >= 100
294
+ ? 100
295
+ : Math.min(100, Math.floor((active.currentXp / Math.max(1, active.currentXp + 50)) * 100));
296
+
297
+ let displayName = active.nickname ?? "";
298
+ if (!displayName) {
299
+ const { POKEMON_BY_ID } = await import("../engine/pokemon-data.js");
300
+ const species = POKEMON_BY_ID.get(active.pokemonId);
301
+ displayName = species?.name ?? `Pokemon #${active.pokemonId}`;
302
+ }
303
+
304
+ const payload = {
305
+ name: displayName,
306
+ level: active.level,
307
+ xpPercent,
308
+ speciesId: active.pokemonId,
309
+ evolutionReady: false,
310
+ encounter: `Wild ${pokemonName} appeared!`,
311
+ };
312
+
313
+ await ensureDir(getStateDir());
314
+ await atomicWrite(getStatusFile(), JSON.stringify(payload));
315
+ }
316
+
317
+ /** Reset singleton for testing purposes */
318
+ static resetInstance(): void {
319
+ StateManager.instance = null;
320
+ }
321
+ }
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code status line for Claudemon.
3
+ # Two-column layout:
4
+ # LEFT: model, context remaining, usage, buddy speech
5
+ # RIGHT: colorscript sprite + name + level (hideable via /buddy hide)
6
+
7
+ STATE_DIR="$HOME/.claudemon"
8
+ STATUS_FILE="$STATE_DIR/status.json"
9
+ CONFIG_FILE="$STATE_DIR/config.json"
10
+
11
+ [ -f "$STATUS_FILE" ] || exit 0
12
+ command -v jq >/dev/null 2>&1 || exit 0
13
+
14
+ # ── Check hide/show toggle ──────────────────────────────────
15
+ SPRITE_HIDDEN="false"
16
+ if [ -f "$CONFIG_FILE" ]; then
17
+ SPRITE_HIDDEN=$(jq -r '.spriteHidden // false' "$CONFIG_FILE" 2>/dev/null)
18
+ fi
19
+
20
+ # ── Read Claude Code stdin JSON ─────────────────────────────
21
+ STDIN_DATA=""
22
+ if [ ! -t 0 ]; then
23
+ STDIN_DATA=$(timeout 1 cat 2>/dev/null || true)
24
+ fi
25
+
26
+ CC_MODEL="" CC_CONTEXT=""
27
+ if [ -n "$STDIN_DATA" ]; then
28
+ CC_MODEL=$(echo "$STDIN_DATA" | jq -r '.model.display_name // empty' 2>/dev/null)
29
+
30
+ # Context memory: count conversation messages from transcript
31
+ TRANSCRIPT=$(echo "$STDIN_DATA" | jq -r '.transcript_path // empty' 2>/dev/null)
32
+ if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
33
+ # Count message objects in transcript as a rough context indicator
34
+ MSG_COUNT=$(jq '[.[] | select(.type == "message" or .type == "human" or .type == "assistant")] | length' "$TRANSCRIPT" 2>/dev/null || echo 0)
35
+ # Rough estimate: ~50 messages fills a 200K context window
36
+ # (each message averages ~4K tokens including tool calls)
37
+ MAX_MSGS=50
38
+ if [ "$MSG_COUNT" -gt 0 ]; then
39
+ USED_PCT=$(( (MSG_COUNT * 100) / MAX_MSGS ))
40
+ [ "$USED_PCT" -gt 100 ] && USED_PCT=100
41
+ LEFT_PCT=$(( 100 - USED_PCT ))
42
+ CC_CONTEXT="${LEFT_PCT}% context"
43
+ fi
44
+ fi
45
+ fi
46
+
47
+ # ── Read buddy status ───────────────────────────────────────
48
+ STATUS=$(cat "$STATUS_FILE" 2>/dev/null) || exit 0
49
+ [ -n "$STATUS" ] || exit 0
50
+
51
+ NAME=$(echo "$STATUS" | jq -r '.name // empty')
52
+ [ -n "$NAME" ] || exit 0
53
+
54
+ LEVEL=$(echo "$STATUS" | jq -r '.level // 0')
55
+ SPECIES_ID=$(echo "$STATUS" | jq -r '.speciesId // 0')
56
+ EVOLVING=$(echo "$STATUS" | jq -r '.evolutionReady // false')
57
+ REACTION=$(echo "$STATUS" | jq -r '.reaction // empty')
58
+ ENCOUNTER=$(echo "$STATUS" | jq -r '.encounter // empty')
59
+
60
+ # ── Colors ──────────────────────────────────────────────────
61
+ NC=$'\033[0m'
62
+ DIM=$'\033[2m'
63
+ CYAN=$'\033[38;2;100;200;220m'
64
+ GRAY=$'\033[38;2;120;120;130m'
65
+ GREEN=$'\033[38;2;100;200;100m'
66
+ B=$'\xe2\xa0\x80'
67
+
68
+ # ── Terminal width ──────────────────────────────────────────
69
+ COLS=0
70
+ PID=$$
71
+ for _ in 1 2 3 4 5; do
72
+ PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
73
+ [ -z "$PID" ] || [ "$PID" = "1" ] && break
74
+ PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
75
+ if [ -c "$PTY" ] 2>/dev/null; then
76
+ COLS=$(stty size < "$PTY" 2>/dev/null | awk '{print $2}')
77
+ [ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
78
+ fi
79
+ done
80
+ [ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=${COLUMNS:-0}
81
+ [ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=125
82
+ [ "$COLS" -lt 40 ] && exit 0
83
+
84
+ # ── Build name line ─────────────────────────────────────────
85
+ if [ "$EVOLVING" = "true" ]; then
86
+ INFO_LINE="${NAME} Lv.${LEVEL} *EVOLVING*"
87
+ else
88
+ INFO_LINE="${NAME} Lv.${LEVEL}"
89
+ fi
90
+
91
+ # ── If sprite hidden, show minimal text-only ────────────────
92
+ if [ "$SPRITE_HIDDEN" = "true" ]; then
93
+ # Left info only, no sprite
94
+ LEFT=""
95
+ [ -n "$CC_MODEL" ] && LEFT="${CYAN}${CC_MODEL}${NC}"
96
+ [ -n "$CC_CONTEXT" ] && LEFT="${LEFT:+${LEFT} ${GRAY}·${NC} }${GREEN}${CC_CONTEXT}${NC}"
97
+
98
+ SPACER=""
99
+ RIGHT_PAD=$(( COLS - ${#INFO_LINE} - 4 ))
100
+ [ "$RIGHT_PAD" -lt 0 ] && RIGHT_PAD=0
101
+ for (( i=0; i<RIGHT_PAD; i++ )); do SPACER+="$B"; done
102
+
103
+ echo "${LEFT}"
104
+ echo "${SPACER}${INFO_LINE}"
105
+ exit 0
106
+ fi
107
+
108
+ # ── Load sprite ─────────────────────────────────────────────
109
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
110
+ SPRITE_DIR="$SCRIPT_DIR/../sprites/colorscripts/small"
111
+ NAME_LOWER=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | sed "s/♀/-f/;s/♂/-m/;s/'//;s/\. /-/;s/ /-/")
112
+ SPRITE_FILE="$SPRITE_DIR/${SPECIES_ID}-${NAME_LOWER}.txt"
113
+
114
+ [ -f "$SPRITE_FILE" ] || exit 0
115
+
116
+ mapfile -t SPRITE_LINES < "$SPRITE_FILE"
117
+
118
+ # Strip empty lines
119
+ while [ ${#SPRITE_LINES[@]} -gt 0 ] && [ -z "${SPRITE_LINES[0]}" ]; do
120
+ SPRITE_LINES=("${SPRITE_LINES[@]:1}")
121
+ done
122
+ while [ ${#SPRITE_LINES[@]} -gt 0 ] && [ -z "${SPRITE_LINES[-1]}" ]; do
123
+ unset 'SPRITE_LINES[-1]'
124
+ done
125
+
126
+ # Trim to max 8 sprite lines (top = head)
127
+ MAX_SPRITE=8
128
+ if [ ${#SPRITE_LINES[@]} -gt "$MAX_SPRITE" ]; then
129
+ SPRITE_LINES=("${SPRITE_LINES[@]:0:$MAX_SPRITE}")
130
+ fi
131
+
132
+ SPRITE_COUNT=${#SPRITE_LINES[@]}
133
+ [ "$SPRITE_COUNT" -eq 0 ] && exit 0
134
+
135
+ # ── Sprite width ────────────────────────────────────────────
136
+ ART_W=0
137
+ for line in "${SPRITE_LINES[@]}"; do
138
+ stripped=$(echo -e "$line" | sed 's/\x1b\[[0-9;]*m//g')
139
+ w=${#stripped}
140
+ [ "$w" -gt "$ART_W" ] && ART_W=$w
141
+ done
142
+ [ "$ART_W" -lt 5 ] && ART_W=12
143
+
144
+ # ── Build left-side lines ───────────────────────────────────
145
+ # Line 1: model · context left
146
+ LEFT_1=""
147
+ [ -n "$CC_MODEL" ] && LEFT_1="${CYAN}${CC_MODEL}${NC}"
148
+ [ -n "$CC_CONTEXT" ] && LEFT_1="${LEFT_1:+${LEFT_1} ${GRAY}·${NC} }${GREEN}${CC_CONTEXT}${NC}"
149
+
150
+ # Line 2: (empty)
151
+ LEFT_2=""
152
+
153
+ # Line 3: buddy speech (rotates every 10s, or shows reaction)
154
+ SPEECH=""
155
+ if [ -n "$ENCOUNTER" ]; then
156
+ # Wild encounter takes priority — flash to get attention
157
+ SPEECH="! ${ENCOUNTER} Use /buddy catch !"
158
+ elif [ -n "$REACTION" ]; then
159
+ SPEECH="$REACTION"
160
+ else
161
+ NOW=$(date +%s)
162
+ IDX=$(( (NOW / 30) % 12 ))
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
+ )
177
+ SPEECH="${SPEECHES[$IDX]}"
178
+ fi
179
+ if [ -n "$ENCOUNTER" ]; then
180
+ # Bright yellow for encounter alerts
181
+ SPEECH_COLOR=$'\033[1;38;2;255;220;50m'
182
+ else
183
+ # Warm muted for idle speech
184
+ SPEECH_COLOR=$'\033[38;2;180;180;120m'
185
+ fi
186
+ LEFT_3="${SPEECH_COLOR}${SPEECH}${NC}"
187
+
188
+ # ── Merge left + right ──────────────────────────────────────
189
+ TOTAL_LINES=$SPRITE_COUNT
190
+
191
+ RIGHT_MARGIN=4
192
+ RIGHT_PAD=$(( COLS - ART_W - RIGHT_MARGIN ))
193
+ [ "$RIGHT_PAD" -lt 0 ] && RIGHT_PAD=0
194
+
195
+ # Build left array — line 1: model+context, line 2: buddy speech, rest empty
196
+ LEFT_LINES=()
197
+ LEFT_LINES+=("$LEFT_1") # line 1: model · context
198
+ LEFT_LINES+=("$LEFT_3") # line 2: buddy speech
199
+
200
+ # Pad rest with empty lines
201
+ while [ ${#LEFT_LINES[@]} -lt "$TOTAL_LINES" ]; do
202
+ LEFT_LINES+=("")
203
+ done
204
+ LEFT_COUNT=${#LEFT_LINES[@]}
205
+
206
+ # ── Build full right-side spacer ─────────────────────────────
207
+ FULL_SPACER=""
208
+ for (( s=0; s<RIGHT_PAD; s++ )); do FULL_SPACER+="$B"; done
209
+
210
+ # ── Output name line ABOVE sprite ────────────────────────────
211
+ echo "${FULL_SPACER}${INFO_LINE}"
212
+
213
+ # ── Output sprite lines (right-aligned, left content merged) ──
214
+ for (( i=0; i<SPRITE_COUNT; i++ )); do
215
+ left="${LEFT_LINES[$i]}"
216
+ left_visible=$(echo -e "$left" | sed 's/\x1b\[[0-9;]*m//g')
217
+ left_w=${#left_visible}
218
+
219
+ right="${SPRITE_LINES[$i]}${NC}"
220
+
221
+ if [ -n "$left" ]; then
222
+ gap=$(( RIGHT_PAD - left_w ))
223
+ [ "$gap" -lt 1 ] && gap=1
224
+ GAP_STR=""
225
+ for (( g=0; g<gap; g++ )); do GAP_STR+="$B"; done
226
+ echo "${left}${GAP_STR}${right}"
227
+ else
228
+ echo "${FULL_SPACER}${right}"
229
+ fi
230
+ done
231
+
232
+
233
+ exit 0