@ramarivera/coding-buddy 0.4.0-alpha.2 → 0.4.0-alpha.4

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 (53) hide show
  1. package/README.md +4 -5
  2. package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
  3. package/{cli → adapters/claude/install}/backup.ts +10 -9
  4. package/{cli → adapters/claude/install}/disable.ts +27 -6
  5. package/{cli → adapters/claude/install}/doctor.ts +44 -31
  6. package/{cli → adapters/claude/install}/hunt.ts +4 -4
  7. package/{cli → adapters/claude/install}/install.ts +81 -42
  8. package/{cli → adapters/claude/install}/pick.ts +3 -3
  9. package/{cli → adapters/claude/install}/settings.ts +1 -1
  10. package/{cli → adapters/claude/install}/show.ts +2 -2
  11. package/{cli → adapters/claude/install}/test-statusline.ts +3 -2
  12. package/{cli → adapters/claude/install}/uninstall.ts +31 -10
  13. package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
  14. package/adapters/claude/popup/buddy-popup.sh +92 -0
  15. package/adapters/claude/popup/buddy-render.sh +540 -0
  16. package/adapters/claude/popup/popup-manager.sh +355 -0
  17. package/{server → adapters/claude/rendering}/art.ts +3 -115
  18. package/{server → adapters/claude/server}/index.ts +49 -71
  19. package/adapters/claude/server/instructions.ts +24 -0
  20. package/adapters/claude/server/resources.ts +38 -0
  21. package/adapters/claude/storage/achievements.ts +253 -0
  22. package/adapters/claude/storage/identity.ts +13 -0
  23. package/adapters/claude/storage/paths.ts +24 -0
  24. package/adapters/claude/storage/settings.ts +41 -0
  25. package/{server → adapters/claude/storage}/state.ts +3 -65
  26. package/adapters/pi/README.md +64 -0
  27. package/adapters/pi/commands.ts +173 -0
  28. package/adapters/pi/events.ts +150 -0
  29. package/adapters/pi/identity.ts +10 -0
  30. package/adapters/pi/index.ts +25 -0
  31. package/adapters/pi/renderers.ts +73 -0
  32. package/adapters/pi/storage.ts +295 -0
  33. package/adapters/pi/tools.ts +6 -0
  34. package/adapters/pi/ui.ts +39 -0
  35. package/cli/index.ts +11 -11
  36. package/cli/verify.ts +2 -2
  37. package/core/achievements.ts +203 -0
  38. package/core/art-data.ts +105 -0
  39. package/core/command-service.ts +338 -0
  40. package/core/model.ts +59 -0
  41. package/core/ports.ts +40 -0
  42. package/core/render-model.ts +10 -0
  43. package/package.json +23 -19
  44. package/server/achievements.ts +0 -445
  45. /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
  46. /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
  47. /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
  48. /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
  49. /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
  50. /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
  51. /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
  52. /package/{server → core}/engine.ts +0 -0
  53. /package/{server → core}/reactions.ts +0 -0
package/README.md CHANGED
@@ -196,11 +196,10 @@ MCP is an industry-standard protocol. Skills are Markdown files. Hooks and statu
196
196
 
197
197
  ```
198
198
  claude-buddy/
199
- ├── server/ # MCP servertools, engine, art, reactions, state
200
- ├── skills/buddy/ # /buddy slash command
201
- ├── hooks/ # PostToolUse + Stop hooks (error & comment detection)
202
- ├── statusline/ # Animated right-aligned buddy display
203
- └── cli/ # install, show, hunt, verify, doctor, backup, uninstall
199
+ ├── core/ # shared buddy domain logic — engine, reactions, achievements, models
200
+ ├── adapters/claude/ # Claude adapter — MCP server, storage, skills, hooks, UI, installer
201
+ ├── adapters/pi/ # pi adapter native pi extension (planned)
202
+ └── cli/ # host-agnostic CLI entrypoints/utilities
204
203
  ```
205
204
 
206
205
  </details>
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "./hooks/react.sh"
9
+ "command": "./adapters/claude/hooks/react.sh"
10
10
  }
11
11
  ]
12
12
  }
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "./hooks/buddy-comment.sh"
19
+ "command": "./adapters/claude/hooks/buddy-comment.sh"
20
20
  }
21
21
  ]
22
22
  }
