honeytree 1.0.0 → 1.0.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.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # Honeytree
2
+
3
+ Grow a pixel-art forest in your terminal every time you use Claude Code.
4
+
5
+ Each prompt plants a new tree. Each tree grows over time. Your forest evolves from a quiet clearing into an ancient woodland — and it never resets.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g honeytree
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ ```bash
16
+ honeytree init
17
+ ```
18
+
19
+ This creates `~/.honeydew/forest.json` and adds a `Stop` hook to your Claude Code settings so a tree is planted after every response.
20
+
21
+ ## Watch your forest
22
+
23
+ ```bash
24
+ honeytree
25
+ ```
26
+
27
+ Open this in a separate terminal. It watches your forest file and animates new trees as they appear. Press `Ctrl+C` to exit.
28
+
29
+ ## How it works
30
+
31
+ 1. **`honeytree init`** — Creates the forest state file and registers a Claude Code hook
32
+ 2. **`honeytree`** — Opens the viewer that renders your forest in real time
33
+
34
+ After init, a tree is automatically planted after every Claude Code response via the hook. Each tree is a random species (oak, pine, birch, willow, or cherry) and growth stage. Existing young trees grow a little each time too.
35
+
36
+ ## Biomes
37
+
38
+ Your forest evolves as it grows:
39
+
40
+ | Trees | Biome | What changes |
41
+ |-------|-------|-------------|
42
+ | 0–9 | Clearing | Sparse stars, light ground |
43
+ | 10–24 | Grove | More stars, richer ground |
44
+ | 25–49 | Woodland | Dense canopy, varied starlight |
45
+ | 50–99 | Old Growth | Deep greens, warm starlight |
46
+ | 100+ | Ancient Forest | Richest palette, brightest sky |
47
+
48
+ Trees are never deleted. The forest only grows.
49
+
50
+ ## Tree types
51
+
52
+ - **Oak** — Wide, rounded canopy
53
+ - **Pine** — Tall, triangular shape
54
+ - **Birch** — Light trunk, bright leaves
55
+ - **Willow** — Drooping canopy
56
+ - **Cherry** — Pink blossoms
57
+
58
+ Each type has 4 growth stages: seed, sapling, young, and full.
59
+
60
+ ## Requirements
61
+
62
+ - Node.js 18+
63
+ - Claude Code (for the automatic hook)
64
+
65
+ ## License
66
+
67
+ MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "honeytree",
3
- "version": "1.0.0",
4
- "description": "Grow a pixel-art forest in your terminal every time you use Claude Code",
3
+ "version": "1.0.1",
4
+ "description": "Grow a forest in your terminal every time you use Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "honeytree": "./bin/honeydew.js"
@@ -24,11 +24,11 @@
24
24
  ],
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/varunnukala/honeytree.git"
27
+ "url": "git+https://github.com/Varun2009178/honeytree.git"
28
28
  },
29
- "homepage": "https://github.com/varunnukala/honeytree#readme",
29
+ "homepage": "https://github.com/Varun2009178/honeytree#readme",
30
30
  "bugs": {
31
- "url": "https://github.com/varunnukala/honeytree/issues"
31
+ "url": "https://github.com/Varun2009178/honeytree/issues"
32
32
  },
33
33
  "author": "Varun Nukala",
34
34
  "license": "MIT",
package/src/plant.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import { getSprite, TREE_TYPES } from "./sprites.js";
2
2
  import { createEmptyForest, readForest, writeForest } from "./state.js";
3
3
 
4
- const DEFAULT_WIDTH = 80;
5
4
  const MIN_GAP = 2;
5
+ const DEFAULT_WIDTH = 80;
6
+
7
+ function getPlantWidth(forest) {
8
+ // Use the width saved by the viewer, fall back to default
9
+ if (forest.viewerWidth && forest.viewerWidth > 40) return forest.viewerWidth;
10
+ return DEFAULT_WIDTH;
11
+ }
6
12
 
7
13
  function randomItem(items) {
8
14
  return items[Math.floor(Math.random() * items.length)];
@@ -20,7 +26,7 @@ function occupiedRanges(trees) {
20
26
  });
21
27
  }
22
28
 
