@umang-boss/claudemon 1.0.0 → 1.1.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 (54) hide show
  1. package/bin/claudemon.js +38 -28
  2. package/cli/doctor.ts +26 -18
  3. package/cli/install.ts +8 -8
  4. package/cli/shared.ts +32 -13
  5. package/cli/uninstall.ts +8 -6
  6. package/cli/update.ts +13 -14
  7. package/dist/cli/doctor.js +280 -0
  8. package/dist/cli/index.js +40 -0
  9. package/dist/cli/install.js +190 -0
  10. package/dist/cli/shared.js +72 -0
  11. package/dist/cli/uninstall.js +120 -0
  12. package/dist/cli/update.js +257 -0
  13. package/dist/src/engine/constants.js +98 -0
  14. package/dist/src/engine/encounter-pool.js +48 -0
  15. package/dist/src/engine/encounters.js +242 -0
  16. package/dist/src/engine/evolution-data.js +454 -0
  17. package/dist/src/engine/evolution.js +238 -0
  18. package/dist/src/engine/pokemon-data.js +1751 -0
  19. package/dist/src/engine/reactions.js +834 -0
  20. package/dist/src/engine/starter-pool.js +46 -0
  21. package/dist/src/engine/stats.js +79 -0
  22. package/dist/src/engine/types.js +76 -0
  23. package/dist/src/engine/xp.js +108 -0
  24. package/dist/src/gamification/achievements.js +176 -0
  25. package/dist/src/gamification/legendary-quests.js +233 -0
  26. package/dist/src/gamification/milestones.js +65 -0
  27. package/dist/src/hooks/award-xp.js +109 -0
  28. package/dist/src/hooks/increment-counter.js +20 -0
  29. package/dist/src/server/index.js +67 -0
  30. package/dist/src/server/instructions.js +155 -0
  31. package/dist/src/server/tools/achievements.js +97 -0
  32. package/dist/src/server/tools/catch.js +250 -0
  33. package/dist/src/server/tools/display-helpers.js +27 -0
  34. package/dist/src/server/tools/evolve.js +189 -0
  35. package/dist/src/server/tools/legendary.js +58 -0
  36. package/dist/src/server/tools/party.js +206 -0
  37. package/dist/src/server/tools/pet.js +101 -0
  38. package/dist/src/server/tools/pokedex.js +235 -0
  39. package/dist/src/server/tools/rename.js +49 -0
  40. package/dist/src/server/tools/show.js +111 -0
  41. package/dist/src/server/tools/starter.js +127 -0
  42. package/dist/src/server/tools/stats.js +98 -0
  43. package/dist/src/server/tools/visibility.js +52 -0
  44. package/dist/src/sprites/index.js +43 -0
  45. package/dist/src/state/io.js +78 -0
  46. package/dist/src/state/schemas.js +97 -0
  47. package/dist/src/state/state-manager.js +272 -0
  48. package/hooks/post-tool-use.sh +20 -6
  49. package/package.json +12 -11
  50. package/src/sprites/index.ts +5 -2
  51. package/src/state/io.ts +12 -23
  52. package/src/state/state-manager.ts +12 -3
  53. package/statusline/buddy-status.sh +14 -4
  54. package/{tsconfig.json → tsconfig.build.json} +9 -15
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
  }