honeytree 1.0.5 → 1.0.7

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.
@@ -0,0 +1,225 @@
1
+ import chalk from "chalk";
2
+
3
+ import { getDynamicScene } from "../core/animation.js";
4
+ import { getEnvironmentSnapshot } from "../core/environment.js";
5
+ import {
6
+ getAnimalSprite,
7
+ getGroundElementSprite,
8
+ getLlamaSprite,
9
+ getTreeSprite,
10
+ materializeSprite,
11
+ } from "../core/sprites.js";
12
+
13
+ const SKY_ROWS = 4;
14
+ const FOREST_ROWS = 8;
15
+ const GROUND_ROWS = 1;
16
+ const ART_ROWS = SKY_ROWS + FOREST_ROWS + GROUND_ROWS;
17
+ const STATS_ROWS = 1;
18
+
19
+ export const SCENE_HEIGHT = ART_ROWS + STATS_ROWS;
20
+
21
+ function createBuffer(width) {
22
+ return Array.from({ length: ART_ROWS }, () =>
23
+ Array.from({ length: width }, () => ({ char: " ", color: null })),
24
+ );
25
+ }
26
+
27
+ function paint(buffer, x, y, char, color) {
28
+ if (y < 0 || y >= buffer.length) return;
29
+ if (x < 0 || x >= buffer[0].length) return;
30
+ buffer[y][x] = { char, color };
31
+ }
32
+
33
+ function compositeSprite(buffer, sprite, centerX, baseY, palette) {
34
+ const cells = materializeSprite(sprite, palette);
35
+ const offsetX = Math.round(centerX) - Math.floor(cells.width / 2);
36
+ for (let rowIndex = 0; rowIndex < cells.rows.length; rowIndex += 1) {
37
+ const row = cells.rows[rowIndex];
38
+ const y = baseY - rowIndex;
39
+ for (let columnIndex = 0; columnIndex < row.length; columnIndex += 1) {
40
+ const [char, color] = row[columnIndex];
41
+ if (!color) continue;
42
+ paint(buffer, offsetX + columnIndex, y, char, color);
43
+ }
44
+ }
45
+ }
46
+
47
+ function colorize(text, color, enabled) {
48
+ if (!enabled || !color) {
49
+ return text;
50
+ }
51
+ return chalk.hex(color)(text);
52
+ }
53
+
54
+ function drawSky(buffer, width, environment, tick) {
55
+ if (environment.sky.name === "night") {
56
+ const stars = getDynamicScene({ animals: [] }, width, tick, environment).stars;
57
+ for (const star of stars) {
58
+ paint(buffer, star.x, star.y, star.twinkle > 0.5 ? "✦" : "·", environment.sky.haze);
59
+ }
60
+ }
61
+
62
+ const clouds = getDynamicScene({ animals: [] }, width, tick, environment).clouds;
63
+ for (const cloud of clouds) {
64
+ const glyph = environment.sky.name === "night" ? "░" : "▓";
65
+ for (let offset = 0; offset < cloud.length; offset += 1) {
66
+ paint(buffer, cloud.x + offset, cloud.y, glyph, environment.sky.haze);
67
+ }
68
+ }
69
+
70
+ const celestialX = Math.round(((width - 6) * environment.sky.progress) + 3);
71
+ const celestialY = environment.sky.name === "night" ? 1 : Math.max(0, 2 - Math.round(environment.sky.progress * 2));
72
+ paint(
73
+ buffer,
74
+ celestialX,
75
+ celestialY,
76
+ environment.sky.name === "night" ? "◐" : "●",
77
+ environment.sky.celestial,
78
+ );
79
+ }
80
+
81
+ function drawWeather(buffer, width, environment, tick) {
82
+ const scene = getDynamicScene({ animals: [] }, width, tick, environment);
83
+ if (environment.weather.name === "rain") {
84
+ for (const drop of scene.particles) {
85
+ if (drop.kind !== "rain") continue;
86
+ paint(buffer, Math.round(drop.x), Math.round(drop.y), "╲", "#8ecae6");
87
+ }
88
+ }
89
+
90
+ if (environment.weather.name === "aurora") {
91
+ for (const band of scene.particles) {
92
+ if (band.kind !== "aurora") continue;
93
+ paint(buffer, band.x, Math.round(band.y), "~", band.alpha > 0.45 ? "#80ffdb" : "#64dfdf");
94
+ }
95
+ }
96
+
97
+ if (environment.weather.name === "rainbow") {
98
+ const midpoint = Math.floor(width / 2);
99
+ const colors = ["#f94144", "#f8961e", "#f9c74f", "#90be6d", "#577590"];
100
+ colors.forEach((color, index) => {
101
+ paint(buffer, midpoint - 2 + index, 1 + (index % 2), "◜", color);
102
+ paint(buffer, midpoint + 1 - index, 1 + (index % 2), "◝", color);
103
+ });
104
+ }
105
+ }
106
+
107
+ function drawSeasonParticles(buffer, width, environment, tick) {
108
+ const scene = getDynamicScene({ animals: [] }, width, tick, environment);
109
+ for (const particle of scene.particles) {
110
+ if (particle.kind === "petal") {
111
+ paint(buffer, Math.round(particle.x), Math.round(particle.y), "•", "#ffcad4");
112
+ }
113
+ if (particle.kind === "leaf") {
114
+ paint(buffer, Math.round(particle.x), Math.round(particle.y), "•", "#f6bd60");
115
+ }
116
+ if (particle.kind === "snow") {
117
+ paint(buffer, Math.round(particle.x), Math.round(particle.y), "*", "#f1faee");
118
+ }
119
+ if (particle.kind === "firefly") {
120
+ paint(buffer, Math.round(particle.x), Math.round(particle.y), "•", "#fef08a");
121
+ }
122
+ }
123
+ }
124
+
125
+ function drawForest(buffer, width, state, environment, tick) {
126
+ const treeBaseY = SKY_ROWS + FOREST_ROWS - 1;
127
+ for (const tree of state.trees) {
128
+ compositeSprite(buffer, getTreeSprite(tree.species), tree.x_position, treeBaseY, environment.palette);
129
+ }
130
+
131
+ for (const element of state.ground_elements) {
132
+ compositeSprite(buffer, getGroundElementSprite(element.type), element.x_position, ART_ROWS - 1, environment.palette);
133
+ }
134
+
135
+ const scene = getDynamicScene(state, width, tick, environment);
136
+ for (const animal of scene.animals) {
137
+ if (animal.type === "owl" && environment.sky.name !== "night") {
138
+ continue;
139
+ }
140
+ compositeSprite(
141
+ buffer,
142
+ getAnimalSprite(animal.type),
143
+ animal.x,
144
+ Math.min(ART_ROWS - 2, Math.round(animal.y)),
145
+ environment.palette,
146
+ );
147
+ }
148
+
149
+ // Llama companion
150
+ const llama = scene.llama;
151
+ if (llama) {
152
+ compositeSprite(
153
+ buffer,
154
+ getLlamaSprite(llama.pose),
155
+ llama.x,
156
+ Math.min(ART_ROWS - 2, llama.y),
157
+ environment.palette,
158
+ );
159
+ }
160
+ }
161
+
162
+ function drawGround(buffer, width, environment) {
163
+ for (let x = 0; x < width; x += 1) {
164
+ paint(buffer, x, ART_ROWS - 1, "▄", x % 3 === 0 ? environment.palette.groundLight : environment.palette.ground);
165
+ }
166
+ }
167
+
168
+ function buildStatsLine(state, environment, width, color) {
169
+ const trees = `${state.trees.length} tree${state.trees.length === 1 ? "" : "s"}`;
170
+ const animals = `${state.animals.length} animal${state.animals.length === 1 ? "" : "s"}`;
171
+ const streak = `${state.current_streak} day streak`;
172
+ const weather = `${environment.season.icon} ${environment.season.label} ${environment.weather.label}`;
173
+ const separator = colorize(" • ", "#6b7280", color);
174
+ const line = [
175
+ colorize("🌲 " + trees, "#7bd389", color),
176
+ colorize("🐇 " + animals, "#f4d35e", color),
177
+ colorize("🔥 " + streak, "#ee964b", color),
178
+ colorize(weather, "#d4d4d8", color),
179
+ ].join(separator);
180
+
181
+ if (line.length >= width) {
182
+ return line.slice(0, width);
183
+ }
184
+ return line.padEnd(width, " ");
185
+ }
186
+
187
+ export function buildTerminalScene(state, termWidth = 80, tick = 0, options = {}) {
188
+ const width = Math.max(20, termWidth);
189
+ const date = options.now instanceof Date ? options.now : new Date();
190
+ const environment = getEnvironmentSnapshot(date, state.current_streak ?? 0);
191
+ const buffer = createBuffer(width);
192
+
193
+ drawSky(buffer, width, environment, tick);
194
+ drawWeather(buffer, width, environment, tick);
195
+ drawSeasonParticles(buffer, width, environment, tick);
196
+ drawForest(buffer, width, state, environment, tick);
197
+ drawGround(buffer, width, environment);
198
+
199
+ return {
200
+ width,
201
+ environment,
202
+ buffer,
203
+ statsLine: buildStatsLine(state, environment, width, options.color !== false),
204
+ };
205
+ }
206
+
207
+ export function renderTerminalFrame(state, termWidth = 80, tick = 0, options = {}) {
208
+ if (termWidth < 20) {
209
+ const environment = getEnvironmentSnapshot(
210
+ options.now instanceof Date ? options.now : new Date(),
211
+ state.current_streak ?? 0,
212
+ );
213
+ return buildStatsLine(state, environment, Math.max(20, termWidth), options.color !== false);
214
+ }
215
+
216
+ const scene = buildTerminalScene(state, termWidth, tick, options);
217
+ const lines = scene.buffer.map((row) =>
218
+ row
219
+ .map((cell) => colorize(cell.char, cell.color, options.color !== false))
220
+ .join(""),
221
+ );
222
+ lines.push(scene.statsLine);
223
+ return lines.join("\n");
224
+ }
225
+
@@ -0,0 +1,43 @@
1
+ export function createActivityTracker({
2
+ onMinutes,
3
+ now = () => Date.now(),
4
+ activeWindowMs = 5 * 60 * 1000,
5
+ } = {}) {
6
+ let lastTickAt = now();
7
+ let lastActiveAt = 0;
8
+ let carriedMs = 0;
9
+
10
+ function markActive(at = now()) {
11
+ lastActiveAt = at;
12
+ }
13
+
14
+ function tick(at = now()) {
15
+ const elapsed = Math.max(0, at - lastTickAt);
16
+ lastTickAt = at;
17
+
18
+ if (!lastActiveAt || at - lastActiveAt > activeWindowMs) {
19
+ carriedMs = 0;
20
+ return 0;
21
+ }
22
+
23
+ carriedMs += elapsed;
24
+ const wholeMinutes = Math.floor(carriedMs / 60_000);
25
+ if (wholeMinutes > 0) {
26
+ carriedMs -= wholeMinutes * 60_000;
27
+ onMinutes?.(wholeMinutes, at);
28
+ }
29
+ return wholeMinutes;
30
+ }
31
+
32
+ return {
33
+ markActive,
34
+ tick,
35
+ start({ intervalMs = 60_000 } = {}) {
36
+ const timer = setInterval(() => {
37
+ tick(now());
38
+ }, intervalMs);
39
+ return () => clearInterval(timer);
40
+ },
41
+ };
42
+ }
43
+
@@ -0,0 +1,44 @@
1
+ import chokidar from "chokidar";
2
+
3
+ const IGNORED_SEGMENTS = [
4
+ "/.git/",
5
+ "/node_modules/",
6
+ "/dist/",
7
+ "/build/",
8
+ "/coverage/",
9
+ "/.next/",
10
+ "/out/",
11
+ ];
12
+
13
+ export function shouldTrackPath(filePath) {
14
+ const normalized = filePath.replace(/\\/g, "/");
15
+ return !IGNORED_SEGMENTS.some((segment) => normalized.includes(segment));
16
+ }
17
+
18
+ export function startFileTracker({
19
+ cwd = process.cwd(),
20
+ onSave,
21
+ watchImpl = chokidar.watch,
22
+ } = {}) {
23
+ const watcher = watchImpl(cwd, {
24
+ ignored: (filePath) => !shouldTrackPath(filePath),
25
+ ignoreInitial: true,
26
+ persistent: true,
27
+ });
28
+
29
+ const handleChange = (filePath) => {
30
+ if (shouldTrackPath(filePath)) {
31
+ onSave?.(filePath);
32
+ }
33
+ };
34
+
35
+ watcher.on("add", handleChange);
36
+ watcher.on("change", handleChange);
37
+
38
+ return {
39
+ close() {
40
+ return watcher.close();
41
+ },
42
+ };
43
+ }
44
+
@@ -0,0 +1,77 @@
1
+ import simpleGit from "simple-git";
2
+
3
+ export async function detectNewCommits({
4
+ cwd = process.cwd(),
5
+ lastCommitHash = "",
6
+ git = simpleGit({ baseDir: cwd }),
7
+ } = {}) {
8
+ try {
9
+ const isRepo = await git.checkIsRepo();
10
+ if (!isRepo) {
11
+ return { isRepo: false, latestHash: lastCommitHash, newCommits: [] };
12
+ }
13
+
14
+ const log = await git.log({ maxCount: 25 });
15
+ const latestHash = log.latest?.hash ?? lastCommitHash;
16
+ if (!latestHash || !lastCommitHash) {
17
+ return { isRepo: true, latestHash, newCommits: [] };
18
+ }
19
+
20
+ const newCommits = [];
21
+ for (const entry of log.all) {
22
+ if (entry.hash === lastCommitHash) {
23
+ break;
24
+ }
25
+ newCommits.push(entry.hash);
26
+ }
27
+
28
+ return {
29
+ isRepo: true,
30
+ latestHash,
31
+ newCommits: newCommits.reverse(),
32
+ };
33
+ } catch {
34
+ return { isRepo: false, latestHash: lastCommitHash, newCommits: [] };
35
+ }
36
+ }
37
+
38
+ export function startGitTracker({
39
+ cwd = process.cwd(),
40
+ lastCommitHash = "",
41
+ pollMs = 30_000,
42
+ onCommit,
43
+ gitFactory = (baseDir) => simpleGit({ baseDir }),
44
+ } = {}) {
45
+ const git = gitFactory(cwd);
46
+ let currentHash = lastCommitHash;
47
+ let stopped = false;
48
+
49
+ async function poll() {
50
+ if (stopped) {
51
+ return { isRepo: false, latestHash: currentHash, newCommits: [] };
52
+ }
53
+
54
+ const result = await detectNewCommits({ cwd, lastCommitHash: currentHash, git });
55
+ currentHash = result.latestHash || currentHash;
56
+
57
+ for (const commitHash of result.newCommits) {
58
+ await onCommit?.(commitHash);
59
+ currentHash = commitHash;
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ const timer = setInterval(() => {
66
+ poll().catch(() => {});
67
+ }, pollMs);
68
+
69
+ return {
70
+ poll,
71
+ stop() {
72
+ stopped = true;
73
+ clearInterval(timer);
74
+ },
75
+ };
76
+ }
77
+
package/src/init.js DELETED
@@ -1,86 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- import {
6
- createEmptyForest,
7
- getHoneydewDir,
8
- readForest,
9
- writeForest,
10
- } from "./state.js";
11
-
12
- function getClaudeSettingsPath() {
13
- const claudeRoot =
14
- process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
15
- return path.join(claudeRoot, "settings.json");
16
- }
17
-
18
- const HONEYDEW_STOP_HOOK = {
19
- matcher: "",
20
- hooks: [
21
- {
22
- type: "command",
23
- command: "honeytree plant",
24
- },
25
- ],
26
- };
27
-
28
- function hasHoneydewHook(settings) {
29
- return (
30
- settings?.hooks?.Stop?.some((entry) =>
31
- entry?.hooks?.some((hook) => hook?.command === "honeytree plant"),
32
- ) ?? false
33
- );
34
- }
35
-
36
- export async function init() {
37
- const honeydewDir = getHoneydewDir();
38
- fs.mkdirSync(honeydewDir, { recursive: true });
39
-
40
- const existing = readForest();
41
- if (!existing) {
42
- writeForest(createEmptyForest());
43
- console.log(`Created ${path.join(honeydewDir, "forest.json")}`);
44
- } else {
45
- let migrated = false;
46
- if (existing.lastActiveDate === undefined) {
47
- existing.lastActiveDate = new Date().toISOString().slice(0, 10);
48
- migrated = true;
49
- }
50
- if (existing.streak === undefined) {
51
- existing.streak = 0;
52
- migrated = true;
53
- }
54
- if (migrated) {
55
- writeForest(existing);
56
- console.log(`Updated forest with streak tracking (${existing.trees.length} trees kept)`);
57
- } else {
58
- console.log(`Forest already up to date at ${path.join(honeydewDir, "forest.json")}`);
59
- }
60
- }
61
-
62
- const settingsPath = getClaudeSettingsPath();
63
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
64
-
65
- let settings = {};
66
- try {
67
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
68
- } catch {
69
- settings = {};
70
- }
71
-
72
- settings.hooks ??= {};
73
- settings.hooks.Stop ??= [];
74
-
75
- if (hasHoneydewHook(settings)) {
76
- console.log(`Claude Code hook already configured in ${settingsPath}`);
77
- } else {
78
- settings.hooks.Stop.push(HONEYDEW_STOP_HOOK);
79
- fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
80
- console.log(`Added honeytree Stop hook to ${settingsPath}`);
81
- }
82
-
83
- console.log("");
84
- console.log("Setup complete.");
85
- console.log("Run `honeytree` in a separate terminal to watch the forest grow.");
86
- }
package/src/plant.js DELETED
@@ -1,101 +0,0 @@
1
- import { getSprite, TREE_TYPES } from "./sprites.js";
2
- import { createEmptyForest, readForest, writeForest } from "./state.js";
3
-
4
- const MIN_GAP = 4;
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
- }
12
-
13
- function randomItem(items) {
14
- return items[Math.floor(Math.random() * items.length)];
15
- }
16
-
17
- function randomGrowth() {
18
- return Math.round((0.3 + Math.random() * 0.7) * 100) / 100;
19
- }
20
-
21
- function occupiedRanges(trees) {
22
- return trees.map((tree) => {
23
- const sprite = getSprite(tree.type, tree.growth);
24
- const half = Math.floor(sprite.width / 2);
25
- return [tree.x - half - MIN_GAP, tree.x + half + MIN_GAP];
26
- });
27
- }
28
-
29
- function findOpenX(trees, type, growth, width) {
30
- const sprite = getSprite(type, growth);
31
- const half = Math.floor(sprite.width / 2);
32
- const margin = half + 1;
33
- const ranges = occupiedRanges(trees);
34
-
35
- for (let attempt = 0; attempt < 100; attempt += 1) {
36
- const x =
37
- margin + Math.floor(Math.random() * Math.max(1, width - margin * 2));
38
- const left = x - half;
39
- const right = x + half;
40
- const collides = ranges.some(
41
- ([occupiedLeft, occupiedRight]) =>
42
- left < occupiedRight && right > occupiedLeft,
43
- );
44
- if (!collides) return x;
45
- }
46
-
47
- return margin + Math.floor(Math.random() * Math.max(1, width - margin * 2));
48
- }
49
-
50
- function nudgeGrowth(growth) {
51
- if (growth >= 1) return 1;
52
- const nextGrowth = growth + 0.1 + Math.random() * 0.1;
53
- return Math.min(1, Math.round(nextGrowth * 100) / 100);
54
- }
55
-
56
- function daysBetween(dateA, dateB) {
57
- const a = new Date(dateA + "T00:00:00");
58
- const b = new Date(dateB + "T00:00:00");
59
- return Math.round(Math.abs(b - a) / (24 * 60 * 60 * 1000));
60
- }
61
-
62
- export async function plant() {
63
- const forest = readForest() ?? createEmptyForest();
64
- const width = getPlantWidth(forest);
65
-
66
- // Update streak
67
- const today = new Date().toISOString().slice(0, 10);
68
- if (forest.lastActiveDate) {
69
- const gap = daysBetween(forest.lastActiveDate, today);
70
- if (gap === 0) {
71
- // Same day — streak stays (ensure at least 1)
72
- forest.streak = Math.max(forest.streak || 0, 1);
73
- } else if (gap === 1) {
74
- forest.streak = (forest.streak || 1) + 1;
75
- } else {
76
- forest.streak = 1;
77
- }
78
- } else {
79
- forest.streak = 1;
80
- }
81
- forest.lastActiveDate = today;
82
-
83
- for (const tree of forest.trees) {
84
- tree.growth = nudgeGrowth(tree.growth);
85
- }
86
-
87
- const type = randomItem(TREE_TYPES);
88
- const growth = randomGrowth();
89
- const nextId = forest.trees.reduce((max, tree) => Math.max(max, tree.id), 0) + 1;
90
-
91
- forest.trees.push({
92
- id: nextId,
93
- type,
94
- growth,
95
- x: findOpenX(forest.trees, type, growth, width),
96
- plantedAt: new Date().toISOString(),
97
- });
98
- forest.totalPrompts += 1;
99
-
100
- writeForest(forest);
101
- }