@umang-boss/claudemon 1.0.0 → 1.1.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.
Files changed (55) hide show
  1. package/README.md +81 -67
  2. package/bin/claudemon.js +38 -28
  3. package/cli/doctor.ts +26 -18
  4. package/cli/install.ts +8 -8
  5. package/cli/shared.ts +32 -13
  6. package/cli/uninstall.ts +8 -6
  7. package/cli/update.ts +13 -14
  8. package/dist/cli/doctor.js +280 -0
  9. package/dist/cli/index.js +40 -0
  10. package/dist/cli/install.js +190 -0
  11. package/dist/cli/shared.js +72 -0
  12. package/dist/cli/uninstall.js +120 -0
  13. package/dist/cli/update.js +257 -0
  14. package/dist/src/engine/constants.js +98 -0
  15. package/dist/src/engine/encounter-pool.js +48 -0
  16. package/dist/src/engine/encounters.js +242 -0
  17. package/dist/src/engine/evolution-data.js +454 -0
  18. package/dist/src/engine/evolution.js +238 -0
  19. package/dist/src/engine/pokemon-data.js +1751 -0
  20. package/dist/src/engine/reactions.js +834 -0
  21. package/dist/src/engine/starter-pool.js +46 -0
  22. package/dist/src/engine/stats.js +79 -0
  23. package/dist/src/engine/types.js +76 -0
  24. package/dist/src/engine/xp.js +108 -0
  25. package/dist/src/gamification/achievements.js +176 -0
  26. package/dist/src/gamification/legendary-quests.js +233 -0
  27. package/dist/src/gamification/milestones.js +65 -0
  28. package/dist/src/hooks/award-xp.js +109 -0
  29. package/dist/src/hooks/increment-counter.js +20 -0
  30. package/dist/src/server/index.js +67 -0
  31. package/dist/src/server/instructions.js +155 -0
  32. package/dist/src/server/tools/achievements.js +97 -0
  33. package/dist/src/server/tools/catch.js +250 -0
  34. package/dist/src/server/tools/display-helpers.js +27 -0
  35. package/dist/src/server/tools/evolve.js +189 -0
  36. package/dist/src/server/tools/legendary.js +58 -0
  37. package/dist/src/server/tools/party.js +206 -0
  38. package/dist/src/server/tools/pet.js +101 -0
  39. package/dist/src/server/tools/pokedex.js +235 -0
  40. package/dist/src/server/tools/rename.js +49 -0
  41. package/dist/src/server/tools/show.js +111 -0
  42. package/dist/src/server/tools/starter.js +127 -0
  43. package/dist/src/server/tools/stats.js +98 -0
  44. package/dist/src/server/tools/visibility.js +52 -0
  45. package/dist/src/sprites/index.js +43 -0
  46. package/dist/src/state/io.js +78 -0
  47. package/dist/src/state/schemas.js +97 -0
  48. package/dist/src/state/state-manager.js +272 -0
  49. package/hooks/post-tool-use.sh +20 -6
  50. package/package.json +12 -11
  51. package/src/sprites/index.ts +5 -2
  52. package/src/state/io.ts +12 -23
  53. package/src/state/state-manager.ts +12 -3
  54. package/statusline/buddy-status.sh +14 -4
  55. package/{tsconfig.json → tsconfig.build.json} +9 -15
package/README.md CHANGED
@@ -15,41 +15,62 @@ fill your Pokedex -- all while you code.
15
15
  - **Level up & evolve** -- Charmander -> Charmeleon -> Charizard
16
16
  - **Wild encounters** -- Pokemon appear based on your coding activity
17
17
  - **Catch 'em all** -- fill your 151-entry Pokedex
18
- - **Achievements** -- 18 milestones to unlock
18
+ - **Achievements** -- 17 milestones to unlock
19
19
  - **Legendary quests** -- multi-step challenges for Articuno, Zapdos, Moltres, Mewtwo, Mew
20
20
  - **Colored terminal sprites** -- hand-crafted pixel art in your terminal
21
+ - **Status line** -- sprite + name + model + buddy speech on the prompt line
21
22
  - **Type personalities** -- 15 unique reaction styles
