@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.
- package/bin/claudemon.js +38 -28
- package/cli/doctor.ts +26 -18
- package/cli/install.ts +8 -8
- package/cli/shared.ts +32 -13
- package/cli/uninstall.ts +8 -6
- package/cli/update.ts +13 -14
- package/dist/cli/doctor.js +280 -0
- package/dist/cli/index.js +40 -0
- package/dist/cli/install.js +190 -0
- package/dist/cli/shared.js +72 -0
- package/dist/cli/uninstall.js +120 -0
- package/dist/cli/update.js +257 -0
- package/dist/src/engine/constants.js +98 -0
- package/dist/src/engine/encounter-pool.js +48 -0
- package/dist/src/engine/encounters.js +242 -0
- package/dist/src/engine/evolution-data.js +454 -0
- package/dist/src/engine/evolution.js +238 -0
- package/dist/src/engine/pokemon-data.js +1751 -0
- package/dist/src/engine/reactions.js +834 -0
- package/dist/src/engine/starter-pool.js +46 -0
- package/dist/src/engine/stats.js +79 -0
- package/dist/src/engine/types.js +76 -0
- package/dist/src/engine/xp.js +108 -0
- package/dist/src/gamification/achievements.js +176 -0
- package/dist/src/gamification/legendary-quests.js +233 -0
- package/dist/src/gamification/milestones.js +65 -0
- package/dist/src/hooks/award-xp.js +109 -0
- package/dist/src/hooks/increment-counter.js +20 -0
- package/dist/src/server/index.js +67 -0
- package/dist/src/server/instructions.js +155 -0
- package/dist/src/server/tools/achievements.js +97 -0
- package/dist/src/server/tools/catch.js +250 -0
- package/dist/src/server/tools/display-helpers.js +27 -0
- package/dist/src/server/tools/evolve.js +189 -0
- package/dist/src/server/tools/legendary.js +58 -0
- package/dist/src/server/tools/party.js +206 -0
- package/dist/src/server/tools/pet.js +101 -0
- package/dist/src/server/tools/pokedex.js +235 -0
- package/dist/src/server/tools/rename.js +49 -0
- package/dist/src/server/tools/show.js +111 -0
- package/dist/src/server/tools/starter.js +127 -0
- package/dist/src/server/tools/stats.js +98 -0
- package/dist/src/server/tools/visibility.js +52 -0
- package/dist/src/sprites/index.js +43 -0
- package/dist/src/state/io.js +78 -0
- package/dist/src/state/schemas.js +97 -0
- package/dist/src/state/state-manager.js +272 -0
- package/hooks/post-tool-use.sh +20 -6
- package/package.json +12 -11
- package/src/sprites/index.ts +5 -2
- package/src/state/io.ts +12 -23
- package/src/state/state-manager.ts +12 -3
- package/statusline/buddy-status.sh +14 -4
- package/{tsconfig.json → tsconfig.build.json} +9 -15
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claudemon Doctor — diagnostic tool.
|
|
3
|
+
* Checks the health of the Claudemon installation.
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun run cli/doctor.ts
|
|
6
|
+
*/
|
|
7
|
+
import { access, stat, unlink, readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { constants as fsConstants } from "node:fs";
|
|
9
|
+
import { resolve, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { StateManager } from "../src/state/state-manager.js";
|
|
13
|
+
import { PlayerStateSchema } from "../src/state/schemas.js";
|
|
14
|
+
import { readJson, info, CLAUDE_CONFIG, CLAUDE_SETTINGS, STATE_DIR, SKILL_DEST, HOOK_SCRIPT, } from "./shared.js";
|
|
15
|
+
// ── Local Constants ─────────────────────────────────────────
|
|
16
|
+
const STATE_FILE = `${STATE_DIR}/state.json`;
|
|
17
|
+
const LOCK_FILE = `${STATE_DIR}/state.lock`;
|
|
18
|
+
const LOCK_MAX_AGE_MS = 5000;
|
|
19
|
+
const EXPECTED_SPRITE_COUNT = 151;
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
const COLORSCRIPT_DIR = resolve(dirname(__dirname), "sprites/colorscripts/small");
|
|
23
|
+
function formatCheck(result) {
|
|
24
|
+
const icon = result.passed ? "\u2713" : "\u2717";
|
|
25
|
+
return `[${icon}] ${result.label}: ${result.detail}`;
|
|
26
|
+
}
|
|
27
|
+
// ── Check 1: Bun Version ─────────────────────────────────────
|
|
28
|
+
async function checkBun() {
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync("bun", ["--version"], { stdio: "pipe" });
|
|
31
|
+
if (result.error)
|
|
32
|
+
throw result.error;
|
|
33
|
+
const output = result.stdout?.toString().trim();
|
|
34
|
+
return { label: "Bun runtime", passed: true, detail: `v${output}` };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { label: "Bun runtime", passed: false, detail: "not found" };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ── Check 2: State Directory ─────────────────────────────────
|
|
41
|
+
async function checkStateDir() {
|
|
42
|
+
try {
|
|
43
|
+
await access(STATE_DIR, fsConstants.F_OK);
|
|
44
|
+
return { label: "State directory", passed: true, detail: "~/.claudemon/" };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {
|
|
48
|
+
label: "State directory",
|
|
49
|
+
passed: false,
|
|
50
|
+
detail: "~/.claudemon/ not found (run installer)",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ── Check 3: State File ──────────────────────────────────────
|
|
55
|
+
async function checkStateFile() {
|
|
56
|
+
try {
|
|
57
|
+
await access(STATE_FILE, fsConstants.F_OK);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { label: "State file", passed: false, detail: "not found (run /buddy starter first)" };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const text = await readFile(STATE_FILE, "utf-8");
|
|
64
|
+
const data = JSON.parse(text);
|
|
65
|
+
// Check for active pokemon
|
|
66
|
+
const party = data["party"];
|
|
67
|
+
if (Array.isArray(party) && party.length > 0) {
|
|
68
|
+
const active = party.find((p) => typeof p === "object" &&
|
|
69
|
+
p !== null &&
|
|
70
|
+
"isActive" in p &&
|
|
71
|
+
p["isActive"] === true);
|
|
72
|
+
if (active && typeof active === "object" && "pokemonId" in active) {
|
|
73
|
+
return {
|
|
74
|
+
label: "State file",
|
|
75
|
+
passed: true,
|
|
76
|
+
detail: `valid, active Pokemon #${active["pokemonId"]}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return { label: "State file", passed: true, detail: "valid, but no active Pokemon set" };
|
|
80
|
+
}
|
|
81
|
+
return { label: "State file", passed: true, detail: "valid JSON, no party members yet" };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { label: "State file", passed: false, detail: "exists but invalid JSON" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── Check 4: MCP Server Registration ─────────────────────────
|
|
88
|
+
async function checkMcpServer() {
|
|
89
|
+
const config = await readJson(CLAUDE_CONFIG);
|
|
90
|
+
if (!config) {
|
|
91
|
+
return { label: "MCP server", passed: false, detail: "~/.claude.json not found" };
|
|
92
|
+
}
|
|
93
|
+
if (config.mcpServers && config.mcpServers["claudemon"]) {
|
|
94
|
+
return { label: "MCP server", passed: true, detail: "registered in ~/.claude.json" };
|
|
95
|
+
}
|
|
96
|
+
return { label: "MCP server", passed: false, detail: "not registered in ~/.claude.json" };
|
|
97
|
+
}
|
|
98
|
+
// ── Check 5: Hooks ───────────────────────────────────────────
|
|
99
|
+
async function checkHooks() {
|
|
100
|
+
const settings = await readJson(CLAUDE_SETTINGS);
|
|
101
|
+
if (!settings) {
|
|
102
|
+
return { label: "Hooks", passed: false, detail: "~/.claude/settings.json not found" };
|
|
103
|
+
}
|
|
104
|
+
if (!settings.hooks || !settings.hooks["PostToolUse"]) {
|
|
105
|
+
return { label: "Hooks", passed: false, detail: "no PostToolUse hooks configured" };
|
|
106
|
+
}
|
|
107
|
+
const hasOurHook = settings.hooks["PostToolUse"].some((m) => m.hooks.some((h) => h.command.includes("post-tool-use.sh")));
|
|
108
|
+
if (hasOurHook) {
|
|
109
|
+
return { label: "Hooks", passed: true, detail: "PostToolUse configured" };
|
|
110
|
+
}
|
|
111
|
+
return { label: "Hooks", passed: false, detail: "PostToolUse exists but missing Claudemon hook" };
|
|
112
|
+
}
|
|
113
|
+
// ── Check 6: Skill ───────────────────────────────────────────
|
|
114
|
+
async function checkSkill() {
|
|
115
|
+
try {
|
|
116
|
+
await access(SKILL_DEST, fsConstants.F_OK);
|
|
117
|
+
return { label: "Skill", passed: true, detail: "/buddy command installed" };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { label: "Skill", passed: false, detail: "~/.claude/skills/buddy/SKILL.md not found" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── Check 7: Hook Script Executable ──────────────────────────
|
|
124
|
+
async function checkHookScript() {
|
|
125
|
+
try {
|
|
126
|
+
await access(HOOK_SCRIPT, fsConstants.X_OK);
|
|
127
|
+
return { label: "Hook script", passed: true, detail: "executable" };
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
try {
|
|
131
|
+
await access(HOOK_SCRIPT, fsConstants.F_OK);
|
|
132
|
+
return {
|
|
133
|
+
label: "Hook script",
|
|
134
|
+
passed: false,
|
|
135
|
+
detail: "exists but not executable (run chmod +x)",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { label: "Hook script", passed: false, detail: "not found" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Check 8: State Manager Load ──────────────────────────────
|
|
144
|
+
async function checkStateManager() {
|
|
145
|
+
try {
|
|
146
|
+
StateManager.resetInstance();
|
|
147
|
+
const manager = StateManager.getInstance();
|
|
148
|
+
const state = await manager.load();
|
|
149
|
+
StateManager.resetInstance();
|
|
150
|
+
if (state) {
|
|
151
|
+
return { label: "State manager", passed: true, detail: "loaded successfully" };
|
|
152
|
+
}
|
|
153
|
+
return { label: "State manager", passed: true, detail: "no state file yet (first run)" };
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
157
|
+
StateManager.resetInstance();
|
|
158
|
+
return { label: "State manager", passed: false, detail: `load failed: ${message}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ── Check 9: Stale Lock File ────────────────────────────────
|
|
162
|
+
async function checkStaleLock() {
|
|
163
|
+
try {
|
|
164
|
+
const lockStat = await stat(LOCK_FILE);
|
|
165
|
+
const lockAge = Date.now() - lockStat.mtimeMs;
|
|
166
|
+
if (lockAge > LOCK_MAX_AGE_MS) {
|
|
167
|
+
// Offer cleanup
|
|
168
|
+
info(`Stale lock file found (${Math.round(lockAge / 1000)}s old). Cleaning up...`);
|
|
169
|
+
try {
|
|
170
|
+
await unlink(LOCK_FILE);
|
|
171
|
+
return { label: "Lock file", passed: true, detail: "stale lock cleaned up" };
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { label: "Lock file", passed: false, detail: "stale lock found, cleanup failed" };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
label: "Lock file",
|
|
179
|
+
passed: true,
|
|
180
|
+
detail: `active (${Math.round(lockAge / 1000)}s old)`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// No lock file — this is the normal case
|
|
185
|
+
return { label: "Lock file", passed: true, detail: "no lock (clean)" };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ── Check 10: Sprite Count ──────────────────────────────────
|
|
189
|
+
async function checkSpriteCount() {
|
|
190
|
+
try {
|
|
191
|
+
await access(COLORSCRIPT_DIR, fsConstants.F_OK);
|
|
192
|
+
const entries = await readdir(COLORSCRIPT_DIR);
|
|
193
|
+
const spriteFiles = entries.filter((f) => f.endsWith(".txt"));
|
|
194
|
+
const count = spriteFiles.length;
|
|
195
|
+
if (count >= EXPECTED_SPRITE_COUNT) {
|
|
196
|
+
return {
|
|
197
|
+
label: "Sprites",
|
|
198
|
+
passed: true,
|
|
199
|
+
detail: `${count}/${EXPECTED_SPRITE_COUNT} sprite files`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
label: "Sprites",
|
|
204
|
+
passed: false,
|
|
205
|
+
detail: `only ${count}/${EXPECTED_SPRITE_COUNT} sprite files (some sprites missing)`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { label: "Sprites", passed: false, detail: "colorscripts directory not found" };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ── Check 11: State File Validity (Zod) ─────────────────────
|
|
213
|
+
async function checkStateValidity() {
|
|
214
|
+
try {
|
|
215
|
+
await access(STATE_FILE, fsConstants.F_OK);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return { label: "State validity", passed: true, detail: "no state file yet" };
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const text = await readFile(STATE_FILE, "utf-8");
|
|
222
|
+
if (!text.trim()) {
|
|
223
|
+
return { label: "State validity", passed: false, detail: "state file is empty" };
|
|
224
|
+
}
|
|
225
|
+
const data = JSON.parse(text);
|
|
226
|
+
const result = PlayerStateSchema.safeParse(data);
|
|
227
|
+
if (result.success) {
|
|
228
|
+
return { label: "State validity", passed: true, detail: "schema valid" };
|
|
229
|
+
}
|
|
230
|
+
// Report first few issues
|
|
231
|
+
const issues = result.error.issues.slice(0, 3);
|
|
232
|
+
const details = issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
233
|
+
return {
|
|
234
|
+
label: "State validity",
|
|
235
|
+
passed: false,
|
|
236
|
+
detail: `schema errors: ${details}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return { label: "State validity", passed: false, detail: "invalid JSON" };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
244
|
+
export async function doctor() {
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log("Claudemon Doctor");
|
|
247
|
+
console.log("================");
|
|
248
|
+
console.log("");
|
|
249
|
+
const checks = [
|
|
250
|
+
await checkBun(),
|
|
251
|
+
await checkStateDir(),
|
|
252
|
+
await checkStateFile(),
|
|
253
|
+
await checkMcpServer(),
|
|
254
|
+
await checkHooks(),
|
|
255
|
+
await checkSkill(),
|
|
256
|
+
await checkHookScript(),
|
|
257
|
+
await checkStateManager(),
|
|
258
|
+
await checkStaleLock(),
|
|
259
|
+
await checkSpriteCount(),
|
|
260
|
+
await checkStateValidity(),
|
|
261
|
+
];
|
|
262
|
+
for (const check of checks) {
|
|
263
|
+
console.log(formatCheck(check));
|
|
264
|
+
}
|
|
265
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
266
|
+
const total = checks.length;
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log(`Result: ${passed}/${total} checks passed`);
|
|
269
|
+
if (passed < total) {
|
|
270
|
+
console.log("");
|
|
271
|
+
console.log("Run 'bun run cli/install.ts' to fix missing components.");
|
|
272
|
+
}
|
|
273
|
+
console.log("");
|
|
274
|
+
}
|
|
275
|
+
// Run if executed directly
|
|
276
|
+
doctor().catch((err) => {
|
|
277
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
278
|
+
console.error(`\nDoctor failed: ${message}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Claudemon CLI entry point.
|
|
4
|
+
* Routes to install, uninstall, update, or doctor based on first argument.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx claudemon install
|
|
8
|
+
* npx claudemon uninstall
|
|
9
|
+
* npx claudemon update
|
|
10
|
+
* npx claudemon doctor
|
|
11
|
+
*/
|
|
12
|
+
const command = process.argv[2];
|
|
13
|
+
switch (command) {
|
|
14
|
+
case "install":
|
|
15
|
+
await import("./install.js");
|
|
16
|
+
break;
|
|
17
|
+
case "uninstall":
|
|
18
|
+
await import("./uninstall.js");
|
|
19
|
+
break;
|
|
20
|
+
case "update":
|
|
21
|
+
await import("./update.js");
|
|
22
|
+
break;
|
|
23
|
+
case "doctor":
|
|
24
|
+
await import("./doctor.js");
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
console.log(`
|
|
28
|
+
Claudemon — Pokemon Gen 1 coding companion for Claude Code
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
claudemon install Set up Claudemon (MCP server, hooks, skill, status line)
|
|
32
|
+
claudemon uninstall Remove Claudemon from Claude Code
|
|
33
|
+
claudemon update Re-register everything (preserves save data)
|
|
34
|
+
claudemon doctor Run diagnostics
|
|
35
|
+
|
|
36
|
+
After install, start a new Claude Code session and type /buddy
|
|
37
|
+
`);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claudemon CLI Installer.
|
|
3
|
+
* Registers MCP server, hooks, and skill into Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun run cli/install.ts
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, copyFile, chmod, access } from "node:fs/promises";
|
|
8
|
+
import { constants as fsConstants } from "node:fs";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { ok, fail, readJson, writeJson, CLAUDE_DIR, CLAUDE_CONFIG, CLAUDE_SETTINGS, STATE_DIR, SKILL_SRC, SKILL_DEST_DIR, SKILL_DEST, HOOK_SCRIPT, STOP_HOOK_SCRIPT, USER_PROMPT_HOOK_SCRIPT, STATUSLINE_SCRIPT, getRuntime, } from "./shared.js";
|
|
11
|
+
// ── Step 1: Check Prerequisites ──────────────────────────────
|
|
12
|
+
async function checkPrerequisites() {
|
|
13
|
+
let allGood = true;
|
|
14
|
+
// Check bun (sanity)
|
|
15
|
+
try {
|
|
16
|
+
const result = spawnSync("bun", ["--version"], { stdio: "pipe" });
|
|
17
|
+
if (result.error)
|
|
18
|
+
throw result.error;
|
|
19
|
+
const output = result.stdout?.toString().trim();
|
|
20
|
+
ok(`Bun runtime: v${output}`);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
fail("Bun runtime not found. Install from https://bun.sh");
|
|
24
|
+
allGood = false;
|
|
25
|
+
}
|
|
26
|
+
// Check Claude Code directory
|
|
27
|
+
try {
|
|
28
|
+
await access(CLAUDE_DIR, fsConstants.F_OK);
|
|
29
|
+
ok("Claude Code directory: ~/.claude/");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
fail("Claude Code not found. Install Claude Code first: https://claude.ai/download");
|
|
33
|
+
console.error(" Expected directory: ~/.claude/");
|
|
34
|
+
allGood = false;
|
|
35
|
+
}
|
|
36
|
+
return allGood;
|
|
37
|
+
}
|
|
38
|
+
// ── Step 2: Create State Directory ───────────────────────────
|
|
39
|
+
async function createStateDirectory() {
|
|
40
|
+
await mkdir(STATE_DIR, { recursive: true });
|
|
41
|
+
ok(`State directory: ${STATE_DIR}/`);
|
|
42
|
+
}
|
|
43
|
+
// ── Step 3: Register MCP Server ──────────────────────────────
|
|
44
|
+
async function registerMcpServer() {
|
|
45
|
+
const existing = await readJson(CLAUDE_CONFIG);
|
|
46
|
+
const config = existing ?? {};
|
|
47
|
+
if (!config.mcpServers) {
|
|
48
|
+
config.mcpServers = {};
|
|
49
|
+
}
|
|
50
|
+
config.mcpServers["claudemon"] = {
|
|
51
|
+
command: getRuntime().command,
|
|
52
|
+
args: [getRuntime().serverEntry],
|
|
53
|
+
env: {},
|
|
54
|
+
};
|
|
55
|
+
await writeJson(CLAUDE_CONFIG, config);
|
|
56
|
+
ok("MCP server: registered in ~/.claude.json");
|
|
57
|
+
}
|
|
58
|
+
// ── Step 4: Install Hooks ────────────────────────────────────
|
|
59
|
+
async function installHooks() {
|
|
60
|
+
const existing = await readJson(CLAUDE_SETTINGS);
|
|
61
|
+
const settings = existing ?? {};
|
|
62
|
+
if (!settings.hooks) {
|
|
63
|
+
settings.hooks = {};
|
|
64
|
+
}
|
|
65
|
+
const ourHook = {
|
|
66
|
+
type: "command",
|
|
67
|
+
command: HOOK_SCRIPT,
|
|
68
|
+
timeout: 5000,
|
|
69
|
+
};
|
|
70
|
+
const ourMatcher = {
|
|
71
|
+
matcher: "Bash|Write|Edit|Read|Grep|Glob",
|
|
72
|
+
hooks: [ourHook],
|
|
73
|
+
};
|
|
74
|
+
// Check if PostToolUse already has our matcher
|
|
75
|
+
const postToolUse = settings.hooks["PostToolUse"];
|
|
76
|
+
if (postToolUse) {
|
|
77
|
+
// Remove any existing claudemon matcher (idempotent reinstall)
|
|
78
|
+
const filtered = postToolUse.filter((m) => !m.hooks.some((h) => h.command.includes("post-tool-use.sh")));
|
|
79
|
+
filtered.push(ourMatcher);
|
|
80
|
+
settings.hooks["PostToolUse"] = filtered;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
settings.hooks["PostToolUse"] = [ourMatcher];
|
|
84
|
+
}
|
|
85
|
+
// ── Stop hook (extracts buddy comments from responses) ────
|
|
86
|
+
const stopHook = {
|
|
87
|
+
type: "command",
|
|
88
|
+
command: STOP_HOOK_SCRIPT,
|
|
89
|
+
timeout: 3000,
|
|
90
|
+
};
|
|
91
|
+
const stopEntry = {
|
|
92
|
+
hooks: [stopHook],
|
|
93
|
+
};
|
|
94
|
+
const stopHooks = settings.hooks["Stop"];
|
|
95
|
+
if (stopHooks) {
|
|
96
|
+
const filtered = stopHooks.filter((m) => !m.hooks.some((h) => h.command.includes("stop.sh")));
|
|
97
|
+
filtered.push(stopEntry);
|
|
98
|
+
settings.hooks["Stop"] = filtered;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
settings.hooks["Stop"] = [stopEntry];
|
|
102
|
+
}
|
|
103
|
+
// ── UserPromptSubmit hook (reacts to Pokemon name mentions) ─
|
|
104
|
+
const userPromptHook = {
|
|
105
|
+
type: "command",
|
|
106
|
+
command: USER_PROMPT_HOOK_SCRIPT,
|
|
107
|
+
timeout: 3000,
|
|
108
|
+
};
|
|
109
|
+
const userPromptEntry = {
|
|
110
|
+
hooks: [userPromptHook],
|
|
111
|
+
};
|
|
112
|
+
const userPromptHooks = settings.hooks["UserPromptSubmit"];
|
|
113
|
+
if (userPromptHooks) {
|
|
114
|
+
const filtered = userPromptHooks.filter((m) => !m.hooks.some((h) => h.command.includes("user-prompt-submit.sh")));
|
|
115
|
+
filtered.push(userPromptEntry);
|
|
116
|
+
settings.hooks["UserPromptSubmit"] = filtered;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
settings.hooks["UserPromptSubmit"] = [userPromptEntry];
|
|
120
|
+
}
|
|
121
|
+
await writeJson(CLAUDE_SETTINGS, settings);
|
|
122
|
+
ok("Hooks: PostToolUse, Stop, UserPromptSubmit configured in ~/.claude/settings.json");
|
|
123
|
+
}
|
|
124
|
+
// ── Step 5: Register Status Line ─────────────────────────────
|
|
125
|
+
async function registerStatusLine() {
|
|
126
|
+
const existing = await readJson(CLAUDE_SETTINGS);
|
|
127
|
+
const settings = existing ?? {};
|
|
128
|
+
// Merge status line config without overwriting other settings
|
|
129
|
+
settings.statusLine = {
|
|
130
|
+
type: "command",
|
|
131
|
+
command: STATUSLINE_SCRIPT,
|
|
132
|
+
refreshInterval: 1,
|
|
133
|
+
};
|
|
134
|
+
await writeJson(CLAUDE_SETTINGS, settings);
|
|
135
|
+
ok("Status line: registered in ~/.claude/settings.json");
|
|
136
|
+
}
|
|
137
|
+
// ── Step 6: Install Skill ─────────────────────────────────────
|
|
138
|
+
async function installSkill() {
|
|
139
|
+
await mkdir(SKILL_DEST_DIR, { recursive: true });
|
|
140
|
+
await copyFile(SKILL_SRC, SKILL_DEST);
|
|
141
|
+
ok("Skill: /buddy command installed to ~/.claude/skills/buddy/");
|
|
142
|
+
}
|
|
143
|
+
// ── Step 7: Set Script Permissions ───────────────────────────
|
|
144
|
+
async function setScriptPermissions() {
|
|
145
|
+
await chmod(HOOK_SCRIPT, 0o755);
|
|
146
|
+
ok("Hook script: post-tool-use.sh set executable");
|
|
147
|
+
await chmod(STOP_HOOK_SCRIPT, 0o755);
|
|
148
|
+
ok("Hook script: stop.sh set executable");
|
|
149
|
+
await chmod(USER_PROMPT_HOOK_SCRIPT, 0o755);
|
|
150
|
+
ok("Hook script: user-prompt-submit.sh set executable");
|
|
151
|
+
await chmod(STATUSLINE_SCRIPT, 0o755);
|
|
152
|
+
ok("Status line script: set executable");
|
|
153
|
+
}
|
|
154
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
155
|
+
export async function install() {
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log("Claudemon Installer");
|
|
158
|
+
console.log("===================");
|
|
159
|
+
console.log("");
|
|
160
|
+
console.log("Checking prerequisites...");
|
|
161
|
+
const prereqOk = await checkPrerequisites();
|
|
162
|
+
if (!prereqOk) {
|
|
163
|
+
console.log("");
|
|
164
|
+
fail("Prerequisites not met. Fix the issues above and try again.");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
console.log("");
|
|
168
|
+
console.log("Installing...");
|
|
169
|
+
await createStateDirectory();
|
|
170
|
+
await registerMcpServer();
|
|
171
|
+
await installHooks();
|
|
172
|
+
await registerStatusLine();
|
|
173
|
+
await installSkill();
|
|
174
|
+
await setScriptPermissions();
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log("\u2713 Claudemon installed successfully!");
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log("Next steps:");
|
|
179
|
+
console.log(" 1. Start a new Claude Code session");
|
|
180
|
+
console.log(" 2. Type /buddy starter to pick your first Pokemon!");
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log("Run 'bun run cli/doctor.ts' to verify installation.");
|
|
183
|
+
console.log("");
|
|
184
|
+
}
|
|
185
|
+
// Run if executed directly
|
|
186
|
+
install().catch((err) => {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
console.error(`\nInstallation failed: ${message}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and utilities for Claudemon CLI commands.
|
|
3
|
+
* Centralizes Claude Code config interfaces and common helpers.
|
|
4
|
+
*/
|
|
5
|
+
import { resolve, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
// ── Constants ────────────────────────────────────────────────
|
|
8
|
+
const HOME = process.env["HOME"];
|
|
9
|
+
if (!HOME) {
|
|
10
|
+
console.error("Error: HOME environment variable is not set.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
export const CLI_HOME = HOME;
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
export const PROJECT_DIR = resolve(dirname(__dirname));
|
|
17
|
+
export const CLAUDE_DIR = `${HOME}/.claude`;
|
|
18
|
+
export const CLAUDE_CONFIG = `${HOME}/.claude.json`;
|
|
19
|
+
export const CLAUDE_SETTINGS = `${CLAUDE_DIR}/settings.json`;
|
|
20
|
+
export const STATE_DIR = `${HOME}/.claudemon`;
|
|
21
|
+
export const SKILL_SRC = `${PROJECT_DIR}/skills/buddy/SKILL.md`;
|
|
22
|
+
export const SKILL_DEST_DIR = `${CLAUDE_DIR}/skills/buddy`;
|
|
23
|
+
export const SKILL_DEST = `${SKILL_DEST_DIR}/SKILL.md`;
|
|
24
|
+
export const HOOK_SCRIPT = `${PROJECT_DIR}/hooks/post-tool-use.sh`;
|
|
25
|
+
export const STOP_HOOK_SCRIPT = `${PROJECT_DIR}/hooks/stop.sh`;
|
|
26
|
+
export const USER_PROMPT_HOOK_SCRIPT = `${PROJECT_DIR}/hooks/user-prompt-submit.sh`;
|
|
27
|
+
export const STATUSLINE_SCRIPT = `${PROJECT_DIR}/statusline/buddy-status.sh`;
|
|
28
|
+
export const SERVER_ENTRY_TS = `${PROJECT_DIR}/src/server/index.ts`;
|
|
29
|
+
export const SERVER_ENTRY_JS = `${PROJECT_DIR}/dist/src/server/index.js`;
|
|
30
|
+
/** Find the best runtime and server entry — prefers bun (fast), falls back to node (compiled JS) */
|
|
31
|
+
export function getRuntime() {
|
|
32
|
+
const { existsSync } = require("node:fs");
|
|
33
|
+
// Check for bun (can run .ts directly)
|
|
34
|
+
const bunCandidates = [`${HOME}/.bun/bin/bun`, "/usr/local/bin/bun", "/usr/bin/bun"];
|
|
35
|
+
for (const p of bunCandidates) {
|
|
36
|
+
if (existsSync(p)) {
|
|
37
|
+
return { command: p, serverEntry: SERVER_ENTRY_TS };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Fall back to node (needs compiled .js from dist/)
|
|
41
|
+
if (existsSync(SERVER_ENTRY_JS)) {
|
|
42
|
+
return { command: "node", serverEntry: SERVER_ENTRY_JS };
|
|
43
|
+
}
|
|
44
|
+
// Last resort: assume bun in PATH
|
|
45
|
+
return { command: "bun", serverEntry: SERVER_ENTRY_TS };
|
|
46
|
+
}
|
|
47
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
48
|
+
export function ok(msg) {
|
|
49
|
+
console.log(` \u2713 ${msg}`);
|
|
50
|
+
}
|
|
51
|
+
export function fail(msg) {
|
|
52
|
+
console.error(` \u2717 ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
export function info(msg) {
|
|
55
|
+
console.log(` - ${msg}`);
|
|
56
|
+
}
|
|
57
|
+
export async function readJson(path) {
|
|
58
|
+
const { readFile, access } = await import("node:fs/promises");
|
|
59
|
+
const { constants } = await import("node:fs");
|
|
60
|
+
try {
|
|
61
|
+
await access(path, constants.F_OK);
|
|
62
|
+
const text = await readFile(path, "utf-8");
|
|
63
|
+
return JSON.parse(text);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function writeJson(path, data) {
|
|
70
|
+
const { writeFile } = await import("node:fs/promises");
|
|
71
|
+
await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
72
|
+
}
|