@ramarivera/coding-buddy 0.4.0-alpha.8 → 0.6.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 (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
  3. package/LICENSE +1 -1
  4. package/README.md +102 -72
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +59 -44
  22. package/cli/biomes.ts +309 -0
  23. package/cli/buddy-shell.ts +818 -0
  24. package/{adapters/claude/install → cli}/disable.ts +14 -32
  25. package/{adapters/claude/install → cli}/doctor.ts +55 -57
  26. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  27. package/cli/index.ts +36 -25
  28. package/{adapters/claude/install → cli}/install.ts +100 -145
  29. package/{adapters/claude/install → cli}/pick.ts +111 -50
  30. package/{adapters/claude/install → cli}/settings.ts +2 -2
  31. package/{adapters/claude/install → cli}/show.ts +4 -4
  32. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  33. package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
  34. package/cli/tui.tsx +2333 -0
  35. package/{adapters/claude/install → cli}/uninstall.ts +23 -38
  36. package/cli/upgrade.ts +213 -0
  37. package/cli/validate-species.ts +209 -0
  38. package/cli/verify.ts +3 -3
  39. package/core/achievements.ts +12 -20
  40. package/core/art-data.ts +26 -0
  41. package/core/command-service.ts +99 -97
  42. package/core/engine.ts +83 -1
  43. package/core/ports.ts +10 -0
  44. package/core/reactions.ts +533 -95
  45. package/core/slot-slug.ts +22 -0
  46. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
  47. package/hooks/file-type-react.sh +401 -0
  48. package/hooks/hooks.json +52 -0
  49. package/hooks/mood-react.sh +208 -0
  50. package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
  51. package/hooks/react.sh +1368 -0
  52. package/hooks/suggest.sh +55 -0
  53. package/package.json +113 -59
  54. package/scripts/gen-emoji-widths.ts +34 -0
  55. package/scripts/paths.sh +56 -0
  56. package/scripts/postinstall.cjs +42 -0
  57. package/scripts/snapshot-statusline.sh +54 -0
  58. package/server/achievements.ts +1419 -0
  59. package/server/art.ts +450 -0
  60. package/server/award-xp.ts +48 -0
  61. package/server/check-suggestions.ts +102 -0
  62. package/server/consolidate.ts +33 -0
  63. package/server/index.ts +1340 -0
  64. package/server/mcp-launcher.sh +26 -0
  65. package/server/memory.ts +404 -0
  66. package/server/mood.ts +254 -0
  67. package/server/path.ts +82 -0
  68. package/server/shift-mood.ts +43 -0
  69. package/{adapters/claude/storage → server}/state.ts +200 -7
  70. package/server/suggestions.ts +335 -0
  71. package/server/theme.ts +53 -0
  72. package/server/xp.ts +435 -0
  73. package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
  74. package/statusline/buddy-status.sh +397 -0
  75. package/statusline/combined-status.sh +134 -0
  76. package/statusline/emoji-widths.data +4 -0
  77. package/adapters/claude/hooks/hooks.json +0 -35
  78. package/adapters/claude/hooks/react.sh +0 -204
  79. package/adapters/claude/popup/buddy-popup.sh +0 -92
  80. package/adapters/claude/popup/buddy-render.sh +0 -540
  81. package/adapters/claude/popup/popup-manager.sh +0 -355
  82. package/adapters/claude/rendering/art.ts +0 -264
  83. package/adapters/claude/server/index.ts +0 -752
  84. package/adapters/claude/server/instructions.ts +0 -24
  85. package/adapters/claude/server/resources.ts +0 -38
  86. package/adapters/claude/statusline/buddy-status.sh +0 -389
  87. package/adapters/claude/storage/achievements.ts +0 -253
  88. package/adapters/claude/storage/identity.ts +0 -13
  89. package/adapters/claude/storage/paths.ts +0 -24
  90. package/adapters/claude/storage/settings.ts +0 -41
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * claude-buddy backup — snapshot all claude-buddy related state
3
+ * coding-buddy backup — snapshot all coding-buddy related state
4
4
  *
5
5
  * Usage:
6
6
  * bun run backup Create a new snapshot
@@ -10,15 +10,20 @@
10
10
  * bun run backup restore <ts> Restore a specific backup
11
11
  * bun run backup delete <ts> Delete a specific backup
12
12
  *
13
- * Backups are stored in ~/.claude-buddy/backups/YYYY-MM-DD-HHMMSS/
13
+ * Backups are stored in <state-dir>/backups/YYYY-MM-DD-HHMMSS/, where
14
+ * <state-dir> resolves via server/paths.ts ($CLAUDE_CONFIG_DIR/buddy-state
15
+ * when that env var is set, else ~/.claude-buddy).
14
16
  *
15
17
  * What gets backed up:
16
- * - ~/.claude/settings.json (full file)
17
- * - ~/.claude.json mcpServers["claude-buddy"] block (only our entry)
18
- * - ~/.claude/skills/buddy/SKILL.md
19
- * - ~/.claude-buddy/companion.json
20
- * - ~/.claude-buddy/status.json
21
- * - ~/.claude-buddy/reaction.json
18
+ * - settings.json (full file)
19
+ * - .claude.json mcpServers["claude-buddy"] block (only our entry)
20
+ * - skills/buddy/SKILL.md
21
+ * - <state-dir>/menagerie.json
22
+ * - <state-dir>/config.json
23
+ * - <state-dir>/status.json
24
+ * - <state-dir>/events.json
25
+ * - <state-dir>/unlocked.json
26
+ * - <state-dir>/active_days.json
22
27
  */
23
28
 
24
29
  import {
@@ -26,15 +31,19 @@ import {
26
31
  readdirSync, statSync, rmSync, copyFileSync,
27
32
  } from "fs";
28
33
  import { dirname, join } from "path";
29
- import { homedir } from "os";
30
- import { getBuddySkillDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
31
-
32
- const HOME = homedir();
33
- const BACKUPS_DIR = join(HOME, ".claude-buddy", "backups");
34
- const SETTINGS = getClaudeSettingsPath();
35
- const CLAUDE_JSON = getClaudeJsonPath();
36
- const SKILL = join(getBuddySkillDir(), "SKILL.md");
37
- const STATE_DIR = join(HOME, ".claude-buddy");
34
+ import {
35
+ buddyStateDir,
36
+ claudeSettingsPath,
37
+ claudeSkillDir,
38
+ claudeUserConfigPath,
39
+ } from "../server/path.ts";
40
+
41
+ const SETTINGS = claudeSettingsPath();
42
+ const CLAUDE_JSON = claudeUserConfigPath();
43
+ const SKILL_DIR = claudeSkillDir("buddy");
44
+ const SKILL = join(SKILL_DIR, "SKILL.md");
45
+ const STATE_DIR = buddyStateDir();
46
+ const BACKUPS_DIR = join(STATE_DIR, "backups");
38
47
 
39
48
  const RED = "\x1b[31m";
40
49
  const GREEN = "\x1b[32m";
@@ -74,16 +83,16 @@ function createBackup(): string {
74
83
  const dir = join(BACKUPS_DIR, ts);
75
84
  mkdirSync(dir, { recursive: true });
76
85
 
77
- const manifest: { timestamp: string; files: string[] } = { timestamp: ts, files: [] };
86
+ const manifest: Record<string, any> = { timestamp: ts, files: [] };
78
87
 
79
88
  // 1. settings.json
80
89
  const settings = tryRead(SETTINGS);
81
90
  if (settings) {
82
91
  writeFileSync(join(dir, "settings.json"), settings);
83
92
  manifest.files.push("settings.json");
84
- ok(`Backed up: ~/.claude/settings.json`);
93
+ ok(`Backed up: ${SETTINGS}`);
85
94
  } else {
86
- warn(`Skipped: ~/.claude/settings.json (not found)`);
95
+ warn(`Skipped: ${SETTINGS} (not found)`);
87
96
  }
88
97
 
89
98
  // 2. claude.json mcpServers["claude-buddy"]
@@ -95,12 +104,12 @@ function createBackup(): string {
95
104
  if (ourMcp) {
96
105
  writeFileSync(join(dir, "mcpserver.json"), JSON.stringify(ourMcp, null, 2));
97
106
  manifest.files.push("mcpserver.json");
98
- ok(`Backed up: ~/.claude.json → mcpServers["claude-buddy"]`);
107
+ ok(`Backed up: ${CLAUDE_JSON} → mcpServers["claude-buddy"]`);
99
108
  } else {
100
- warn(`Skipped: ~/.claude.json mcpServers["claude-buddy"] (not registered)`);
109
+ warn(`Skipped: ${CLAUDE_JSON} mcpServers["claude-buddy"] (not registered)`);
101
110
  }
102
111
  } catch {
103
- err(`Failed to parse ~/.claude.json`);
112
+ err(`Failed to parse ${CLAUDE_JSON}`);
104
113
  }
105
114
  }
106
115
 
@@ -109,21 +118,21 @@ function createBackup(): string {
109
118
  if (skill) {
110
119
  writeFileSync(join(dir, "SKILL.md"), skill);
111
120
  manifest.files.push("SKILL.md");
112
- ok(`Backed up: ~/.claude/skills/buddy/SKILL.md`);
121
+ ok(`Backed up: ${SKILL}`);
113
122
  } else {
114
- warn(`Skipped: ~/.claude/skills/buddy/SKILL.md (not found)`);
123
+ warn(`Skipped: ${SKILL} (not found)`);
115
124
  }
116
125
 
117
- // 4-6. ~/.claude-buddy/ state files (don't back up the backups dir itself)
126
+ // 4+. ~/.claude-buddy/ state files (don't back up the backups dir itself)
118
127
  const stateDestDir = join(dir, "claude-buddy");
119
128
  mkdirSync(stateDestDir, { recursive: true });
120
- const stateFiles = ["companion.json", "status.json", "reaction.json"];
129
+ const stateFiles = ["menagerie.json", "config.json", "status.json", "events.json", "unlocked.json", "active_days.json"];
121
130
  for (const f of stateFiles) {
122
131
  const src = join(STATE_DIR, f);
123
132
  if (existsSync(src)) {
124
133
  copyFileSync(src, join(stateDestDir, f));
125
134
  manifest.files.push(`claude-buddy/${f}`);
126
- ok(`Backed up: ~/.claude-buddy/${f}`);
135
+ ok(`Backed up: ${join(STATE_DIR, f)}`);
127
136
  }
128
137
  }
129
138
 
@@ -141,7 +150,7 @@ function cmdList() {
141
150
  info(`Run '${BOLD}bun run backup${NC}' to create one.`);
142
151
  return;
143
152
  }
144
- console.log(`\n${BOLD}claude-buddy backups${NC}\n`);
153
+ console.log(`\n${BOLD}coding-buddy backups${NC}\n`);
145
154
  for (const ts of backups) {
146
155
  const manifestPath = join(BACKUPS_DIR, ts, "manifest.json");
147
156
  const manifest = tryRead(manifestPath);
@@ -196,29 +205,32 @@ function restoreBackup(ts: string) {
196
205
  if (existsSync(settingsBak)) {
197
206
  mkdirSync(dirname(SETTINGS), { recursive: true });
198
207
  copyFileSync(settingsBak, SETTINGS);
199
- ok("Restored: ~/.claude/settings.json");
208
+ ok(`Restored: ${SETTINGS}`);
200
209
  }
201
210
 
202
211
  // 2. claude.json mcpServers — merge our entry back in
203
212
  const mcpBak = join(dir, "mcpserver.json");
204
213
  if (existsSync(mcpBak)) {
205
214
  const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
206
- let claudeJson: { mcpServers?: Record<string, unknown> } = {};
215
+ let claudeJson: Record<string, any> = {};
207
216
  try {
208
- claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8")) as { mcpServers?: Record<string, unknown> };
217
+ claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
209
218
  } catch { /* empty */ }
210
219
  if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
211
220
  claudeJson.mcpServers["claude-buddy"] = ourMcp;
221
+ // Under CLAUDE_CONFIG_DIR the parent dir is not guaranteed to exist
222
+ // if settings.json wasn't in this backup (and so step 1 was skipped).
223
+ mkdirSync(dirname(CLAUDE_JSON), { recursive: true });
212
224
  writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
213
- ok("Restored: ~/.claude.json → mcpServers[\"claude-buddy\"]");
225
+ ok(`Restored: ${CLAUDE_JSON} → mcpServers["claude-buddy"]`);
214
226
  }
215
227
 
216
228
  // 3. SKILL.md
217
229
  const skillBak = join(dir, "SKILL.md");
218
230
  if (existsSync(skillBak)) {
219
- mkdirSync(dirname(SKILL), { recursive: true });
231
+ mkdirSync(SKILL_DIR, { recursive: true });
220
232
  copyFileSync(skillBak, SKILL);
221
- ok("Restored: ~/.claude/skills/buddy/SKILL.md");
233
+ ok(`Restored: ${SKILL}`);
222
234
  }
223
235
 
224
236
  // 4. ~/.claude-buddy/ state files
@@ -227,7 +239,7 @@ function restoreBackup(ts: string) {
227
239
  mkdirSync(STATE_DIR, { recursive: true });
228
240
  for (const f of readdirSync(stateDir)) {
229
241
  copyFileSync(join(stateDir, f), join(STATE_DIR, f));
230
- ok(`Restored: ~/.claude-buddy/${f}`);
242
+ ok(`Restored: ${join(STATE_DIR, f)}`);
231
243
  }
232
244
  }
233
245
 
@@ -254,7 +266,7 @@ const arg = process.argv[3];
254
266
  switch (action) {
255
267
  case "create":
256
268
  case undefined: {
257
- console.log(`\n${BOLD}Creating claude-buddy backup...${NC}\n`);
269
+ console.log(`\n${BOLD}Creating coding-buddy backup...${NC}\n`);
258
270
  const ts = createBackup();
259
271
  console.log(`\n${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
260
272
  console.log(`${GREEN} Backup created: ${ts}${NC}`);
@@ -307,7 +319,7 @@ switch (action) {
307
319
  case "--help":
308
320
  case "-h":
309
321
  console.log(`
310
- ${BOLD}claude-buddy backup${NC} — snapshot and restore all claude-buddy state
322
+ ${BOLD}coding-buddy backup${NC} — snapshot and restore all coding-buddy state
311
323
 
312
324
  ${BOLD}Commands:${NC}
313
325
  bun run backup Create a new snapshot
@@ -318,12 +330,15 @@ ${BOLD}Commands:${NC}
318
330
  bun run backup delete <ts> Delete a specific backup
319
331
 
320
332
  ${BOLD}What gets backed up:${NC}
321
- - ~/.claude/settings.json (full)
322
- - ~/.claude.json mcpServers["claude-buddy"] (only our entry)
323
- - ~/.claude/skills/buddy/SKILL.md
324
- - ~/.claude-buddy/companion.json
325
- - ~/.claude-buddy/status.json
326
- - ~/.claude-buddy/reaction.json
333
+ - ${SETTINGS}
334
+ - ${CLAUDE_JSON} mcpServers["claude-buddy"] (only our entry)
335
+ - ${SKILL}
336
+ - ${STATE_DIR}/menagerie.json
337
+ - ${STATE_DIR}/config.json
338
+ - ${STATE_DIR}/status.json
339
+ - ${STATE_DIR}/events.json
340
+ - ${STATE_DIR}/unlocked.json
341
+ - ${STATE_DIR}/active_days.json
327
342
 
328
343
  ${BOLD}Backup location:${NC}
329
344
  ${BACKUPS_DIR}/<timestamp>/
package/cli/biomes.ts ADDED
@@ -0,0 +1,309 @@
1
+ /**
2
+ * biomes.ts — landscape themes for the buddy panel
3
+ *
4
+ * 5 default biomes (mapped to rarity) + 10 extra biomes selectable via --biome flag.
5
+ *
6
+ * Usage:
7
+ * npx tsx cli/buddy-shell.ts claude # auto-detect by rarity
8
+ * npx tsx cli/buddy-shell.ts claude --biome volcano # force a specific biome
9
+ * npx tsx cli/buddy-shell.ts --biomes # list all available biomes
10
+ */
11
+
12
+ const ESC = "\x1b";
13
+ const CSI = `${ESC}[`;
14
+ const NC = `${CSI}0m`;
15
+
16
+ function c(r: number, g: number, b: number): string {
17
+ return `${CSI}38;2;${r};${g};${b}m`;
18
+ }
19
+
20
+ // ─── Biome definition ───────────────────────────────────────────────────────
21
+
22
+ export interface Biome {
23
+ name: string;
24
+ sky: string;
25
+ ground: string;
26
+ particle: { color: string; chars: string[] };
27
+ groundChars: string[];
28
+ structure: string[];
29
+ }
30
+
31
+ // ═══════════════════════════════════════════════════════════════════════════
32
+ // DEFAULT BIOMES (by rarity)
33
+ // ═══════════════════════════════════════════════════════════════════════════
34
+
35
+ const meadow: Biome = {
36
+ name: "meadow",
37
+ sky: c(40, 40, 60),
38
+ ground: c(60, 90, 40),
39
+ particle: { color: c(150, 150, 130), chars: ["✦", "·", "·"] },
40
+ groundChars: ["▄", "▄", "▄", "▓", "▄", "▒"],
41
+ structure: [
42
+ `${c(120, 80, 60)} ░░${NC}`,
43
+ `${c(160, 60, 50)} ▄████████▄${NC}`,
44
+ `${c(160, 60, 50)} ▟${c(140, 90, 50)}██${c(200, 200, 100)}▪▪${c(140, 90, 50)}██${c(200, 200, 100)}▪▪${c(140, 90, 50)}██${c(160, 60, 50)}▙${NC}`,
45
+ `${c(140, 90, 50)} ██${c(200, 200, 100)}▪▪${c(140, 90, 50)}██${c(200, 200, 100)}▪▪${c(140, 90, 50)}████${NC}`,
46
+ `${c(140, 90, 50)} ██${c(200, 200, 100)}▪▪${c(140, 90, 50)}█${c(100, 60, 30)}▐██▌${c(140, 90, 50)}█${c(200, 200, 100)}▪▪${c(140, 90, 50)}██${NC}`,
47
+ `${c(140, 90, 50)} ██${c(200, 200, 100)}▪▪${c(140, 90, 50)}█${c(100, 60, 30)}▐██▌${c(140, 90, 50)}█${c(200, 200, 100)}▪▪${c(140, 90, 50)}██${NC}`,
48
+ ],
49
+ };
50
+
51
+ const forest: Biome = {
52
+ name: "forest",
53
+ sky: c(15, 35, 25),
54
+ ground: c(30, 80, 35),
55
+ particle: { color: c(80, 160, 80), chars: ["✦", "·", "·"] },
56
+ groundChars: ["▄", "▄", "▓", "▄", "▒", "▄"],
57
+ structure: [
58
+ `${c(40, 110, 40)} ▄██████▄${NC}`,
59
+ `${c(50, 130, 50)}▄██${c(200, 50, 30)}●${c(50, 130, 50)}███${c(200, 50, 30)}●${c(50, 130, 50)}██▄${NC}`,
60
+ `${c(60, 150, 60)}████${c(200, 50, 30)}●${c(60, 150, 60)}███████${NC}`,
61
+ `${c(60, 150, 60)}██████${c(200, 50, 30)}●${c(60, 150, 60)}████${NC}`,
62
+ `${c(50, 130, 50)} ▀████████▀${NC}`,
63
+ `${c(80, 60, 40)} ▟${c(60, 100, 50)}██${c(200, 200, 100)}▪▪${c(60, 100, 50)}██${c(80, 60, 40)}▙${NC}`,
64
+ `${c(80, 60, 40)} ║${c(60, 100, 50)}██${c(200, 200, 100)}▪▪${c(60, 100, 50)}██${c(80, 60, 40)}║${NC}`,
65
+ `${c(80, 60, 40)} ║██${c(100, 60, 30)}▐██▌${c(80, 60, 40)}██║${NC}`,
66
+ ],
67
+ };
68
+
69
+ const ocean: Biome = {
70
+ name: "ocean",
71
+ sky: c(15, 20, 55),
72
+ ground: c(40, 90, 140),
73
+ particle: { color: c(100, 140, 220), chars: ["✦", "·", "~"] },
74
+ groundChars: ["▄", "~", "≈", "▄", "~", "▄"],
75
+ structure: [
76
+ `${c(200, 200, 100)} ◉${NC}`,
77
+ `${c(200, 50, 50)} ▄███▄${NC}`,
78
+ `${c(220, 220, 220)} ▐${c(200, 50, 50)}█${c(220, 220, 220)}███${c(200, 50, 50)}█${c(220, 220, 220)}▌${NC}`,
79
+ `${c(220, 220, 220)} ▐█████▌${NC}`,
80
+ `${c(220, 220, 220)} ▐█████▌${NC}`,
81
+ `${c(180, 180, 180)} ▟███████▙${NC}`,
82
+ ],
83
+ };
84
+
85
+ const cyberpunk: Biome = {
86
+ name: "cyberpunk",
87
+ sky: c(20, 10, 35),
88
+ ground: c(40, 30, 60),
89
+ particle: { color: c(180, 80, 220), chars: ["✦", "·", "░"] },
90
+ groundChars: ["▀", "▄", "█", "▀", "▄", "░"],
91
+ structure: [
92
+ `${c(255, 0, 100)} ▄${c(0, 255, 200)}█${c(255, 0, 100)}█${c(0, 255, 200)}█${c(255, 0, 100)}▄${NC}`,
93
+ `${c(80, 60, 100)} █${c(0, 255, 200)}▪${c(80, 60, 100)}█${c(255, 0, 100)}▪${c(80, 60, 100)}█${NC}`,
94
+ `${c(80, 60, 100)} █${c(255, 0, 100)}▪${c(80, 60, 100)}█${c(0, 255, 200)}▪${c(80, 60, 100)}█${NC}`,
95
+ `${c(80, 60, 100)} █${c(0, 255, 200)}▪${c(80, 60, 100)}█${c(255, 0, 100)}▪${c(80, 60, 100)}█${NC}`,
96
+ `${c(80, 60, 100)} ██${c(0, 255, 200)}▪${c(80, 60, 100)}█${c(255, 0, 100)}▪${c(80, 60, 100)}██${NC}`,
97
+ `${c(80, 60, 100)} ██${c(255, 200, 0)}▐▌${c(80, 60, 100)}█${c(255, 200, 0)}▐▌${c(80, 60, 100)}██${NC}`,
98
+ ],
99
+ };
100
+
101
+ const space: Biome = {
102
+ name: "space",
103
+ sky: c(8, 5, 20),
104
+ ground: c(50, 40, 70),
105
+ particle: { color: c(255, 200, 50), chars: ["✦", "·", "·", "✧"] },
106
+ groundChars: ["▄", "░", "▒", "▄", "▓", "░"],
107
+ structure: [
108
+ `${c(255, 200, 50)} ✨${NC}`,
109
+ `${c(100, 80, 150)} ▄████▄${NC}`,
110
+ `${c(120, 100, 170)} ▟${c(150, 130, 200)}██${c(255, 220, 100)}◆◆${c(150, 130, 200)}██${c(120, 100, 170)}▙${NC}`,
111
+ `${c(150, 130, 200)} ██${c(255, 220, 100)}◆◆${c(150, 130, 200)}████${NC}`,
112
+ `${c(150, 130, 200)} ██${c(255, 220, 100)}◆◆${c(150, 130, 200)}█${c(200, 180, 255)}▐██▌${c(150, 130, 200)}██${NC}`,
113
+ `${c(150, 130, 200)} ████████████${NC}`,
114
+ ],
115
+ };
116
+
117
+ // ═══════════════════════════════════════════════════════════════════════════
118
+ // EXTRA BIOMES (selectable via --biome flag)
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+
121
+ const volcano: Biome = {
122
+ name: "volcano",
123
+ sky: c(50, 15, 5),
124
+ ground: c(80, 30, 10),
125
+ particle: { color: c(255, 120, 20), chars: ["✦", "·", "▪"] },
126
+ groundChars: ["▄", "▓", "▄", "█", "▒", "▄"],
127
+ structure: [
128
+ `${c(255, 80, 0)} ▓${NC}`,
129
+ `${c(255, 120, 20)} ▄${c(200, 50, 10)}█${c(255, 120, 20)}▄${NC}`,
130
+ `${c(200, 50, 10)} ▟███▙${NC}`,
131
+ `${c(150, 40, 10)} ▟█████▙${NC}`,
132
+ `${c(120, 30, 10)} ▟███████▙${NC}`,
133
+ `${c(100, 25, 8)} ▟█████████▙${NC}`,
134
+ ],
135
+ };
136
+
137
+ const arctic: Biome = {
138
+ name: "arctic",
139
+ sky: c(20, 30, 50),
140
+ ground: c(180, 200, 220),
141
+ particle: { color: c(200, 220, 255), chars: ["❄", "·", "✧"] },
142
+ groundChars: ["▄", "▄", "░", "▄", "▄", "▒"],
143
+ structure: [
144
+ `${c(200, 220, 240)} ▄▄▄${NC}`,
145
+ `${c(200, 220, 240)} ▄█████▄${NC}`,
146
+ `${c(220, 230, 245)} █████████${NC}`,
147
+ `${c(220, 230, 245)} ███${c(60, 80, 120)}▐██▌${c(220, 230, 245)}███${NC}`,
148
+ `${c(220, 230, 245)} █████████${NC}`,
149
+ `${c(200, 220, 240)} █████████${NC}`,
150
+ ],
151
+ };
152
+
153
+ const desert: Biome = {
154
+ name: "desert",
155
+ sky: c(40, 25, 15),
156
+ ground: c(180, 150, 80),
157
+ particle: { color: c(200, 180, 100), chars: ["✦", "·", "·"] },
158
+ groundChars: ["▄", "░", "▄", "▄", "░", "▄"],
159
+ structure: [
160
+ `${c(200, 180, 100)} ▲${NC}`,
161
+ `${c(200, 180, 100)} ▟█▙${NC}`,
162
+ `${c(190, 170, 90)} ▟███▙${NC}`,
163
+ `${c(180, 160, 80)} ▟█████▙${NC}`,
164
+ `${c(170, 150, 70)} ▟███████▙${NC}`,
165
+ `${c(160, 140, 60)} ▟█████████▙${NC}`,
166
+ ],
167
+ };
168
+
169
+ const haunted: Biome = {
170
+ name: "haunted",
171
+ sky: c(15, 10, 25),
172
+ ground: c(40, 35, 50),
173
+ particle: { color: c(100, 80, 140), chars: ["✦", "·", "👻"] },
174
+ groundChars: ["▄", "▒", "▄", "▓", "▄", "▒"],
175
+ structure: [
176
+ `${c(60, 50, 80)} ▄█▄${NC}`,
177
+ `${c(60, 50, 80)} ▄█████▄${NC}`,
178
+ `${c(50, 40, 70)} ██${c(200, 50, 50)}▪▪▪${c(50, 40, 70)}██${NC}`,
179
+ `${c(50, 40, 70)} ██${c(200, 50, 50)}▪▪▪${c(50, 40, 70)}██${NC}`,
180
+ `${c(50, 40, 70)} ████${c(30, 20, 40)}▐▌${c(50, 40, 70)}████${NC}`,
181
+ `${c(40, 30, 60)} ████████████${NC}`,
182
+ ],
183
+ };
184
+
185
+ const sakura: Biome = {
186
+ name: "sakura",
187
+ sky: c(40, 20, 35),
188
+ ground: c(80, 60, 50),
189
+ particle: { color: c(255, 150, 180), chars: ["✿", "·", "✦"] },
190
+ groundChars: ["▄", "▄", "▓", "▄", "▄", "▒"],
191
+ structure: [
192
+ `${c(200, 50, 80)} ▄▄▄▄▄${NC}`,
193
+ `${c(200, 50, 80)} ▄███████▄${NC}`,
194
+ `${c(160, 40, 60)} ▀███████▀${NC}`,
195
+ `${c(120, 70, 50)} ║║${NC}`,
196
+ `${c(120, 70, 50)} ║║${NC}`,
197
+ `${c(120, 70, 50)} ▄▄║║▄▄${NC}`,
198
+ ],
199
+ };
200
+
201
+ const underwater: Biome = {
202
+ name: "underwater",
203
+ sky: c(10, 30, 60),
204
+ ground: c(40, 80, 60),
205
+ particle: { color: c(80, 180, 220), chars: ["°", "·", "○"] },
206
+ groundChars: ["▄", "▒", "▓", "▄", "▒", "▄"],
207
+ structure: [
208
+ `${c(80, 200, 100)} \\│//${NC}`,
209
+ `${c(80, 200, 100)} \\│/${NC}`,
210
+ `${c(80, 200, 100)} │${NC}`,
211
+ `${c(255, 100, 80)} ▄███▄${NC}`,
212
+ `${c(255, 80, 60)} ▟█████▙${NC}`,
213
+ `${c(255, 60, 40)} ▟███████▙${NC}`,
214
+ ],
215
+ };
216
+
217
+ const candyland: Biome = {
218
+ name: "candyland",
219
+ sky: c(50, 20, 50),
220
+ ground: c(200, 100, 150),
221
+ particle: { color: c(255, 150, 200), chars: ["✦", "♥", "·"] },
222
+ groundChars: ["▄", "▄", "░", "▄", "▄", "▒"],
223
+ structure: [
224
+ `${c(255, 200, 100)} ★${NC}`,
225
+ `${c(200, 100, 50)} ▄█████▄${NC}`,
226
+ `${c(220, 150, 100)} █${c(255, 100, 150)}▪${c(100, 200, 255)}▪${c(255, 255, 100)}▪${c(100, 255, 100)}▪${c(255, 100, 150)}▪${c(220, 150, 100)}█${NC}`,
227
+ `${c(220, 150, 100)} █${c(100, 255, 100)}▪${c(255, 100, 150)}▪${c(100, 200, 255)}▪${c(255, 255, 100)}▪${c(100, 255, 100)}▪${c(220, 150, 100)}█${NC}`,
228
+ `${c(220, 150, 100)} █████${c(180, 80, 50)}▐▌${c(220, 150, 100)}████${NC}`,
229
+ `${c(220, 150, 100)} █████████████${NC}`,
230
+ ],
231
+ };
232
+
233
+ const dungeon: Biome = {
234
+ name: "dungeon",
235
+ sky: c(20, 18, 15),
236
+ ground: c(60, 55, 45),
237
+ particle: { color: c(255, 150, 30), chars: ["✦", "·", "·"] },
238
+ groundChars: ["▄", "█", "▄", "▓", "▄", "█"],
239
+ structure: [
240
+ `${c(80, 75, 65)} ▄█▄█▄█▄█▄${NC}`,
241
+ `${c(80, 75, 65)} ██████████${NC}`,
242
+ `${c(70, 65, 55)} ██${c(255, 150, 30)}▪▪${c(70, 65, 55)}██${c(255, 150, 30)}▪▪${c(70, 65, 55)}██${NC}`,
243
+ `${c(70, 65, 55)} ██████████${NC}`,
244
+ `${c(70, 65, 55)} ██${c(40, 35, 25)}▐██▌${c(70, 65, 55)}████${NC}`,
245
+ `${c(60, 55, 45)} ██████████${NC}`,
246
+ ],
247
+ };
248
+
249
+ const cloudkingdom: Biome = {
250
+ name: "cloudkingdom",
251
+ sky: c(30, 40, 70),
252
+ ground: c(180, 190, 220),
253
+ particle: { color: c(200, 210, 240), chars: ["✦", "☁", "·"] },
254
+ groundChars: ["▄", "░", "▄", "▄", "░", "░"],
255
+ structure: [
256
+ `${c(200, 180, 255)} ▲${NC}`,
257
+ `${c(200, 180, 255)} ▟█▙${NC}`,
258
+ `${c(180, 160, 240)} ▟███▙${NC}`,
259
+ `${c(180, 160, 240)} █${c(255, 220, 100)}◆◆${c(180, 160, 240)}██${NC}`,
260
+ `${c(180, 160, 240)} █████${NC}`,
261
+ `${c(200, 200, 230)} ▟█████████▙${NC}`,
262
+ ],
263
+ };
264
+
265
+ const matrix: Biome = {
266
+ name: "matrix",
267
+ sky: c(0, 10, 0),
268
+ ground: c(0, 40, 0),
269
+ particle: { color: c(0, 200, 0), chars: ["0", "1", "·"] },
270
+ groundChars: ["▄", "█", "▄", "▀", "▄", "█"],
271
+ structure: [
272
+ `${c(0, 150, 0)} ▄██████▄${NC}`,
273
+ `${c(0, 120, 0)} █${c(0, 255, 0)}01${c(0, 120, 0)}██${c(0, 255, 0)}10${c(0, 120, 0)}█${NC}`,
274
+ `${c(0, 120, 0)} █${c(0, 255, 0)}10${c(0, 120, 0)}██${c(0, 255, 0)}01${c(0, 120, 0)}█${NC}`,
275
+ `${c(0, 120, 0)} █${c(0, 255, 0)}01${c(0, 120, 0)}██${c(0, 255, 0)}10${c(0, 120, 0)}█${NC}`,
276
+ `${c(0, 100, 0)} █${c(0, 200, 0)}▐▌${c(0, 100, 0)}██${c(0, 200, 0)}▐▌${c(0, 100, 0)}█${NC}`,
277
+ `${c(0, 80, 0)} ▀██████▀${NC}`,
278
+ ],
279
+ };
280
+
281
+ // ─── Registry ───────────────────────────────────────────────────────────────
282
+
283
+ const RARITY_BIOMES: Record<string, Biome> = {
284
+ common: meadow,
285
+ uncommon: forest,
286
+ rare: ocean,
287
+ epic: cyberpunk,
288
+ legendary: space,
289
+ };
290
+
291
+ const ALL_BIOMES: Record<string, Biome> = {
292
+ // Defaults (by rarity name)
293
+ meadow, forest, ocean, cyberpunk, space,
294
+ // Extras
295
+ volcano, arctic, desert, haunted, sakura,
296
+ underwater, candyland, dungeon, cloudkingdom, matrix,
297
+ };
298
+
299
+ export function getBiome(rarity: string, override?: string): Biome {
300
+ if (override && ALL_BIOMES[override]) return ALL_BIOMES[override];
301
+ return RARITY_BIOMES[rarity] ?? meadow;
302
+ }
303
+
304
+ export function listBiomes(): { name: string; isDefault: boolean }[] {
305
+ return Object.keys(ALL_BIOMES).map(name => ({
306
+ name,
307
+ isDefault: Object.values(RARITY_BIOMES).some(b => b.name === name),
308
+ }));
309
+ }