23
+ - **Nickname your Pokemon** -- give them custom names
22
24
 
23
- ## Quick Start
25
+ ## Install
24
26
 
25
27
  ```bash
26
- # Install
27
- bun run cli/install.ts
28
-
29
- # Start a new Claude Code session, then:
30
- /buddy # Pick your starter
31
- /buddy show # See your Pokemon
32
- /buddy pet # Bond with your companion
33
- /buddy stats # Detailed stats
34
- /buddy evolve # Check evolution status
35
- /buddy catch # Catch wild Pokemon
36
- /buddy party # Manage your party
37
- /buddy pokedex # Track your collection
38
- /buddy achievements # View progress
39
- /buddy legendary # Legendary quest chains
28
+ npx @umang-boss/claudemon install
40
29
  ```
41
30
 
31
+ That's it! Start a new Claude Code session and type `/buddy`.
32
+
33
+ **Requirements:** Node.js 18+ (Bun optional, auto-detected for faster startup)
34
+
35
+ ### Other CLI Commands
36
+
37
+ ```bash
38
+ npx @umang-boss/claudemon doctor # Check installation health
39
+ npx @umang-boss/claudemon update # Re-register after updates
40
+ npx @umang-boss/claudemon uninstall # Remove (preserves save data)
41
+ ```
42
+
43
+ ## Commands
44
+
45
+ Once installed, use `/buddy` in Claude Code:
46
+
47
+ | Command | What it does |
48
+ |---------|-------------|
49
+ | `/buddy` | Show your Pokemon |
50
+ | `/buddy pet` | Bond with your buddy (+XP, +happiness) |
51
+ | `/buddy stats` | Detailed stat breakdown |
52
+ | `/buddy rename Sparky` | Give a nickname |
53
+ | `/buddy rename` | Reset to species name |
54
+ | `/buddy evolve` | Check/trigger evolution |
55
+ | `/buddy catch` | Catch wild Pokemon |
56
+ | `/buddy catch confirm` | Throw a Pokeball! |
57
+ | `/buddy party` | View party (6 max) |
58
+ | `/buddy switch 2` | Switch active Pokemon |
59
+ | `/buddy pokedex` | Track your 151 collection |
60
+ | `/buddy achievements` | View progress |
61
+ | `/buddy legendary` | Legendary quest chains |
62
+ | `/buddy hide` | Hide sprite from status line |
63
+ | `/buddy unhide` | Show sprite |
64
+
42
65
  ## How It Works
43
66
 
44
67
  Claudemon runs as an MCP (Model Context Protocol) server alongside Claude Code.
45
68
  It uses hooks to detect your coding activity and award XP automatically.
46
69
 
47
- ### Architecture
48
-
49
70
  ```
50
71
  Claude Code -> MCP Server (Claudemon)
51
72
  -> Hooks (PostToolUse, Stop, UserPromptSubmit)
52
- -> Status Line (name + level + XP bar)
73
+ -> Status Line (sprite + name + model + speech)
53
74
  -> /buddy Skill (slash commands)
54
75
  ```
55
76
 
@@ -70,10 +91,10 @@ Claude Code -> MCP Server (Claudemon)
70
91
  ### Evolution
71
92
 
72
93
  Pokemon evolve at the same levels as the original Gen 1 games:
73
- - Level-based: Charmander -> Charmeleon (L16) -> Charizard (L36)
74
- - Badge-based: Pikachu -> Raichu (Spark Badge -- 200 commits)
75
- - Collaboration: Kadabra -> Alakazam (10 PRs merged)
76
- - Stat-based: Eevee -> Flareon/Vaporeon/Jolteon (dominant coding stat)
94
+ - **Level-based:** Charmander -> Charmeleon (L16) -> Charizard (L36)
95
+ - **Badge-based:** Pikachu -> Raichu (Spark Badge -- 200 commits)
96
+ - **Collaboration:** Kadabra -> Alakazam (10 PRs merged)
97
+ - **Stat-based:** Eevee -> Flareon/Vaporeon/Jolteon (dominant coding stat)
77
98
 
78
99
  ### Badges
79
100
 
