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 +67 -0
- package/package.json +5 -5
- package/src/plant.js +12 -5
- package/src/renderer.js +58 -11
- package/src/viewer.js +12 -1
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.
|
|
4
|
-
"description": "Grow a
|
|
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/
|
|
27
|
+
"url": "git+https://github.com/Varun2009178/honeytree.git"
|
|
28
28
|
},
|
|
29
|
-
"homepage": "https://github.com/
|
|
29
|
+
"homepage": "https://github.com/Varun2009178/honeytree#readme",
|
|
30
30
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/
|
|
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,
|
|
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,
|
|
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 %
|
|
84
|
+
if (seeded % biome.starDensity !== 0) continue;
|
|
40
85
|
stars.push({
|
|
41
86
|
x,
|
|
42
87
|
y: seeded % SKY_ROWS,
|
|
43
|
-
char:
|
|
44
|
-
color: seeded %
|
|
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:
|
|
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
|
});
|