@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.6.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 (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
  3. package/LICENSE +1 -1
  4. package/README.md +88 -37
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +130 -33
  22. package/cli/buddy-shell.ts +1 -1
  23. package/{adapters/claude/install → cli}/disable.ts +19 -32
  24. package/cli/doctor.ts +231 -0
  25. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  26. package/cli/index.ts +30 -26
  27. package/{adapters/claude/install → cli}/install.ts +65 -83
  28. package/{adapters/claude/install → cli}/pick.ts +111 -50
  29. package/{adapters/claude/install → cli}/settings.ts +2 -2
  30. package/{adapters/claude/install → cli}/show.ts +4 -4
  31. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  32. package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
  33. package/cli/tui.tsx +106 -17
  34. package/{adapters/claude/install → cli}/uninstall.ts +40 -47
  35. package/cli/upgrade.ts +2 -2
  36. package/cli/validate-species.ts +209 -0
  37. package/cli/verify.ts +3 -3
  38. package/core/achievements.ts +12 -20
  39. package/core/art-data.ts +26 -0
  40. package/core/command-service.ts +99 -97
  41. package/core/engine.ts +83 -1
  42. package/core/ports.ts +10 -0
  43. package/core/reactions.ts +533 -95
  44. package/core/slot-slug.ts +22 -0
  45. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +19 -1
  46. package/hooks/file-type-react.sh +401 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +208 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
  50. package/hooks/react.sh +1368 -0
  51. package/hooks/suggest.sh +55 -0
  52. package/package.json +70 -31
  53. package/scripts/gen-emoji-widths.ts +34 -0
  54. package/scripts/paths.sh +17 -1
  55. package/scripts/postinstall.cjs +42 -0
  56. package/scripts/snapshot-statusline.sh +54 -0
  57. package/server/achievements.ts +1419 -15
  58. package/server/art.ts +450 -1
  59. package/server/award-xp.ts +48 -0
  60. package/server/check-suggestions.ts +102 -0
  61. package/server/consolidate.ts +33 -0
  62. package/{adapters/claude/server → server}/index.ts +556 -95
  63. package/server/mcp-launcher.sh +16 -6
  64. package/server/memory.ts +404 -0
  65. package/server/mood.ts +254 -0
  66. package/server/path.ts +70 -18
  67. package/server/shift-mood.ts +43 -0
  68. package/server/state.ts +540 -3
  69. package/server/suggestions.ts +335 -0
  70. package/server/theme.ts +53 -0
  71. package/server/xp.ts +435 -0
  72. package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
  73. package/statusline/buddy-status.sh +397 -0
  74. package/statusline/combined-status.sh +134 -0
  75. package/statusline/emoji-widths.data +4 -0
  76. package/adapters/claude/hooks/hooks.json +0 -35
  77. package/adapters/claude/hooks/react.sh +0 -207
  78. package/adapters/claude/install/doctor.ts +0 -172
  79. package/adapters/claude/rendering/art.ts +0 -264
  80. package/adapters/claude/server/instructions.ts +0 -24
  81. package/adapters/claude/server/path.ts +0 -12
  82. package/adapters/claude/server/resources.ts +0 -38
  83. package/adapters/claude/statusline/buddy-status.sh +0 -408
  84. package/adapters/claude/storage/achievements.ts +0 -253
  85. package/adapters/claude/storage/identity.ts +0 -13
  86. package/adapters/claude/storage/paths.ts +0 -33
  87. package/adapters/claude/storage/settings.ts +0 -91
  88. package/adapters/claude/storage/state.ts +0 -365
  89. package/adapters/pi/README.md +0 -83
  90. package/adapters/pi/tools.ts +0 -6
  91. package/server/engine.ts +0 -1
  92. package/server/reactions.ts +0 -1
@@ -1,6 +1,29 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * claude-buddy backup — snapshot all claude-buddy related state
3
+ * coding-buddy backup — snapshot all coding-buddy related state
4
+ *
5
+ * Usage:
6
+ * bun run backup Create a new snapshot
7
+ * bun run backup list List all backups
8
+ * bun run backup show <ts> Show what's in a backup
9
+ * bun run backup restore Restore the latest backup
10
+ * bun run backup restore <ts> Restore a specific backup
11
+ * bun run backup delete <ts> Delete a specific backup
12
+ *
13
+ * Backups are stored in <state-dir>/backups/YYYY-MM-DD-HHMMSS/, where
14
+ * <state-dir> resolves via server/paths.ts ($CLAUDE_CONFIG_DIR/buddy-state
15
+ * when that env var is set, else ~/.claude-buddy).
16
+ *
17
+ * What gets backed up:
18
+ * - settings.json (full file)
19
+ * - .claude.json mcpServers["claude-buddy"] block (only our entry)
20
+ * - skills/buddy/SKILL.md
21
+ * - <state-dir>/menagerie.json
22
+ * - <state-dir>/config.json
23
+ * - <state-dir>/status.json
24
+ * - <state-dir>/events.json
25
+ * - <state-dir>/unlocked.json
26
+ * - <state-dir>/active_days.json
4
27
  */
5
28
 
6
29
  import {
@@ -8,12 +31,18 @@ import {
8
31
  readdirSync, statSync, rmSync, copyFileSync,
9
32
  } from "fs";
10
33
  import { dirname, join } from "path";
11
- import { getBuddySkillDir, getBuddyStateDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
34
+ import {
35
+ buddyStateDir,
36
+ claudeSettingsPath,
37
+ claudeSkillDir,
38
+ claudeUserConfigPath,
39
+ } from "../server/path.ts";
12
40
 
13
- const SETTINGS = getClaudeSettingsPath();
14
- const CLAUDE_JSON = getClaudeJsonPath();
15
- const SKILL = join(getBuddySkillDir(), "SKILL.md");
16
- const STATE_DIR = getBuddyStateDir();
41
+ const SETTINGS = claudeSettingsPath();
42
+ const CLAUDE_JSON = claudeUserConfigPath();
43
+ const SKILL_DIR = claudeSkillDir("buddy");
44
+ const SKILL = join(SKILL_DIR, "SKILL.md");
45
+ const STATE_DIR = buddyStateDir();
17
46
  const BACKUPS_DIR = join(STATE_DIR, "backups");
18
47
 
19
48
  const RED = "\x1b[31m";
@@ -42,18 +71,21 @@ function tryRead(path: string): string | null {
42
71
  function listBackups(): string[] {
43
72
  if (!existsSync(BACKUPS_DIR)) return [];
44
73
  return readdirSync(BACKUPS_DIR)
45
- .filter((f) => /^\d{4}-\d{2}-\d{2}-\d{6}$/.test(f))
46
- .filter((f) => statSync(join(BACKUPS_DIR, f)).isDirectory())
74
+ .filter(f => /^\d{4}-\d{2}-\d{2}-\d{6}$/.test(f))
75
+ .filter(f => statSync(join(BACKUPS_DIR, f)).isDirectory())
47
76
  .sort();
48
77
  }
49
78
 
79
+ // ─── Create backup ──────────────────────────────────────────────────────────
80
+
50
81
  function createBackup(): string {
51
82
  const ts = timestamp();
52
83
  const dir = join(BACKUPS_DIR, ts);
53
84
  mkdirSync(dir, { recursive: true });
54
85
 
55
- const manifest: { timestamp: string; files: string[] } = { timestamp: ts, files: [] };
86
+ const manifest: Record<string, any> = { timestamp: ts, files: [] };
56
87
 
88
+ // 1. settings.json
57
89
  const settings = tryRead(SETTINGS);
58
90
  if (settings) {
59
91
  writeFileSync(join(dir, "settings.json"), settings);
@@ -63,6 +95,7 @@ function createBackup(): string {
63
95
  warn(`Skipped: ${SETTINGS} (not found)`);
64
96
  }
65
97
 
98
+ // 2. claude.json mcpServers["claude-buddy"]
66
99
  const claudeJsonRaw = tryRead(CLAUDE_JSON);
67
100
  if (claudeJsonRaw) {
68
101
  try {
@@ -80,6 +113,7 @@ function createBackup(): string {
80
113
  }
81
114
  }
82
115
 
116
+ // 3. SKILL.md
83
117
  const skill = tryRead(SKILL);
84
118
  if (skill) {
85
119
  writeFileSync(join(dir, "SKILL.md"), skill);
@@ -89,22 +123,26 @@ function createBackup(): string {
89
123
  warn(`Skipped: ${SKILL} (not found)`);
90
124
  }
91
125
 
126
+ // 4+. ~/.claude-buddy/ state files (don't back up the backups dir itself)
92
127
  const stateDestDir = join(dir, "claude-buddy");
93
128
  mkdirSync(stateDestDir, { recursive: true });
94
129
  const stateFiles = ["menagerie.json", "config.json", "status.json", "events.json", "unlocked.json", "active_days.json"];
95
- for (const file of stateFiles) {
96
- const src = join(STATE_DIR, file);
130
+ for (const f of stateFiles) {
131
+ const src = join(STATE_DIR, f);
97
132
  if (existsSync(src)) {
98
- copyFileSync(src, join(stateDestDir, file));
99
- manifest.files.push(`claude-buddy/${file}`);
100
- ok(`Backed up: ${join(STATE_DIR, file)}`);
133
+ copyFileSync(src, join(stateDestDir, f));
134
+ manifest.files.push(`claude-buddy/${f}`);
135
+ ok(`Backed up: ${join(STATE_DIR, f)}`);
101
136
  }
102
137
  }
103
138
 
104
139
  writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
140
+
105
141
  return ts;
106
142
  }
107
143
 
144
+ // ─── List backups ───────────────────────────────────────────────────────────
145
+
108
146
  function cmdList() {
109
147
  const backups = listBackups();
110
148
  if (backups.length === 0) {
@@ -112,13 +150,15 @@ function cmdList() {
112
150
  info(`Run '${BOLD}bun run backup${NC}' to create one.`);
113
151
  return;
114
152
  }
115
- console.log(`\n${BOLD}claude-buddy backups${NC}\n`);
153
+ console.log(`\n${BOLD}coding-buddy backups${NC}\n`);
116
154
  for (const ts of backups) {
117
155
  const manifestPath = join(BACKUPS_DIR, ts, "manifest.json");
118
156
  const manifest = tryRead(manifestPath);
119
157
  let count = "?";
120
158
  if (manifest) {
121
- try { count = String(JSON.parse(manifest).files?.length ?? 0); } catch {}
159
+ try {
160
+ count = String(JSON.parse(manifest).files?.length ?? 0);
161
+ } catch {}
122
162
  }
123
163
  const isLatest = ts === backups[backups.length - 1];
124
164
  const tag = isLatest ? `${GREEN}(latest)${NC}` : "";
@@ -127,6 +167,8 @@ function cmdList() {
127
167
  console.log("");
128
168
  }
129
169
 
170
+ // ─── Show backup contents ───────────────────────────────────────────────────
171
+
130
172
  function cmdShow(ts: string) {
131
173
  const dir = join(BACKUPS_DIR, ts);
132
174
  if (!existsSync(dir)) {
@@ -141,10 +183,14 @@ function cmdShow(ts: string) {
141
183
  const data = JSON.parse(manifest);
142
184
  console.log(`\n${BOLD}Backup ${ts}${NC}\n`);
143
185
  console.log(` ${DIM}Files:${NC}`);
144
- for (const file of data.files) console.log(` - ${file}`);
186
+ for (const f of data.files) {
187
+ console.log(` - ${f}`);
188
+ }
145
189
  console.log("");
146
190
  }
147
191
 
192
+ // ─── Restore backup ─────────────────────────────────────────────────────────
193
+
148
194
  function restoreBackup(ts: string) {
149
195
  const dir = join(BACKUPS_DIR, ts);
150
196
  if (!existsSync(dir)) {
@@ -154,6 +200,7 @@ function restoreBackup(ts: string) {
154
200
 
155
201
  info(`Restoring backup ${ts}...\n`);
156
202
 
203
+ // 1. settings.json — overwrite
157
204
  const settingsBak = join(dir, "settings.json");
158
205
  if (existsSync(settingsBak)) {
159
206
  mkdirSync(dirname(SETTINGS), { recursive: true });
@@ -161,39 +208,46 @@ function restoreBackup(ts: string) {
161
208
  ok(`Restored: ${SETTINGS}`);
162
209
  }
163
210
 
211
+ // 2. claude.json mcpServers — merge our entry back in
164
212
  const mcpBak = join(dir, "mcpserver.json");
165
213
  if (existsSync(mcpBak)) {
166
214
  const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
167
- let claudeJson: { mcpServers?: Record<string, unknown> } = {};
215
+ let claudeJson: Record<string, any> = {};
168
216
  try {
169
- claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8")) as { mcpServers?: Record<string, unknown> };
170
- } catch {}
217
+ claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
218
+ } catch { /* empty */ }
171
219
  if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
172
220
  claudeJson.mcpServers["claude-buddy"] = ourMcp;
221
+ // Under CLAUDE_CONFIG_DIR the parent dir is not guaranteed to exist
222
+ // if settings.json wasn't in this backup (and so step 1 was skipped).
173
223
  mkdirSync(dirname(CLAUDE_JSON), { recursive: true });
174
224
  writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
175
225
  ok(`Restored: ${CLAUDE_JSON} → mcpServers["claude-buddy"]`);
176
226
  }
177
227
 
228
+ // 3. SKILL.md
178
229
  const skillBak = join(dir, "SKILL.md");
179
230
  if (existsSync(skillBak)) {
180
- mkdirSync(dirname(SKILL), { recursive: true });
231
+ mkdirSync(SKILL_DIR, { recursive: true });
181
232
  copyFileSync(skillBak, SKILL);
182
233
  ok(`Restored: ${SKILL}`);
183
234
  }
184
235
 
236
+ // 4. ~/.claude-buddy/ state files
185
237
  const stateDir = join(dir, "claude-buddy");
186
238
  if (existsSync(stateDir)) {
187
239
  mkdirSync(STATE_DIR, { recursive: true });
188
- for (const file of readdirSync(stateDir)) {
189
- copyFileSync(join(stateDir, file), join(STATE_DIR, file));
190
- ok(`Restored: ${join(STATE_DIR, file)}`);
240
+ for (const f of readdirSync(stateDir)) {
241
+ copyFileSync(join(stateDir, f), join(STATE_DIR, f));
242
+ ok(`Restored: ${join(STATE_DIR, f)}`);
191
243
  }
192
244
  }
193
245
 
194
246
  console.log(`\n${GREEN}Restore complete.${NC} Restart Claude Code to apply.\n`);
195
247
  }
196
248
 
249
+ // ─── Delete backup ──────────────────────────────────────────────────────────
250
+
197
251
  function cmdDelete(ts: string) {
198
252
  const dir = join(BACKUPS_DIR, ts);
199
253
  if (!existsSync(dir)) {
@@ -204,12 +258,15 @@ function cmdDelete(ts: string) {
204
258
  ok(`Deleted backup ${ts}`);
205
259
  }
206
260
 
261
+ // ─── Main ───────────────────────────────────────────────────────────────────
262
+
207
263
  const action = process.argv[2] || "create";
208
264
  const arg = process.argv[3];
209
265
 
210
266
  switch (action) {
211
- case "create": {
212
- console.log(`\n${BOLD}Creating claude-buddy backup...${NC}\n`);
267
+ case "create":
268
+ case undefined: {
269
+ console.log(`\n${BOLD}Creating coding-buddy backup...${NC}\n`);
213
270
  const ts = createBackup();
214
271
  console.log(`\n${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
215
272
  console.log(`${GREEN} Backup created: ${ts}${NC}`);
@@ -219,37 +276,77 @@ switch (action) {
219
276
  console.log(`${DIM} Or: bun run backup restore ${ts}${NC}\n`);
220
277
  break;
221
278
  }
279
+
222
280
  case "list":
223
281
  case "ls":
224
282
  cmdList();
225
283
  break;
226
- case "show":
284
+
285
+ case "show": {
227
286
  if (!arg) {
228
287
  err("Usage: bun run backup show <timestamp>");
229
288
  process.exit(1);
230
289
  }
231
290
  cmdShow(arg);
232
291
  break;
292
+ }
293
+
233
294
  case "restore": {
234
- const backups = listBackups();
235
- const ts = arg ?? backups[backups.length - 1];
295
+ let ts = arg;
236
296
  if (!ts) {
237
- err("No backups found to restore");
238
- process.exit(1);
297
+ const all = listBackups();
298
+ if (all.length === 0) {
299
+ err("No backups to restore");
300
+ process.exit(1);
301
+ }
302
+ ts = all[all.length - 1];
303
+ info(`Restoring latest backup: ${ts}`);
239
304
  }
240
305
  restoreBackup(ts);
241
306
  break;
242
307
  }
308
+
243
309
  case "delete":
244
- case "rm":
310
+ case "rm": {
245
311
  if (!arg) {
246
312
  err("Usage: bun run backup delete <timestamp>");
247
313
  process.exit(1);
248
314
  }
249
315
  cmdDelete(arg);
250
316
  break;
317
+ }
318
+
319
+ case "--help":
320
+ case "-h":
321
+ console.log(`
322
+ ${BOLD}coding-buddy backup${NC} — snapshot and restore all coding-buddy state
323
+
324
+ ${BOLD}Commands:${NC}
325
+ bun run backup Create a new snapshot
326
+ bun run backup list List all backups
327
+ bun run backup show <ts> Show what's in a backup
328
+ bun run backup restore Restore the latest backup
329
+ bun run backup restore <ts> Restore a specific backup
330
+ bun run backup delete <ts> Delete a specific backup
331
+
332
+ ${BOLD}What gets backed up:${NC}
333
+ - ${SETTINGS}
334
+ - ${CLAUDE_JSON} mcpServers["claude-buddy"] (only our entry)
335
+ - ${SKILL}
336
+ - ${STATE_DIR}/menagerie.json
337
+ - ${STATE_DIR}/config.json
338
+ - ${STATE_DIR}/status.json
339
+ - ${STATE_DIR}/events.json
340
+ - ${STATE_DIR}/unlocked.json
341
+ - ${STATE_DIR}/active_days.json
342
+
343
+ ${BOLD}Backup location:${NC}
344
+ ${BACKUPS_DIR}/<timestamp>/
345
+ `);
346
+ break;
347
+
251
348
  default:
252
349
  err(`Unknown action: ${action}`);
253
- console.log("Usage: bun run backup [list|show <ts>|restore [ts]|delete <ts>]");
350
+ console.log(`Run 'bun run backup --help' for usage.`);
254
351
  process.exit(1);
255
352
  }
@@ -38,7 +38,7 @@ import { fileURLToPath } from "node:url";
38
38
 
39
39
  import { buddyStateDir } from "../server/path.ts";
40
40
  import { getArtFrame, HAT_ART } from "../server/art.ts";
41
- import type { Species, Eye, Hat } from "../server/engine.ts";
41
+ import type { Species, Eye, Hat } from "../core/engine.ts"
42
42
  import { getBiome, listBiomes } from "./biomes.ts";
43
43
  import xtermPkg from "@xterm/headless";
44
44
  import serializePkg from "@xterm/addon-serialize";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * claude-buddy disable — temporarily deactivate buddy without losing data
3
+ * coding-buddy disable — temporarily deactivate buddy without losing data
4
4
  *
5
5
  * Removes: MCP server, status line, hooks
6
6
  * Keeps: companion data, backups, skill files
@@ -8,28 +8,12 @@
8
8
  * Re-enable with: bun run install-buddy
9
9
  */
10
10
 
11
- import { readFileSync, writeFileSync } from "fs";
12
- import { getBuddyStateDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
13
-
14
- interface HookCommand {
15
- type: "command";
16
- command: string;
17
- }
18
-
19
- interface HookMatcherEntry {
20
- matcher?: string;
21
- hooks?: HookCommand[];
22
- }
23
-
24
- interface ClaudeSettings {
25
- statusLine?: { command?: string };
26
- hooks?: Record<string, HookMatcherEntry[]>;
27
- [key: string]: unknown;
28
- }
29
-
30
- function hasBuddyHook(entry: HookMatcherEntry): boolean {
31
- return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
32
- }
11
+ import { readFileSync, writeFileSync, existsSync } from "fs";
12
+ import {
13
+ buddyStateDir,
14
+ claudeSettingsPath,
15
+ claudeUserConfigPath,
16
+ } from "../server/path.ts";
33
17
 
34
18
  const GREEN = "\x1b[32m";
35
19
  const YELLOW = "\x1b[33m";
@@ -40,12 +24,13 @@ const NC = "\x1b[0m";
40
24
  function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
41
25
  function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
42
26
 
43
- const CLAUDE_JSON = getClaudeJsonPath();
44
- const SETTINGS = getClaudeSettingsPath();
45
- const STATE_DIR = getBuddyStateDir();
27
+ const CLAUDE_JSON = claudeUserConfigPath();
28
+ const SETTINGS = claudeSettingsPath();
29
+ const STATE_DIR = buddyStateDir();
46
30
 
47
- console.log(`\n${BOLD}Disabling claude-buddy...${NC}\n`);
31
+ console.log(`\n${BOLD}Disabling coding-buddy...${NC}\n`);
48
32
 
33
+ // 1. Remove MCP server from ~/.claude.json
49
34
  try {
50
35
  const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
51
36
  if (claudeJson.mcpServers?.["claude-buddy"]) {
@@ -60,8 +45,9 @@ try {
60
45
  warn(`Could not update ${CLAUDE_JSON}`);
61
46
  }
62
47
 
48
+ // 2. Remove status line + hooks from settings.json
63
49
  try {
64
- const settings = JSON.parse(readFileSync(SETTINGS, "utf8")) as ClaudeSettings;
50
+ const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
65
51
  let changed = false;
66
52
 
67
53
  if (settings.statusLine?.command?.includes("buddy")) {
@@ -74,7 +60,9 @@ try {
74
60
  for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd", "UserPromptSubmit"]) {
75
61
  if (settings.hooks[hookType]) {
76
62
  const before = settings.hooks[hookType].length;
77
- settings.hooks[hookType] = settings.hooks[hookType].filter((h) => !hasBuddyHook(h));
63
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
64
+ (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
65
+ );
78
66
  if (settings.hooks[hookType].length < before) changed = true;
79
67
  if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
80
68
  }
@@ -90,14 +78,13 @@ try {
90
78
  warn("Could not update settings.json");
91
79
  }
92
80
 
81
+ // 3. Stop tmux popup if running
93
82
  try {
94
83
  if (process.env.TMUX) {
95
84
  const { execSync } = await import("child_process");
96
85
  execSync("tmux display-popup -C 2>/dev/null", { stdio: "ignore" });
97
86
  }
98
- } catch {
99
- // noop
100
- }
87
+ } catch { /* not in tmux */ }
101
88
 
102
89
  console.log(`
103
90
  ${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
package/cli/doctor.ts ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * coding-buddy doctor — comprehensive diagnostic report
4
+ *
5
+ * Run: bun run doctor
6
+ *
7
+ * Outputs a complete environment report for bug reports.
8
+ * Copy the entire output and paste it in a GitHub issue.
9
+ */
10
+
11
+ import { readFileSync, existsSync, statSync } from "fs";
12
+ import { execSync } from "child_process";
13
+ import { join, resolve, dirname } from "path";
14
+ import {
15
+ buddyStateDir,
16
+ claudeConfigDir,
17
+ claudeSettingsPath,
18
+ claudeSkillDir,
19
+ claudeUserConfigPath,
20
+ } from "../server/path.ts";
21
+
22
+ const PROJECT_ROOT = resolve(dirname(import.meta.dir));
23
+ const CLAUDE_DIR = claudeConfigDir();
24
+ const CLAUDE_JSON = claudeUserConfigPath();
25
+ const STATE_DIR = buddyStateDir();
26
+ const SETTINGS = claudeSettingsPath();
27
+ const SKILL_DIR = claudeSkillDir("buddy");
28
+ const STATUS_SCRIPT = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
29
+
30
+ const RED = "\x1b[31m";
31
+ const GREEN = "\x1b[32m";
32
+ const YELLOW = "\x1b[33m";
33
+ const CYAN = "\x1b[36m";
34
+ const BOLD = "\x1b[1m";
35
+ const DIM = "\x1b[2m";
36
+ const NC = "\x1b[0m";
37
+
38
+ function section(title: string) {
39
+ console.log(`\n${CYAN}${BOLD}━━━ ${title} ${"━".repeat(Math.max(0, 60 - title.length))}${NC}`);
40
+ }
41
+
42
+ function row(label: string, value: string) {
43
+ console.log(` ${DIM}${label.padEnd(28)}${NC} ${value}`);
44
+ }
45
+
46
+ function ok(msg: string) { console.log(` ${GREEN}✓${NC} ${msg}`); }
47
+ function warn(msg: string) { console.log(` ${YELLOW}⚠${NC} ${msg}`); }
48
+ function err(msg: string) { console.log(` ${RED}✗${NC} ${msg}`); }
49
+
50
+ function tryExec(cmd: string, fallback = "(failed)"): string {
51
+ try {
52
+ return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
53
+ } catch {
54
+ return fallback;
55
+ }
56
+ }
57
+
58
+ function tryRead(path: string): string | null {
59
+ try { return readFileSync(path, "utf8"); } catch { return null; }
60
+ }
61
+
62
+ function tryParseJson(text: string | null): any | null {
63
+ if (!text) return null;
64
+ try { return JSON.parse(text); } catch { return null; }
65
+ }
66
+
67
+ // ─── Header ─────────────────────────────────────────────────────────────────
68
+
69
+ console.log(`${CYAN}${BOLD}
70
+ ╔══════════════════════════════════════════════════════════╗
71
+ ║ coding-buddy doctor — diagnostic report ║
72
+ ╚══════════════════════════════════════════════════════════╝${NC}`);
73
+
74
+ console.log(`\n${DIM}Copy this entire output into your GitHub issue.${NC}`);
75
+
76
+ // ─── Environment ────────────────────────────────────────────────────────────
77
+
78
+ section("Environment");
79
+ row("OS", tryExec("uname -srm"));
80
+ row("Hostname", tryExec("uname -n"));
81
+ row("User shell", process.env.SHELL ?? "(unset)");
82
+ row("Bash version", tryExec("bash --version | head -1"));
83
+ row("Bun version", tryExec("bun --version"));
84
+ row("Node version", tryExec("node --version", "(not installed)"));
85
+ row("jq version", tryExec("jq --version", "(not installed)"));
86
+ row("Claude Code version", tryExec("claude --version", "(not in PATH)"));
87
+
88
+ // ─── Terminal ───────────────────────────────────────────────────────────────
89
+
90
+ section("Terminal");
91
+ row("TERM", process.env.TERM ?? "(unset)");
92
+ row("COLORTERM", process.env.COLORTERM ?? "(unset)");
93
+ row("TERM_PROGRAM", process.env.TERM_PROGRAM ?? "(unset)");
94
+ row("LANG", process.env.LANG ?? "(unset)");
95
+ row("COLUMNS env var", process.env.COLUMNS ?? "(unset in subprocess)");
96
+ row("stty size", tryExec("stty size 2>/dev/null", "(no tty)"));
97
+ row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
98
+
99
+ // ─── Filesystem checks ──────────────────────────────────────────────────────
100
+
101
+ section("Filesystem");
102
+ const procExists = existsSync("/proc");
103
+ row("/proc exists", procExists ? `${GREEN}yes${NC} (Linux)` : `${RED}no${NC} (macOS/BSD)`);
104
+ row("CLAUDE_CONFIG_DIR", process.env.CLAUDE_CONFIG_DIR ?? "(unset)");
105
+ row(`${CLAUDE_DIR} exists`, existsSync(CLAUDE_DIR) ? "yes" : "no");
106
+ row(`${CLAUDE_JSON} exists`, existsSync(CLAUDE_JSON) ? "yes" : "no");
107
+ row(`${STATE_DIR} exists`, existsSync(STATE_DIR) ? "yes" : "no");
108
+ row("Project root", PROJECT_ROOT);
109
+ row("Status script exists", existsSync(STATUS_SCRIPT) ? "yes" : `${RED}no${NC}`);
110
+
111
+ // ─── coding-buddy state ─────────────────────────────────────────────────────
112
+
113
+ section("coding-buddy state");
114
+ const menagerie = tryParseJson(tryRead(join(STATE_DIR, "menagerie.json")));
115
+ const status = tryParseJson(tryRead(join(STATE_DIR, "status.json")));
116
+
117
+ if (menagerie) {
118
+ const activeSlot = menagerie.active ?? "buddy";
119
+ const companion = menagerie.companions?.[activeSlot];
120
+ row("Active slot", activeSlot);
121
+ row("Total slots", String(Object.keys(menagerie.companions ?? {}).length));
122
+ if (companion) {
123
+ row("Companion name", companion.name ?? "(none)");
124
+ row("Species", companion.bones?.species ?? "(none)");
125
+ row("Rarity", companion.bones?.rarity ?? "(none)");
126
+ row("Hat", companion.bones?.hat ?? "(none)");
127
+ row("Eye", companion.bones?.eye ?? "(none)");
128
+ row("Shiny", String(companion.bones?.shiny ?? false));
129
+ } else {
130
+ err(`No companion found in active slot "${activeSlot}"`);
131
+ }
132
+ } else {
133
+ err(`No manifest found at ${join(STATE_DIR, "menagerie.json")}`);
134
+ }
135
+
136
+ if (status) {
137
+ row("Status muted", String(status.muted ?? false));
138
+ row("Current reaction", status.reaction || "(none)");
139
+ } else {
140
+ warn(`No status state at ${join(STATE_DIR, "status.json")}`);
141
+ }
142
+
143
+ // ─── settings.json ──────────────────────────────────────────────────────────
144
+
145
+ section("Claude Code config");
146
+ const settings = tryParseJson(tryRead(SETTINGS));
147
+ const claudeJson = tryParseJson(tryRead(CLAUDE_JSON));
148
+
149
+ if (settings?.statusLine) {
150
+ console.log(` ${DIM}statusLine:${NC}`);
151
+ console.log(` ${JSON.stringify(settings.statusLine, null, 2).split("\n").join("\n ")}`);
152
+ } else {
153
+ warn(`No statusLine in ${SETTINGS}`);
154
+ }
155
+
156
+ if (settings?.hooks) {
157
+ console.log(` ${DIM}hooks:${NC}`);
158
+ for (const event of Object.keys(settings.hooks)) {
159
+ const count = settings.hooks[event]?.length ?? 0;
160
+ row(` ${event}`, `${count} entr${count === 1 ? "y" : "ies"}`);
161
+ }
162
+ } else {
163
+ warn("No hooks configured");
164
+ }
165
+
166
+ if (claudeJson?.mcpServers?.["claude-buddy"]) {
167
+ ok(`MCP server registered in ${CLAUDE_JSON}`);
168
+ console.log(` ${JSON.stringify(claudeJson.mcpServers["claude-buddy"], null, 2).split("\n").join("\n ")}`);
169
+ } else {
170
+ err(`MCP server NOT registered in ${CLAUDE_JSON}`);
171
+ }
172
+
173
+ const skillPath = join(SKILL_DIR, "SKILL.md");
174
+ if (existsSync(skillPath)) {
175
+ ok(`Skill installed: ${skillPath}`);
176
+ } else {
177
+ err(`Skill missing: ${skillPath}`);
178
+ }
179
+
180
+ // ─── Live status line test ──────────────────────────────────────────────────
181
+
182
+ section("Live status line output");
183
+ console.log(` ${DIM}(running: echo '{}' | ${STATUS_SCRIPT})${NC}\n`);
184
+ const liveOutput = tryExec(`echo '{}' | bash "${STATUS_SCRIPT}" 2>&1`, "(script failed)");
185
+ const lines = liveOutput.split("\n");
186
+ console.log(lines.map(l => ` │ ${l}`).join("\n"));
187
+ console.log(` ${DIM}(${lines.length} lines, total ${liveOutput.length} bytes)${NC}`);
188
+
189
+ // ─── Padding strategy test ──────────────────────────────────────────────────
190
+
191
+ section("Padding strategy test");
192
+ console.log(` ${DIM}Each row should appear right-aligned with marker '|END'.${NC}`);
193
+ console.log(` ${DIM}If a row is misaligned, that strategy is broken in this terminal.${NC}\n`);
194
+
195
+ const PAD = 40;
196
+ const strategies: [string, string][] = [
197
+ ["space (will be trimmed!)", " "],
198
+ ["braille blank U+2800", "\u2800"],
199
+ ["non-breaking space U+00A0", "\u00A0"],
200
+ ["dot .", "."],
201
+ ["middle dot ·", "\u00B7"],
202
+ ];
203
+
204
+ for (const [name, ch] of strategies) {
205
+ const padding = ch.repeat(PAD);
206
+ console.log(` ${padding}|END ${DIM}← ${name}${NC}`);
207
+ }
208
+
209
+ // ─── string-width comparison ────────────────────────────────────────────────
210
+
211
+ section("Display width vs string-width (npm)");
212
+ console.log(` ${DIM}Most terminals render Braille Blank as 2 columns.${NC}`);
213
+ console.log(` ${DIM}But the npm 'string-width' package counts it as 1.${NC}`);
214
+ console.log(` ${DIM}Claude Code uses string-width for layout calculations.${NC}\n`);
215
+
216
+ try {
217
+ // Try to load string-width if available
218
+ const sw = require("string-width");
219
+ row("string-width(' ')", String(sw(" ")));
220
+ row("string-width('\\u2800')", String(sw("\u2800")));
221
+ row("string-width('\\u00A0')", String(sw("\u00A0")));
222
+ row("string-width('-o-OO-o-')", String(sw("-o-OO-o-")));
223
+ row("string-width('⠀⠀⠀⠀⠀-o-OO-o-')", String(sw("\u2800\u2800\u2800\u2800\u2800-o-OO-o-")));
224
+ } catch {
225
+ warn("string-width not installed in project — skipping comparison");
226
+ }
227
+
228
+ // ─── Footer ─────────────────────────────────────────────────────────────────
229
+
230
+ console.log(`\n${CYAN}${BOLD}━━━ End of report ${"━".repeat(46)}${NC}\n`);
231
+ console.log(`${DIM}Copy everything above into your GitHub issue.${NC}\n`);