@@ -85,73 +106,66 @@ Pokemon evolve at the same levels as the original Gen 1 games:
85
106
  | Lunar Badge | 30-day streak | Moon Stone evolutions |
86
107
  | Growth Badge | Edit 500 files | Leaf Stone evolutions |
87
108
 
88
- ### Trainer Titles
109
+ ### Wild Encounters
89
110
 
90
- Your title progresses as your Pokemon levels up:
111
+ As you code, wild Pokemon appear based on your activity:
112
+ - Fixing bugs -> Bug/Poison types
113
+ - Writing tests -> Fighting/Normal types
114
+ - Large refactors -> Psychic/Dragon types
115
+ - Build/compile -> Fire/Rock types
91
116
 
92
- | Level | Title |
93
- |-------|-------|
94
- | 1 | Bug Catcher |
95
- | 6 | Youngster |
96
- | 11 | Hiker |
97
- | 21 | Ace Trainer |
98
- | 31 | Cooltrainer |
99
- | 41 | Veteran |
100
- | 51 | Elite Four |
101
- | 61 | Champion |
102
- | 76 | Pokemon Master |
103
- | 91 | Professor |
117
+ Encounters trigger roughly every 500 XP earned. Use `/buddy catch` to try catching them!
104
118
 
105
- ## CLI Tools
119
+ ### Legendary Quests
106
120
 
107
- ```bash
108
- # Install Claudemon into Claude Code
109
- bun run cli/install.ts
121
+ 5 multi-step quest chains for legendary Pokemon:
122
+ - **Articuno** -- The Ice Bird of Endurance (100-day streak)
123
+ - **Zapdos** -- The Thunder of Testing (1000 tests passed)
124
+ - **Moltres** -- The Flame of Debugging (500 bugs fixed)
125
+ - **Mewtwo** -- The Ultimate Creation (140 Pokedex entries)
126
+ - **Mew** -- The Myth (365-day coding streak)
127
+
128
+ ## Status Line
110
129
 
111
- # Uninstall (preserves your save data)
112
- bun run cli/uninstall.ts
130
+ The status line shows your Pokemon sprite on the right side of the input prompt:
113
131
 
114
- # Diagnose installation issues
115
- bun run cli/doctor.ts
116
132
  ```
133
+ *Pikachu hums softly* Pikachu Lv.10
134
+ Opus 4.6 [colored sprite]
135
+ [colored sprite]
136
+ [colored sprite]
137
+ ```
138
+
139
+ - Buddy speech rotates every 30 seconds (zero API cost -- hardcoded messages)
140
+ - Model name from Claude Code
141
+ - `/buddy hide` to toggle sprite visibility
117
142
 
118
143
  ## Development
119
144
 
120
145
  ```bash
121
- # Run the MCP server directly
122
- bun run server
146
+ # Clone and install
147
+ git clone https://github.com/umang-dabhi/claudemon.git
148
+ cd claudemon
149
+ bun install
123
150
 
124
- # Run tests
151
+ # Run tests (309 tests)
125
152
  bun test
126
153
 
127
154
  # Type checking
128
155
  bun run typecheck
129
156
 
130
- # Format code
157
+ # Format
131
158
  bun run format