23
- function findOpenX(trees, type, growth) {
29
+ function findOpenX(trees, type, growth, width) {
24
30
  const sprite = getSprite(type, growth);
25
31
  const half = Math.floor(sprite.width / 2);
26
32
  const margin = half + 1;
@@ -28,7 +34,7 @@ function findOpenX(trees, type, growth) {
28
34
 
29
35
  for (let attempt = 0; attempt < 100; attempt += 1) {
30
36
  const x =
31
- margin + Math.floor(Math.random() * Math.max(1, DEFAULT_WIDTH - margin * 2));
37
+ margin + Math.floor(Math.random() * Math.max(1, width - margin * 2));
32
38
  const left = x - half;
33
39
  const right = x + half;
34
40
  const collides = ranges.some(
@@ -38,7 +44,7 @@ function findOpenX(trees, type, growth) {
38
44
  if (!collides) return x;
39
45
  }
40
46
 
41
- return margin + Math.floor(Math.random() * Math.max(1, DEFAULT_WIDTH - margin * 2));
47
+ return margin + Math.floor(Math.random() * Math.max(1, width - margin * 2));
42
48
  }
43
49
 
44
50
  function nudgeGrowth(growth) {
@@ -49,6 +55,7 @@ function nudgeGrowth(growth) {
49
55
 
50
56
  export async function plant() {
51
57
  const forest = readForest() ?? createEmptyForest();
58
+ const width = getPlantWidth(forest);
52
59
 
53
60
  for (const tree of forest.trees) {
54
61
  tree.growth = nudgeGrowth(tree.growth);
@@ -62,7 +69,7 @@ export async function plant() {
62
69
  id: nextId,
63
70
  type,
64
71
  growth,
65
- x: findOpenX(forest.trees, type, growth),
72
+ x: findOpenX(forest.trees, type, growth, width),
66
73
  plantedAt: new Date().toISOString(),
67
74
  });
68
75
  forest.totalPrompts += 1;
package/src/renderer.js CHANGED
@@ -11,14 +11,59 @@ const STATS_ROWS = 1;
11
11
  export const SCENE_HEIGHT =
12
12
  SKY_ROWS + TREE_ROWS + GROUND_ROWS + SPACER_ROWS + STATS_ROWS;
13
13
 
14
- const STAR_GLYPHS = ["·", "·", "✦", "."];
15
- const GROUND_COLORS = ["#22492d", "#18361f"];
16
14
  const STATS_ACCENT = "#f5a50b";
17
15
  const STATS_TEXT = "#8e8a84";
18
16
  const BAR_FILL = "#6cb95e";
19
17
  const BAR_EMPTY = "#3d3d3d";
20
18
  const MILESTONES = [10, 25, 50, 100, 250, 500, 1000];
21
19
 
20
+ // Biomes evolve as the forest grows — never resets, only gets richer
21
+ const BIOMES = [
22
+ { // 0-9: sparse clearing
23
+ ground: ["#2a3a28", "#1e2d1c"],
24
+ starGlyphs: ["·", ".", " ", " "],
25
+ starDensity: 12,
26
+ starColors: ["#3a3a3a", "#444444"],
27
+ label: "clearing",
28
+ },
29
+ { // 10-24: young grove
30
+ ground: ["#22492d", "#18361f"],
31
+ starGlyphs: ["·", "·", "✦", "."],
32
+ starDensity: 9,
33
+ starColors: ["#444444", "#5d5d5d"],
34
+ label: "grove",
35
+ },
36
+ { // 25-49: dense woodland
37
+ ground: ["#1e4a28", "#163a1e"],
38
+ starGlyphs: ["·", "✦", "✧", "·", "."],
39
+ starDensity: 7,
40
+ starColors: ["#4d4d4d", "#5d5d5d", "#6a6a55"],
41
+ label: "woodland",
42
+ },
43
+ { // 50-99: old growth
44
+ ground: ["#1a5230", "#124020"],
45
+ starGlyphs: ["✦", "✧", "·", "·", "✦", "."],
46
+ starDensity: 6,
47
+ starColors: ["#5d5d5d", "#6d6d5a", "#7a7a60"],
48
+ label: "old growth",
49
+ },
50
+ { // 100+: ancient forest
51
+ ground: ["#165a32", "#0e4822"],
52
+ starGlyphs: ["✦", "✧", "·", "✦", "⋆", "."],
53
+ starDensity: 5,
54
+ starColors: ["#6d6d5a", "#7a7a60", "#8a8a6a"],
55
+ label: "ancient forest",
56
+ },
57
+ ];
58
+
59
+ function getBiome(treeCount) {
60
+ if (treeCount < 10) return BIOMES[0];
61
+ if (treeCount < 25) return BIOMES[1];
62
+ if (treeCount < 50) return BIOMES[2];
63
+ if (treeCount < 100) return BIOMES[3];
64
+ return BIOMES[4];
65
+ }
66
+
22
67
  function createBuffer(width) {
23
68
  return Array.from({ length: SCENE_HEIGHT }, () =>
24
69
  Array.from({ length: width }, () => ({ char: " ", color: null })),
@@ -32,16 +77,16 @@ function hash(seed) {
32
77
  return ((value >>> 16) ^ value) >>> 0;
33
78
  }
34
79
 
35
- function generateStars(width, twinkle = 0) {
80
+ function generateStars(width, biome, twinkle = 0) {
36
81
  const stars = [];
37
82
  for (let x = 0; x < width; x += 1) {
38
83
  const seeded = hash(x + width * 17 + twinkle * 101);
39
- if (seeded % 9 !== 0) continue;
84
+ if (seeded % biome.starDensity !== 0) continue;
40
85
  stars.push({
41
86
  x,
42
87
  y: seeded % SKY_ROWS,
43
- char: STAR_GLYPHS[seeded % STAR_GLYPHS.length],
44
- color: seeded % 3 === 0 ? "#5d5d5d" : "#444444",
88
+ char: biome.starGlyphs[seeded % biome.starGlyphs.length],
89
+ color: biome.starColors[seeded % biome.starColors.length],
45
90
  });
46
91
  }
47
92
  return stars;
@@ -78,7 +123,7 @@ function getDayCount(createdAt) {
78
123
  return Math.max(1, days + 1);
79
124
  }
80
125
 
81
- function buildStatsLine(forest) {
126
+ function buildStatsLine(forest, biome) {
82
127
  const treeCount = forest.trees.length;
83
128
  const milestone = getNextMilestone(treeCount);
84
129
  const progress = milestone === 0 ? 0 : treeCount / milestone;
@@ -96,7 +141,8 @@ function buildStatsLine(forest) {
96
141
  )} day${getDayCount(forest.createdAt) === 1 ? "" : "s"} · `,
97
142
  ) +
98
143
  bar +
99
- chalk.hex(STATS_TEXT)(` next: ${getNextTreeType(treeCount)}`)
144
+ chalk.hex(STATS_TEXT)(` next: ${getNextTreeType(treeCount)}`) +
145
+ chalk.hex("#555555")(` [${biome.label}]`)
100
146
  );
101
147
  }
102
148
 
@@ -104,8 +150,9 @@ export function renderFrame(forest, termWidth = 80, options = {}) {
104
150
  const width = Math.max(40, termWidth);
105
151
  const buffer = createBuffer(width);
106
152
  const groundStart = SKY_ROWS + TREE_ROWS;
153
+ const biome = getBiome(forest.trees.length);
107
154
 
108
- for (const star of generateStars(width, options.twinkleSeed ?? 0)) {
155
+ for (const star of generateStars(width, biome, options.twinkleSeed ?? 0)) {
109
156
  buffer[star.y][star.x] = { char: star.char, color: star.color };
110
157
  }
111
158
 
@@ -113,7 +160,7 @@ export function renderFrame(forest, termWidth = 80, options = {}) {
113
160
  for (let x = 0; x < width; x += 1) {
114
161
  buffer[groundStart + rowIndex][x] = {
115
162
  char: "█",
116
- color: GROUND_COLORS[rowIndex],
163
+ color: biome.ground[rowIndex],
117
164
  };
118
165
  }
119
166
  }
@@ -133,7 +180,7 @@ export function renderFrame(forest, termWidth = 80, options = {}) {
133
180
  }
134
181
 
135
182
  lines.push("");
136
- lines.push(buildStatsLine(forest));
183
+ lines.push(buildStatsLine(forest, biome));
137
184
 
138
185
  return lines.join("\n");
139
186
  }
package/src/viewer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
 
3
3
  import { renderFrame } from "./renderer.js";
4
- import { getForestFile, readForest } from "./state.js";
4
+ import { getForestFile, readForest, writeForest } from "./state.js";
5
5
 
6
6
  function writeAnsi(code) {
7
7
  process.stdout.write(code);
@@ -63,6 +63,16 @@ export async function viewer() {
63
63
  process.exit(1);
64
64
  }
65
65
 
66
+ // Save terminal width so plant knows how wide to spread trees
67
+ function syncWidth() {
68
+ const cols = process.stdout.columns || 80;
69
+ if (forest.viewerWidth !== cols) {
70
+ forest.viewerWidth = cols;
71
+ writeForest(forest);
72
+ }
73
+ }
74
+
75
+ syncWidth();
66
76
  hideCursor();
67
77
  clearScreen();
68
78
  renderForest(forest);
@@ -81,6 +91,7 @@ export async function viewer() {
81
91
  process.on("SIGINT", cleanup);
82
92
  process.on("SIGTERM", cleanup);
83
93
  process.stdout.on("resize", () => {
94
+ syncWidth();
84
95
  clearScreen();
85
96
  renderForest(forest);
86
97
  });