@@ -26,7 +26,7 @@
26
26
  "hooks": [
27
27
  {
28
28
  "type": "command",
29
- "command": "./hooks/name-react.sh"
29
+ "command": "./adapters/claude/hooks/name-react.sh"
30
30
  }
31
31
  ]
32
32
  }
@@ -25,14 +25,15 @@ import {
25
25
  readFileSync, writeFileSync, mkdirSync, existsSync,
26
26
  readdirSync, statSync, rmSync, copyFileSync,
27
27
  } from "fs";
28
- import { join } from "path";
28
+ import { dirname, join } from "path";
29
29
  import { homedir } from "os";
30
+ import { getBuddySkillDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
30
31
 
31
32
  const HOME = homedir();
32
33
  const BACKUPS_DIR = join(HOME, ".claude-buddy", "backups");
33
- const SETTINGS = join(HOME, ".claude", "settings.json");
34
- const CLAUDE_JSON = join(HOME, ".claude.json");
35
- const SKILL = join(HOME, ".claude", "skills", "buddy", "SKILL.md");
34
+ const SETTINGS = getClaudeSettingsPath();
35
+ const CLAUDE_JSON = getClaudeJsonPath();
36
+ const SKILL = join(getBuddySkillDir(), "SKILL.md");
36
37
  const STATE_DIR = join(HOME, ".claude-buddy");
37
38
 
38
39
  const RED = "\x1b[31m";
@@ -73,7 +74,7 @@ function createBackup(): string {
73
74
  const dir = join(BACKUPS_DIR, ts);
74
75
  mkdirSync(dir, { recursive: true });
75
76
 
76
- const manifest: Record<string, any> = { timestamp: ts, files: [] };
77
+ const manifest: { timestamp: string; files: string[] } = { timestamp: ts, files: [] };
77
78
 
78
79
  // 1. settings.json
79
80
  const settings = tryRead(SETTINGS);
@@ -193,7 +194,7 @@ function restoreBackup(ts: string) {
193
194
  // 1. settings.json — overwrite
194
195
  const settingsBak = join(dir, "settings.json");
195
196
  if (existsSync(settingsBak)) {
196
- mkdirSync(join(HOME, ".claude"), { recursive: true });
197
+ mkdirSync(dirname(SETTINGS), { recursive: true });
197
198
  copyFileSync(settingsBak, SETTINGS);
198
199
  ok("Restored: ~/.claude/settings.json");
199
200
  }
@@ -202,9 +203,9 @@ function restoreBackup(ts: string) {
202
203
  const mcpBak = join(dir, "mcpserver.json");
203
204
  if (existsSync(mcpBak)) {
204
205
  const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
205
- let claudeJson: Record<string, any> = {};
206
+ let claudeJson: { mcpServers?: Record<string, unknown> } = {};
206
207
  try {
207
- claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
208
+ claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8")) as { mcpServers?: Record<string, unknown> };
208
209
  } catch { /* empty */ }
209
210
  if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
210
211
  claudeJson.mcpServers["claude-buddy"] = ourMcp;
@@ -215,7 +216,7 @@ function restoreBackup(ts: string) {
215
216
  // 3. SKILL.md
216
217
  const skillBak = join(dir, "SKILL.md");
217
218
  if (existsSync(skillBak)) {
218
- mkdirSync(join(HOME, ".claude", "skills", "buddy"), { recursive: true });
219
+ mkdirSync(dirname(SKILL), { recursive: true });
219
220
  copyFileSync(skillBak, SKILL);
220
221
  ok("Restored: ~/.claude/skills/buddy/SKILL.md");
221
222
  }
@@ -11,6 +11,27 @@
11
11
  import { readFileSync, writeFileSync, existsSync } from "fs";
12
12
  import { join } from "path";
13
13
  import { homedir } from "os";
14
+ import { getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
15
+
16
+ interface HookCommand {
17
+ type: "command";
18
+ command: string;
19
+ }
20
+
21
+ interface HookMatcherEntry {
22
+ matcher?: string;
23
+ hooks?: HookCommand[];
24
+ }
25
+
26
+ interface ClaudeSettings {
27
+ statusLine?: { command?: string };
28
+ hooks?: Record<string, HookMatcherEntry[]>;
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ function hasBuddyHook(entry: HookMatcherEntry): boolean {
33
+ return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
34
+ }
14
35
 
15
36
  const GREEN = "\x1b[32m";
16
37
  const YELLOW = "\x1b[33m";
@@ -22,8 +43,8 @@ function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
22
43
  function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
23
44
 
24
45
  const HOME = homedir();
25
- const CLAUDE_JSON = join(HOME, ".claude.json");
26
- const SETTINGS = join(HOME, ".claude", "settings.json");
46
+ const CLAUDE_JSON = getClaudeJsonPath();
47
+ const SETTINGS = getClaudeSettingsPath();
27
48
 
28
49
  console.log(`\n${BOLD}Disabling claude-buddy...${NC}\n`);
29
50
 
@@ -34,17 +55,17 @@ try {
34
55
  delete claudeJson.mcpServers["claude-buddy"];
35
56
  if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
36
57
  writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
37
- ok("MCP server removed from ~/.claude.json");
58
+ ok(`MCP server removed from ${CLAUDE_JSON}`);
38
59
  } else {
39
60
  warn("MCP server was not registered");
40
61
  }
41
62
  } catch {
42
- warn("Could not update ~/.claude.json");
63
+ warn(`Could not update ${CLAUDE_JSON}`);
43
64
  }
44
65
 
45
66
  // 2. Remove status line + hooks from settings.json
46
67
  try {
47
- const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
68
+ const settings = JSON.parse(readFileSync(SETTINGS, "utf8")) as ClaudeSettings;
48
69
  let changed = false;
49
70
 
50
71
  if (settings.statusLine?.command?.includes("buddy")) {
@@ -58,7 +79,7 @@ try {
58
79
  if (settings.hooks[hookType]) {
59
80
  const before = settings.hooks[hookType].length;
60
81
  settings.hooks[hookType] = settings.hooks[hookType].filter(
61
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
82
+ (h) => !hasBuddyHook(h),
62
83
  );
63
84
  if (settings.hooks[hookType].length < before) changed = true;
64
85
  if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
@@ -10,12 +10,17 @@
10
10
 
11
11
  import { readFileSync, existsSync, statSync } from "fs";
12
12
  import { execSync } from "child_process";
13
- import { join, resolve, dirname } from "path";
13
+ import { join, resolve } from "path";
14
14
  import { homedir } from "os";
15
+ import { getBuddySkillDir, getClaudeConfigDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
15
16
 
16
- const PROJECT_ROOT = resolve(dirname(import.meta.dir));
17
+ const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
17
18
  const HOME = homedir();
18
- const STATUS_SCRIPT = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
19
+ const CLAUDE_DIR = getClaudeConfigDir();
20
+ const CLAUDE_JSON = getClaudeJsonPath();
21
+ const SETTINGS = getClaudeSettingsPath();
22
+ const SKILL_PATH = join(getBuddySkillDir(), "SKILL.md");
23
+ const STATUS_SCRIPT = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
19
24
 
20
25
  const RED = "\x1b[31m";
21
26
  const GREEN = "\x1b[32m";
@@ -49,9 +54,13 @@ function tryRead(path: string): string | null {
49
54
  try { return readFileSync(path, "utf8"); } catch { return null; }
50
55
  }
51
56
 
52
- function tryParseJson(text: string | null): any | null {
57
+ function tryParseJson(text: string | null): unknown {
53
58
  if (!text) return null;
54
- try { return JSON.parse(text); } catch { return null; }
59
+ try { return JSON.parse(text) as unknown; } catch { return null; }
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null;
55
64
  }
56
65
 
57
66
  // ─── Header ─────────────────────────────────────────────────────────────────
@@ -91,8 +100,10 @@ row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
91
100
  section("Filesystem");
92
101
  const procExists = existsSync("/proc");
93
102
  row("/proc exists", procExists ? `${GREEN}yes${NC} (Linux)` : `${RED}no${NC} (macOS/BSD)`);
94
- row("~/.claude/ exists", existsSync(join(HOME, ".claude")) ? "yes" : "no");
95
- row("~/.claude.json exists", existsSync(join(HOME, ".claude.json")) ? "yes" : "no");
103
+ row("Claude config dir", CLAUDE_DIR);
104
+ row("Claude config dir exists", existsSync(CLAUDE_DIR) ? "yes" : "no");
105
+ row("Claude config file", CLAUDE_JSON);
106
+ row("Claude config file exists", existsSync(CLAUDE_JSON) ? "yes" : "no");
96
107
  row("~/.claude-buddy/ exists", existsSync(join(HOME, ".claude-buddy")) ? "yes" : "no");
97
108
  row("Project root", PROJECT_ROOT);
98
109
  row("Status script exists", existsSync(STATUS_SCRIPT) ? "yes" : `${RED}no${NC}`);
@@ -103,18 +114,20 @@ section("claude-buddy state");
103
114
  const menagerie = tryParseJson(tryRead(join(HOME, ".claude-buddy", "menagerie.json")));
104
115
  const status = tryParseJson(tryRead(join(HOME, ".claude-buddy", "status.json")));
105
116
 
106
- if (menagerie) {
107
- const activeSlot = menagerie.active ?? "buddy";
108
- const companion = menagerie.companions?.[activeSlot];
117
+ if (isRecord(menagerie)) {
118
+ const activeSlot = typeof menagerie.active === "string" ? menagerie.active : "buddy";
119
+ const companions = isRecord(menagerie.companions) ? menagerie.companions : {};
120
+ const companion = companions[activeSlot];
109
121
  row("Active slot", activeSlot);
110
- row("Total slots", String(Object.keys(menagerie.companions ?? {}).length));
111
- if (companion) {
112
- row("Companion name", companion.name ?? "(none)");
113
- row("Species", companion.bones?.species ?? "(none)");
114
- row("Rarity", companion.bones?.rarity ?? "(none)");
115
- row("Hat", companion.bones?.hat ?? "(none)");
116
- row("Eye", companion.bones?.eye ?? "(none)");
117
- row("Shiny", String(companion.bones?.shiny ?? false));
122
+ row("Total slots", String(Object.keys(companions).length));
123
+ if (isRecord(companion)) {
124
+ const bones = isRecord(companion.bones) ? companion.bones : {};
125
+ row("Companion name", typeof companion.name === "string" ? companion.name : "(none)");
126
+ row("Species", typeof bones.species === "string" ? bones.species : "(none)");
127
+ row("Rarity", typeof bones.rarity === "string" ? bones.rarity : "(none)");
128
+ row("Hat", typeof bones.hat === "string" ? bones.hat : "(none)");
129
+ row("Eye", typeof bones.eye === "string" ? bones.eye : "(none)");
130
+ row("Shiny", String(typeof bones.shiny === "boolean" ? bones.shiny : false));
118
131
  } else {
119
132
  err(`No companion found in active slot "${activeSlot}"`);
120
133
  }
@@ -122,9 +135,9 @@ if (menagerie) {
122
135
  err("No manifest found at ~/.claude-buddy/menagerie.json");
123
136
  }
124
137
 
125
- if (status) {
126
- row("Status muted", String(status.muted ?? false));
127
- row("Current reaction", status.reaction || "(none)");
138
+ if (isRecord(status)) {
139
+ row("Status muted", String(typeof status.muted === "boolean" ? status.muted : false));
140
+ row("Current reaction", typeof status.reaction === "string" && status.reaction.length > 0 ? status.reaction : "(none)");
128
141
  } else {
129
142
  warn("No status state at ~/.claude-buddy/status.json");
130
143
  }
@@ -132,38 +145,38 @@ if (status) {
132
145
  // ─── settings.json ──────────────────────────────────────────────────────────
133
146
 
134
147
  section("Claude Code config");
135
- const settings = tryParseJson(tryRead(join(HOME, ".claude", "settings.json")));
136
- const claudeJson = tryParseJson(tryRead(join(HOME, ".claude.json")));
148
+ const settings = tryParseJson(tryRead(SETTINGS));
149
+ const claudeJson = tryParseJson(tryRead(CLAUDE_JSON));
137
150
 
138
- if (settings?.statusLine) {
151
+ if (isRecord(settings) && "statusLine" in settings && settings.statusLine !== undefined) {
139
152
  console.log(` ${DIM}statusLine:${NC}`);
140
153
  console.log(` ${JSON.stringify(settings.statusLine, null, 2).split("\n").join("\n ")}`);
141
154
  } else {
142
155
  warn("No statusLine in ~/.claude/settings.json");
143
156
  }
144
157
 
145
- if (settings?.hooks) {
158
+ if (isRecord(settings) && isRecord(settings.hooks)) {
146
159
  console.log(` ${DIM}hooks:${NC}`);
147
160
  for (const event of Object.keys(settings.hooks)) {
148
- const count = settings.hooks[event]?.length ?? 0;
161
+ const hookEntries = settings.hooks[event];
162
+ const count = Array.isArray(hookEntries) ? hookEntries.length : 0;
149
163
  row(` ${event}`, `${count} entr${count === 1 ? "y" : "ies"}`);
150
164
  }
151
165
  } else {
152
166
  warn("No hooks configured");
153
167
  }
154
168
 
155
- if (claudeJson?.mcpServers?.["claude-buddy"]) {
169
+ if (isRecord(claudeJson) && isRecord(claudeJson.mcpServers) && isRecord(claudeJson.mcpServers["claude-buddy"])) {
156
170
  ok("MCP server registered in ~/.claude.json");
157
171
  console.log(` ${JSON.stringify(claudeJson.mcpServers["claude-buddy"], null, 2).split("\n").join("\n ")}`);
158
172
  } else {
159
173
  err("MCP server NOT registered in ~/.claude.json");
160
174
  }
161
175
 
162
- const skillPath = join(HOME, ".claude", "skills", "buddy", "SKILL.md");
163
- if (existsSync(skillPath)) {
164
- ok(`Skill installed: ${skillPath}`);
176
+ if (existsSync(SKILL_PATH)) {
177
+ ok(`Skill installed: ${SKILL_PATH}`);
165
178
  } else {
166
- err(`Skill missing: ${skillPath}`);
179
+ err(`Skill missing: ${SKILL_PATH}`);
167
180
  }
168
181
 
169
182
  // ─── Live status line test ──────────────────────────────────────────────────
@@ -10,11 +10,11 @@
10
10
  import {
11
11
  searchBuddy, renderBuddy, SPECIES, RARITIES, STAT_NAMES,
12
12
  type Species, type Rarity, type StatName, type SearchCriteria,
13
- } from "../server/engine.ts";
13
+ } from "../../../core/engine.ts";
14
14
  import {
15
15
  saveCompanionSlot, saveActiveSlot, writeStatusState,
16
16
  slugify, unusedName, listCompanionSlots,
17
- } from "../server/state.ts";
17
+ } from "../storage/state.ts";
18
18
  import { createInterface } from "readline";
19
19
 
20
20
  const CYAN = "\x1b[36m";
@@ -67,8 +67,8 @@ ${CYAN}╚═══════════════════════
67
67
  const statsAns = await ask(`\n Configure stats? [Y/n]: `);
68
68
  if (statsAns.toLowerCase() !== "n") {
69
69
  wantPeak = await pickFromList("Peak stat (highest):", STAT_NAMES);
70
- const dumpOptions = STAT_NAMES.filter((s) => s !== wantPeak);
71
- wantDump = await pickFromList("Dump stat (lowest):", dumpOptions as any);
70
+ const dumpOptions = STAT_NAMES.filter((s): s is Exclude<StatName, typeof wantPeak> => s !== wantPeak);
71
+ wantDump = await pickFromList("Dump stat (lowest):", dumpOptions);
72
72
  console.log(`${GREEN}✓${NC} peak=${wantPeak} dump=${wantDump}`);
73
73
  }
74
74
 
@@ -5,14 +5,15 @@
5
5
  * Checks: bun, jq, ~/.claude/ directory
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, statSync } from "fs";
9
9
  import { execSync } from "child_process";
10
- import { join, resolve, dirname } from "path";
11
- import { homedir } from "os";
10
+ import { join, resolve } from "path";
12
11
 
13
- import { generateBones, renderBuddy, renderFace, RARITY_STARS } from "../server/engine.ts";
14
- import { loadCompanion, saveCompanion, resolveUserId, writeStatusState } from "../server/state.ts";
15
- import { generateFallbackName } from "../server/reactions.ts";
12
+ import { generateBones, renderBuddy, renderFace, RARITY_STARS } from "../../../core/engine.ts";
13
+ import { loadCompanion, saveCompanion, writeStatusState } from "../storage/state.ts";
14
+ import { resolveUserId } from "../storage/identity.ts";
15
+ import { generateFallbackName } from "../../../core/reactions.ts";
16
+ import { getBuddySkillDir, getClaudeConfigDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
16
17
 
17
18
  const CYAN = "\x1b[36m";
18
19
  const GREEN = "\x1b[32m";
@@ -22,10 +23,11 @@ const BOLD = "\x1b[1m";
22
23
  const DIM = "\x1b[2m";
23
24
  const NC = "\x1b[0m";
24
25
 
25
- const CLAUDE_DIR = join(homedir(), ".claude");
26
- const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
27
- const BUDDY_DIR = join(CLAUDE_DIR, "skills", "buddy");
28
- const PROJECT_ROOT = resolve(dirname(import.meta.dir));
26
+ const CLAUDE_DIR = getClaudeConfigDir();
27
+ const SETTINGS_FILE = getClaudeSettingsPath();
28
+ const BUDDY_DIR = getBuddySkillDir();
29
+ const CLAUDE_JSON = getClaudeJsonPath();
30
+ const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
29
31
 
30
32
  function banner() {
31
33
  console.log(`
@@ -70,21 +72,23 @@ function preflight(): boolean {
70
72
  }
71
73
  }
72
74
 
73
- // Check ~/.claude/ exists
75
+ // Check Claude config dir exists and is actually a directory
74
76
  if (!existsSync(CLAUDE_DIR)) {
75
- err("~/.claude/ not found. Start Claude Code once first, then re-run.");
77
+ err(`Claude config dir not found: ${CLAUDE_DIR}`);
78
+ pass = false;
79
+ } else if (!statSync(CLAUDE_DIR).isDirectory()) {
80
+ err(`Claude config path is not a directory: ${CLAUDE_DIR}`);
76
81
  pass = false;
77
82
  } else {
78
- ok("~/.claude/ found");
83
+ ok(`Claude config dir found: ${CLAUDE_DIR}`);
79
84
  }
80
85
 
81
- // Check ~/.claude.json exists
82
- const claudeJson = join(homedir(), ".claude.json");
83
- if (!existsSync(claudeJson)) {
84
- err("~/.claude.json not found. Start Claude Code once first, then re-run.");
86
+ // Check .claude.json exists in the active Claude config location
87
+ if (!existsSync(CLAUDE_JSON)) {
88
+ err(`Claude config file not found: ${CLAUDE_JSON}`);
85
89
  pass = false;
86
90
  } else {
87
- ok("~/.claude.json found");
91
+ ok(`Claude config file found: ${CLAUDE_JSON}`);
88
92
  }
89
93
 
90
94
  return pass;
@@ -92,15 +96,50 @@ function preflight(): boolean {
92
96
 
93
97
  // ─── Load / update settings.json ────────────────────────────────────────────
94
98
 
95
- function loadSettings(): Record<string, any> {
99
+ interface HookCommand {
100
+ type: "command";
101
+ command: string;
102
+ }
103
+
104
+ interface HookMatcherEntry {
105
+ matcher?: string;
106
+ hooks?: HookCommand[];
107
+ }
108
+
109
+ interface ClaudeStatusLineConfig {
110
+ type: "command";
111
+ command: string;
112
+ padding: number;
113
+ refreshInterval: number;
114
+ }
115
+
116
+ interface ClaudeSettings {
117
+ statusLine?: ClaudeStatusLineConfig;
118
+ hooks?: Partial<Record<"SessionStart" | "SessionEnd" | "PostToolUse" | "Stop" | "UserPromptSubmit", HookMatcherEntry[]>>;
119
+ permissions?: {
120
+ allow?: string[];
121
+ };
122
+ [key: string]: unknown;
123
+ }
124
+
125
+ interface ClaudeJsonConfig {
126
+ mcpServers?: Record<string, { command: string; args: string[]; cwd: string }>;
127
+ [key: string]: unknown;
128
+ }
129
+
130
+ function hasBuddyHook(entry: HookMatcherEntry): boolean {
131
+ return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
132
+ }
133
+
134
+ function loadSettings(): ClaudeSettings {
96
135
  try {
97
- return JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
136
+ return JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as ClaudeSettings;
98
137
  } catch {
99
138
  return {};
100
139
  }
101
140
  }
102
141
 
103
- function saveSettings(settings: Record<string, any>) {
142
+ function saveSettings(settings: ClaudeSettings) {
104
143
  mkdirSync(CLAUDE_DIR, { recursive: true });
105
144
  writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
106
145
  }
@@ -108,12 +147,12 @@ function saveSettings(settings: Record<string, any>) {
108
147
  // ─── Step 1: Register MCP server (in ~/.claude.json) ────────────────────────
109
148
 
110
149
  function installMcp() {
111
- const serverPath = join(PROJECT_ROOT, "server", "index.ts");
112
- const claudeJsonPath = join(homedir(), ".claude.json");
150
+ const serverPath = join(PROJECT_ROOT, "adapters", "claude", "server", "index.ts");
151
+ const claudeJsonPath = CLAUDE_JSON;
113
152
 
114
- let claudeJson: Record<string, any> = {};
153
+ let claudeJson: ClaudeJsonConfig = {};
115
154
  try {
116
- claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
155
+ claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8")) as ClaudeJsonConfig;
117
156
  } catch { /* fresh config */ }
118
157
 
119
158
  if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
@@ -125,22 +164,22 @@ function installMcp() {
125
164
  };
126
165
 
127
166
  writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
128
- ok("MCP server registered in ~/.claude.json");
167
+ ok(`MCP server registered in ${claudeJsonPath}`);
129
168
  }
130
169
 
131
170
  // ─── Step 2: Install skill ──────────────────────────────────────────────────
132
171
 
133
172
  function installSkill() {
134
- const srcSkill = join(PROJECT_ROOT, "skills", "buddy", "SKILL.md");
173
+ const srcSkill = join(PROJECT_ROOT, "adapters", "claude", "skills", "buddy", "SKILL.md");
135
174
  mkdirSync(BUDDY_DIR, { recursive: true });
136
175
  cpSync(srcSkill, join(BUDDY_DIR, "SKILL.md"), { force: true });
137
- ok("Skill installed: ~/.claude/skills/buddy/SKILL.md");
176
+ ok(`Skill installed: ${join(BUDDY_DIR, "SKILL.md")}`);
138
177
  }
139
178
 
140
179
  // ─── Step 3: Configure status line (with animation refresh) ─────────────────
141
180
 
142
- function installStatusLine(settings: Record<string, any>) {
143
- const statusScript = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
181
+ function installStatusLine(settings: ClaudeSettings) {
182
+ const statusScript = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
144
183
 
145
184
  settings.statusLine = {
146
185
  type: "command",
@@ -167,15 +206,15 @@ function detectTmux(): boolean {
167
206
  }
168
207
  }
169
208
 
170
- function installPopupHooks(settings: Record<string, any>) {
171
- const popupManager = join(PROJECT_ROOT, "popup", "popup-manager.sh");
209
+ function installPopupHooks(settings: ClaudeSettings) {
210
+ const popupManager = join(PROJECT_ROOT, "adapters", "claude", "popup", "popup-manager.sh");
172
211
 
173
212
  if (!settings.hooks) settings.hooks = {};
174
213
 
175
214
  // SessionStart: open popup
176
215
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
177
216
  settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
178
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
217
+ (h) => !hasBuddyHook(h),
179
218
  );
180
219
  settings.hooks.SessionStart.push({
181
220
  hooks: [{ type: "command", command: `${popupManager} start` }],
@@ -184,7 +223,7 @@ function installPopupHooks(settings: Record<string, any>) {
184
223
  // SessionEnd: close popup
185
224
  if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
186
225
  settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
187
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
226
+ (h) => !hasBuddyHook(h),
188
227
  );
189
228
  settings.hooks.SessionEnd.push({
190
229
  hooks: [{ type: "command", command: `${popupManager} stop` }],
@@ -195,17 +234,17 @@ function installPopupHooks(settings: Record<string, any>) {
195
234
 
196
235
  // ─── Step 4: Register hooks ─────────────────────────────────────────────────
197
236
 
198
- function installHooks(settings: Record<string, any>) {
199
- const reactHook = join(PROJECT_ROOT, "hooks", "react.sh");
200
- const commentHook = join(PROJECT_ROOT, "hooks", "buddy-comment.sh");
201
- const nameHook = join(PROJECT_ROOT, "hooks", "name-react.sh");
237
+ function installHooks(settings: ClaudeSettings) {
238
+ const reactHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "react.sh");
239
+ const commentHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "buddy-comment.sh");
240
+ const nameHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "name-react.sh");
202
241
 
203
242
  if (!settings.hooks) settings.hooks = {};
204
243
 
205
244
  // PostToolUse: detect errors/test failures/successes in Bash output
206
245
  if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
207
246
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
208
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
247
+ (h) => !hasBuddyHook(h),
209
248
  );
210
249
  settings.hooks.PostToolUse.push({
211
250
  matcher: "Bash",
@@ -215,7 +254,7 @@ function installHooks(settings: Record<string, any>) {
215
254
  // Stop: extract <!-- buddy: --> comment from Claude's response
216
255
  if (!settings.hooks.Stop) settings.hooks.Stop = [];
217
256
  settings.hooks.Stop = settings.hooks.Stop.filter(
218
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
257
+ (h) => !hasBuddyHook(h),
219
258
  );
220
259
  settings.hooks.Stop.push({
221
260
  hooks: [{ type: "command", command: commentHook }],
@@ -224,7 +263,7 @@ function installHooks(settings: Record<string, any>) {
224
263
  // UserPromptSubmit: detect buddy's name in user message → instant status line reaction
225
264
  if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
226
265
  settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
227
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
266
+ (h) => !hasBuddyHook(h),
228
267
  );
229
268
  settings.hooks.UserPromptSubmit.push({
230
269
  hooks: [{ type: "command", command: nameHook }],
@@ -235,7 +274,7 @@ function installHooks(settings: Record<string, any>) {
235
274
 
236
275
  // ─── Step 5: Ensure MCP tools are allowed ───────────────────────────────────
237
276
 
238
- function ensurePermissions(settings: Record<string, any>) {
277
+ function ensurePermissions(settings: ClaudeSettings) {
239
278
  if (!settings.permissions) settings.permissions = {};
240
279
  if (!settings.permissions.allow) settings.permissions.allow = [];
241
280
 
@@ -18,12 +18,12 @@
18
18
  import {
19
19
  loadActiveSlot, saveActiveSlot, listCompanionSlots,
20
20
  loadCompanionSlot, saveCompanionSlot, slugify, unusedName, writeStatusState,
21
- } from "../server/state.ts";
21
+ } from "../storage/state.ts";
22
22
  import {
23
23
  generateBones, SPECIES, RARITIES, STAT_NAMES, RARITY_STARS,
24
24
  type Species, type Rarity, type StatName, type BuddyBones, type Companion,
25
- } from "../server/engine.ts";
26
- import { renderCompanionCard } from "../server/art.ts";
25
+ } from "../../../core/engine.ts";
26
+ import { renderCompanionCard } from "../rendering/art.ts";
27
27
  import { randomBytes } from "crypto";
28
28
 
29
29
  // ─── ANSI ─────────────────────────────────────────────────────────────────────
@@ -7,7 +7,7 @@
7
7
  * bun run settings cooldown 0 Set comment cooldown (0-300 seconds)
8
8
  */
9
9
 
10
- import { loadConfig, saveConfig } from "../server/state.ts";
10
+ import { loadConfig, saveConfig } from "../storage/state.ts";
11
11
 
12
12
  const args = process.argv.slice(2);
13
13
  const key = args[0];
@@ -2,8 +2,8 @@
2
2
  * claude-buddy show — display current companion in terminal
3
3
  */
4
4
 
5
- import { renderBuddy, renderFace, RARITY_STARS } from "../server/engine.ts";
6
- import { loadCompanion, loadReaction } from "../server/state.ts";
5
+ import { renderBuddy, renderFace, RARITY_STARS } from "../../../core/engine.ts";
6
+ import { loadCompanion, loadReaction } from "../storage/state.ts";
7
7
 
8
8
  const BOLD = "\x1b[1m";
9
9
  const DIM = "\x1b[2m";
@@ -14,9 +14,10 @@
14
14
  import { readFileSync, writeFileSync, existsSync, copyFileSync, chmodSync, mkdirSync } from "fs";
15
15
  import { join, dirname, resolve } from "path";
16
16
  import { homedir } from "os";
17
+ import { getClaudeSettingsPath } from "../storage/paths.ts";
17
18
 
18
19
  const HOME = homedir();
19
- const SETTINGS = join(HOME, ".claude", "settings.json");
20
+ const SETTINGS = getClaudeSettingsPath();
20
21
  const BACKUP = join(HOME, ".claude-buddy", "statusline.bak");
21
22
  const TEST_SCRIPT = join(HOME, ".claude-buddy", "test-statusline.sh");
22
23
  const SOURCE_SCRIPT = resolve(import.meta.dir, "test-statusline.sh");
@@ -41,7 +42,7 @@ if (action === "install") {
41
42
  console.log(`\n${BOLD}claude-buddy test status line installer${NC}\n`);
42
43
 
43
44
  if (!existsSync(SETTINGS)) {
44
- err("~/.claude/settings.json not found");
45
+ err(`Claude settings not found: ${SETTINGS}`);
45
46
  process.exit(1);
46
47
  }
47
48
  if (!existsSync(SOURCE_SCRIPT)) {