132
- ```
133
-
134
- ### Project Structure
135
159
 
136
- ```
137
- src/
138
- engine/ # Game logic: XP, evolution, encounters, stats
139
- gamification/ # Achievements, milestones, legendary quests
140
- server/ # MCP server and tool handlers
141
- sprites/ # Terminal sprite rendering
142
- state/ # Save state management
143
- cli/ # Install, uninstall, doctor scripts
144
- hooks/ # PostToolUse, Stop, UserPromptSubmit shell scripts
145
- skills/buddy/ # /buddy slash command definition
146
- sprites/full/ # 151 colored terminal sprite files
147
- statusline/ # Status bar shell script
148
- tests/ # Test suites
160
+ # Build for npm (compiles TS to JS)
161
+ npm run build
149
162
  ```
150
163
 
151
164
  ## Requirements
152
165
 
153
- - [Claude Code](https://claude.ai/code) v2.1.80+
154
- - [Bun](https://bun.sh) v1.0+
166
+ - [Claude Code](https://claude.ai/code)
167
+ - Node.js 18+ (or [Bun](https://bun.sh) for faster startup)
168
+ - `jq` for status line (`sudo apt install jq` on Ubuntu)
155
169
 
156
170
  ## Disclaimer
157
171
 
package/bin/claudemon.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Claudemon CLI wrapper detects Bun and delegates to it.
4
- * Works with `npx claudemon` even on systems with only Node.js.
3
+ * Claudemon CLI — works with Node.js (no Bun required for users).
4
+ * Routes to install/uninstall/update/doctor.
5
5
  */
6
6
 
7
7
  import { spawnSync } from "node:child_process";
@@ -10,43 +10,53 @@ import { resolve, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
 
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const cliEntry = resolve(__dirname, "..", "cli", "index.ts");
14
13
  const args = process.argv.slice(2);
15
14
 
16
- // Find bun binary
17
- function findBun() {
18
- const home = process.env.HOME || "";
19
- const candidates = [
20
- `${home}/.bun/bin/bun`,
21
- "/usr/local/bin/bun",
22
- "/usr/bin/bun",
23
- ];
24
- for (const p of candidates) {
25
- if (existsSync(p)) return p;
26
- }
27
- // Try PATH
28
- const result = spawnSync("which", ["bun"], { stdio: "pipe" });
29
- if (result.status === 0) return "bun";
30
- return null;
31
- }
32
-
33
- const bunPath = findBun();
15
+ // Try compiled JS first (dist/), then fall back to TS with bun
16
+ const distCli = resolve(__dirname, "..", "dist", "cli", "index.js");
17
+ const srcCli = resolve(__dirname, "..", "cli", "index.ts");
34
18
 
35
- if (bunPath) {
36
- const result = spawnSync(bunPath, ["run", cliEntry, ...args], {
19
+ if (existsSync(distCli)) {
20
+ // Run compiled JS with node
21
+ const result = spawnSync("node", [distCli, ...args], {
37
22
  stdio: "inherit",
38
23
  env: process.env,
39
24
  });
40
25
  process.exit(result.status ?? 1);
41
26
  } else {
27
+ // Fallback: try bun with source TS
28
+ const home = process.env.HOME || "";
29
+ const bunPaths = [`${home}/.bun/bin/bun`, "/usr/local/bin/bun", "/usr/bin/bun"];
30
+ let bunPath = null;
31
+
32
+ for (const p of bunPaths) {
33
+ if (existsSync(p)) {
34
+ bunPath = p;
35
+ break;
36
+ }
37
+ }
38
+
39
+ if (!bunPath) {
40
+ const which = spawnSync("which", ["bun"], { stdio: "pipe" });
41
+ if (which.status === 0) bunPath = "bun";
42
+ }
43
+
44
+ if (bunPath && existsSync(srcCli)) {
45
+ const result = spawnSync(bunPath, ["run", srcCli, ...args], {
46
+ stdio: "inherit",
47
+ env: process.env,
48
+ });
49
+ process.exit(result.status ?? 1);
50
+ }
51
+
42
52
  console.error(`
43
- Claudemon requires Bun (https://bun.sh) to run.
53
+ Error: Could not find compiled files (dist/) or Bun runtime.
44
54
 
45
- Install Bun:
46
- curl -fsSL https://bun.sh/install | bash
55
+ If you installed via npm, try reinstalling:
56
+ npm install -g @umang-boss/claudemon
47
57
 
48
- Then run:
49
- npx claudemon install
58
+ Or install Bun and run from source:
59
+ curl -fsSL https://bun.sh/install | bash
50
60
  `);
51
61
  process.exit(1);
52
62
  }
package/cli/doctor.ts CHANGED
@@ -5,9 +5,11 @@
5
5
  * Usage: bun run cli/doctor.ts
6
6
  */
7
7
 
8
- import { access, stat, unlink, readdir } from "node:fs/promises";
8
+ import { access, stat, unlink, readdir, readFile } from "node:fs/promises";
9
9
  import { constants as fsConstants } from "node:fs";
10
10
  import { resolve, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { spawnSync } from "node:child_process";
11
13
  import { StateManager } from "../src/state/state-manager.js";
12
14
  import { PlayerStateSchema } from "../src/state/schemas.js";
13
15
 
@@ -28,7 +30,9 @@ const STATE_FILE = `${STATE_DIR}/state.json`;
28
30
  const LOCK_FILE = `${STATE_DIR}/state.lock`;
29
31
  const LOCK_MAX_AGE_MS = 5000;
30
32
  const EXPECTED_SPRITE_COUNT = 151;
31
- const COLORSCRIPT_DIR = resolve(dirname(import.meta.dir), "sprites/colorscripts/small");
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = dirname(__filename);
35
+ const COLORSCRIPT_DIR = resolve(dirname(__dirname), "sprites/colorscripts/small");
32
36
 
33
37
  // ── Helpers ──────────────────────────────────────────────────
34
38
 
@@ -47,10 +51,10 @@ function formatCheck(result: CheckResult): string {
47
51
 
48
52
  async function checkBun(): Promise<CheckResult> {
49
53
  try {
50
- const proc = Bun.spawn(["bun", "--version"], { stdout: "pipe", stderr: "pipe" });
51
- const output = await new Response(proc.stdout).text();
52
- await proc.exited;
53
- return { label: "Bun runtime", passed: true, detail: `v${output.trim()}` };
54
+ const result = spawnSync("bun", ["--version"], { stdio: "pipe" });
55
+ if (result.error) throw result.error;
56
+ const output = result.stdout?.toString().trim();
57
+ return { label: "Bun runtime", passed: true, detail: `v${output}` };
54
58
  } catch {
55
59
  return { label: "Bun runtime", passed: false, detail: "not found" };
56
60
  }
@@ -74,13 +78,14 @@ async function checkStateDir(): Promise<CheckResult> {
74
78
  // ── Check 3: State File ──────────────────────────────────────
75
79
 
76
80
  async function checkStateFile(): Promise<CheckResult> {
77
- const file = Bun.file(STATE_FILE);
78
- if (!(await file.exists())) {
81
+ try {
82
+ await access(STATE_FILE, fsConstants.F_OK);
83
+ } catch {
79
84
  return { label: "State file", passed: false, detail: "not found (run /buddy starter first)" };
80
85
  }
81
86
 
82
87
  try {
83
- const text = await file.text();
88
+ const text = await readFile(STATE_FILE, "utf-8");
84
89
  const data = JSON.parse(text) as Record<string, unknown>;
85
90
 
86
91
  // Check for active pokemon
@@ -150,11 +155,12 @@ async function checkHooks(): Promise<CheckResult> {
150
155
  // ── Check 6: Skill ───────────────────────────────────────────
151
156
 
152
157
  async function checkSkill(): Promise<CheckResult> {
153
- const file = Bun.file(SKILL_DEST);
154
- if (await file.exists()) {
158
+ try {
159
+ await access(SKILL_DEST, fsConstants.F_OK);
155
160
  return { label: "Skill", passed: true, detail: "/buddy command installed" };
161
+ } catch {
162
+ return { label: "Skill", passed: false, detail: "~/.claude/skills/buddy/SKILL.md not found" };
156
163
  }
157
- return { label: "Skill", passed: false, detail: "~/.claude/skills/buddy/SKILL.md not found" };
158
164
  }
159
165
 
160
166
  // ── Check 7: Hook Script Executable ──────────────────────────
@@ -164,15 +170,16 @@ async function checkHookScript(): Promise<CheckResult> {
164
170
  await access(HOOK_SCRIPT, fsConstants.X_OK);
165
171
  return { label: "Hook script", passed: true, detail: "executable" };
166
172
  } catch {
167
- const file = Bun.file(HOOK_SCRIPT);
168
- if (await file.exists()) {
173
+ try {
174
+ await access(HOOK_SCRIPT, fsConstants.F_OK);
169
175
  return {
170
176
  label: "Hook script",
171
177
  passed: false,
172
178
  detail: "exists but not executable (run chmod +x)",
173
179
  };
180
+ } catch {
181
+ return { label: "Hook script", passed: false, detail: "not found" };
174
182
  }
175
- return { label: "Hook script", passed: false, detail: "not found" };
176
183
  }
177
184
  }
178
185
 
@@ -255,13 +262,14 @@ async function checkSpriteCount(): Promise<CheckResult> {
255
262
  // ── Check 11: State File Validity (Zod) ─────────────────────
256
263
 
257
264
  async function checkStateValidity(): Promise<CheckResult> {
258
- const file = Bun.file(STATE_FILE);
259
- if (!(await file.exists())) {
265
+ try {
266
+ await access(STATE_FILE, fsConstants.F_OK);
267
+ } catch {
260
268
  return { label: "State validity", passed: true, detail: "no state file yet" };
261
269
  }
262
270
 
263
271
  try {
264
- const text = await file.text();
272
+ const text = await readFile(STATE_FILE, "utf-8");
265
273
  if (!text.trim()) {
266
274
  return { label: "State validity", passed: false, detail: "state file is empty" };
267
275
  }
package/cli/install.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { mkdir, copyFile, chmod, access } from "node:fs/promises";
9
9
  import { constants as fsConstants } from "node:fs";
10
+ import { spawnSync } from "node:child_process";
10
11
 
11
12
  import type { ClaudeConfig, ClaudeSettings, HookCommand, HookMatcher } from "./shared.js";
12
13
  import {
@@ -25,8 +26,7 @@ import {
25
26
  STOP_HOOK_SCRIPT,
26
27
  USER_PROMPT_HOOK_SCRIPT,
27
28
  STATUSLINE_SCRIPT,
28
- SERVER_ENTRY,
29
- getBunPath,
29
+ getRuntime,
30
30
  } from "./shared.js";
31
31
 
32
32
  // ── Step 1: Check Prerequisites ──────────────────────────────
@@ -36,10 +36,10 @@ async function checkPrerequisites(): Promise<boolean> {
36
36
 
37
37
  // Check bun (sanity)
38
38
  try {
39
- const proc = Bun.spawn(["bun", "--version"], { stdout: "pipe", stderr: "pipe" });
40
- const output = await new Response(proc.stdout).text();
41
- await proc.exited;
42
- ok(`Bun runtime: v${output.trim()}`);
39
+ const result = spawnSync("bun", ["--version"], { stdio: "pipe" });
40
+ if (result.error) throw result.error;
41
+ const output = result.stdout?.toString().trim();
42
+ ok(`Bun runtime: v${output}`);
43
43
  } catch {
44
44
  fail("Bun runtime not found. Install from https://bun.sh");
45
45
  allGood = false;
@@ -76,8 +76,8 @@ async function registerMcpServer(): Promise<void> {
76
76
  }
77
77
 
78
78
  config.mcpServers["claudemon"] = {
79
- command: getBunPath(),
80
- args: ["run", SERVER_ENTRY],
79
+ command: getRuntime().command,
80
+ args: [getRuntime().serverEntry],
81
81
  env: {},
82
82
  };
83
83
 
package/cli/shared.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { resolve, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
 
8
9
  // ── Config shape interfaces ──────────────────────────────────
9
10
 
@@ -50,7 +51,9 @@ if (!HOME) {
50
51
  }
51
52
 
52
53
  export const CLI_HOME = HOME;
53
- export const PROJECT_DIR = resolve(dirname(import.meta.dir));
54
+ const __filename = fileURLToPath(import.meta.url);
55
+ const __dirname = dirname(__filename);
56
+ export const PROJECT_DIR = resolve(dirname(__dirname));
54
57
  export const CLAUDE_DIR = `${HOME}/.claude`;
55
58
  export const CLAUDE_CONFIG = `${HOME}/.claude.json`;
56
59
  export const CLAUDE_SETTINGS = `${CLAUDE_DIR}/settings.json`;
@@ -62,16 +65,28 @@ export const HOOK_SCRIPT = `${PROJECT_DIR}/hooks/post-tool-use.sh`;
62
65
  export const STOP_HOOK_SCRIPT = `${PROJECT_DIR}/hooks/stop.sh`;
63
66
  export const USER_PROMPT_HOOK_SCRIPT = `${PROJECT_DIR}/hooks/user-prompt-submit.sh`;
64
67
  export const STATUSLINE_SCRIPT = `${PROJECT_DIR}/statusline/buddy-status.sh`;
65
- export const SERVER_ENTRY = `${PROJECT_DIR}/src/server/index.ts`;
68
+ export const SERVER_ENTRY_TS = `${PROJECT_DIR}/src/server/index.ts`;
69
+ export const SERVER_ENTRY_JS = `${PROJECT_DIR}/dist/src/server/index.js`;
66
70
 
67
- /** Resolve full path to bun binary (Claude Code may not have bun in PATH) */
68
- export function getBunPath(): string {
71
+ /** Find the best runtime and server entry — prefers bun (fast), falls back to node (compiled JS) */
72
+ export function getRuntime(): { command: string; serverEntry: string } {
69
73
  const { existsSync } = require("node:fs") as { existsSync: (p: string) => boolean };
70
- const candidates = [`${HOME}/.bun/bin/bun`, "/usr/local/bin/bun", "/usr/bin/bun"];
71
- for (const p of candidates) {
72
- if (existsSync(p)) return p;
74
+
75
+ // Check for bun (can run .ts directly)
76
+ const bunCandidates = [`${HOME}/.bun/bin/bun`, "/usr/local/bin/bun", "/usr/bin/bun"];
77
+ for (const p of bunCandidates) {
78
+ if (existsSync(p)) {
79
+ return { command: p, serverEntry: SERVER_ENTRY_TS };
80
+ }
81
+ }
82
+
83
+ // Fall back to node (needs compiled .js from dist/)
84
+ if (existsSync(SERVER_ENTRY_JS)) {
85
+ return { command: "node", serverEntry: SERVER_ENTRY_JS };
73
86
  }
74
- return "bun";
87
+
88
+ // Last resort: assume bun in PATH
89
+ return { command: "bun", serverEntry: SERVER_ENTRY_TS };
75
90
  }
76
91
 
77
92
  // ── Helpers ──────────────────────────────────────────────────
@@ -89,14 +104,18 @@ export function info(msg: string): void {
89
104
  }
90
105
 
91
106
  export async function readJson<T>(path: string): Promise<T | null> {
92
- const file = Bun.file(path);
93
- if (!(await file.exists())) {
107
+ const { readFile, access } = await import("node:fs/promises");
108
+ const { constants } = await import("node:fs");
109
+ try {
110
+ await access(path, constants.F_OK);
111
+ const text = await readFile(path, "utf-8");
112
+ return JSON.parse(text) as T;
113
+ } catch {
94
114
  return null;
95
115
  }
96
- const text = await file.text();
97
- return JSON.parse(text) as T;
98
116
  }
99
117
 
100
118
  export async function writeJson(path: string, data: unknown): Promise<void> {
101
- await Bun.write(path, JSON.stringify(data, null, 2) + "\n");
119
+ const { writeFile } = await import("node:fs/promises");
120
+ await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
102
121
  }
package/cli/uninstall.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * Usage: bun run cli/uninstall.ts
6
6
  */
7
7
 
8
- import { rm } from "node:fs/promises";
8
+ import { rm, access } from "node:fs/promises";
9
+ import { constants as fsConstants } from "node:fs";
9
10
 
10
11
  import type { ClaudeConfig, ClaudeSettings } from "./shared.js";
11
12
  import {
@@ -106,8 +107,9 @@ async function removeHooks(): Promise<void> {
106
107
  // ── Step 3: Remove Skill ─────────────────────────────────────
107
108
 
108
109
  async function removeSkill(): Promise<void> {
109
- const file = Bun.file(`${SKILL_DEST_DIR}/SKILL.md`);
110
- if (!(await file.exists())) {
110
+ try {
111
+ await access(`${SKILL_DEST_DIR}/SKILL.md`, fsConstants.F_OK);
112
+ } catch {
111
113
  info("Skill: ~/.claude/skills/buddy/ not found (nothing to remove)");
112
114
  return;
113
115
  }
@@ -119,10 +121,10 @@ async function removeSkill(): Promise<void> {
119
121
  // ── Step 4: Preserve State Data ──────────────────────────────
120
122
 
121
123
  async function preserveStateData(): Promise<void> {
122
- const stateFile = Bun.file(`${STATE_DIR}/state.json`);
123
- if (await stateFile.exists()) {
124
+ try {
125
+ await access(`${STATE_DIR}/state.json`, fsConstants.F_OK);
124
126
  info(`Your Pokemon data is preserved at ${STATE_DIR}/. Delete manually to remove.`);
125
- } else {
127
+ } catch {
126
128
  info(`State directory: ${STATE_DIR}/ (kept, no state file found)`);
127
129
  }
128
130
  }
package/cli/update.ts CHANGED
@@ -10,9 +10,9 @@
10
10
  */
11
11
 
12
12
  import { mkdir, copyFile, chmod, access } from "node:fs/promises";
13
- import { constants as fsConstants } from "node:fs";
13
+ import { constants as fsConstants, existsSync, readdirSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import { existsSync } from "node:fs";
15
+ import { spawnSync } from "node:child_process";
16
16
 
17
17
  import type { ClaudeConfig, ClaudeSettings, HookCommand, HookMatcher } from "./shared.js";
18
18
  import {
@@ -32,9 +32,8 @@ import {
32
32
  STOP_HOOK_SCRIPT,
33
33
  USER_PROMPT_HOOK_SCRIPT,
34
34
  STATUSLINE_SCRIPT,
35
- SERVER_ENTRY,
36
35
  PROJECT_DIR,
37
- getBunPath,
36
+ getRuntime,
38
37
  } from "./shared.js";
39
38
 
40
39
  // ── Step 1: Check Prerequisites ──────────────────────────────
@@ -43,10 +42,10 @@ async function checkPrerequisites(): Promise<boolean> {
43
42
  let allGood = true;
44
43
 
45
44
  try {
46
- const proc = Bun.spawn(["bun", "--version"], { stdout: "pipe", stderr: "pipe" });
47
- const output = await new Response(proc.stdout).text();
48
- await proc.exited;
49
- ok(`Bun runtime: v${output.trim()}`);
45
+ const result = spawnSync("bun", ["--version"], { stdio: "pipe" });
46
+ if (result.error) throw result.error;
47
+ const output = result.stdout?.toString().trim();
48
+ ok(`Bun runtime: v${output}`);
50
49
  } catch {
51
50
  fail("Bun runtime not found.");
52
51
  allGood = false;
@@ -122,8 +121,8 @@ async function registerMcpServer(): Promise<void> {
122
121
  }
123
122
 
124
123
  config.mcpServers["claudemon"] = {
125
- command: getBunPath(),
126
- args: ["run", SERVER_ENTRY],
124
+ command: getRuntime().command,
125
+ args: [getRuntime().serverEntry],
127
126
  env: {},
128
127
  };
129
128
 
@@ -240,7 +239,7 @@ async function checkColorscripts(): Promise<{ missing: number; total: number }>
240
239
  }
241
240
 
242
241
  // Check for files matching the pattern
243
- const files = await Array.fromAsync(new Bun.Glob(`${prefix}*.txt`).scan(smallDir));
242
+ const files = readdirSync(smallDir).filter((f) => f.startsWith(prefix) && f.endsWith(".txt"));
244
243
  if (files.length === 0) {
245
244
  missing++;
246
245
  }
@@ -252,10 +251,10 @@ async function checkColorscripts(): Promise<{ missing: number; total: number }>
252
251
  // ── Step 5: State Preservation Check ─────────────────────────
253
252
 
254
253
  async function checkStatePreserved(): Promise<void> {
255
- const stateFile = Bun.file(`${STATE_DIR}/state.json`);
256
- if (await stateFile.exists()) {
254
+ try {
255
+ await access(`${STATE_DIR}/state.json`, fsConstants.F_OK);
257
256
  ok("State data: preserved (not modified)");
258
- } else {
257
+ } catch {
259
258
  info("State data: no save file found (run /buddy starter to begin)");
260
259
  }
261
260
  }