@ramarivera/coding-buddy 0.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +40 -0
- package/.claude-plugin/plugin.json +28 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/cli/backup.ts +336 -0
- package/cli/disable.ts +94 -0
- package/cli/doctor.ts +220 -0
- package/cli/hunt.ts +167 -0
- package/cli/index.ts +115 -0
- package/cli/install.ts +335 -0
- package/cli/pick.ts +492 -0
- package/cli/settings.ts +68 -0
- package/cli/show.ts +31 -0
- package/cli/test-statusline.sh +41 -0
- package/cli/test-statusline.ts +122 -0
- package/cli/uninstall.ts +110 -0
- package/cli/verify.ts +19 -0
- package/hooks/buddy-comment.sh +65 -0
- package/hooks/hooks.json +35 -0
- package/hooks/name-react.sh +176 -0
- package/hooks/react.sh +204 -0
- package/package.json +60 -0
- package/server/achievements.ts +445 -0
- package/server/art.ts +376 -0
- package/server/engine.ts +448 -0
- package/server/index.ts +774 -0
- package/server/reactions.ts +187 -0
- package/server/state.ts +409 -0
- package/skills/buddy/SKILL.md +59 -0
- package/statusline/buddy-status.sh +389 -0
package/cli/backup.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* claude-buddy backup — snapshot all claude-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 ~/.claude-buddy/backups/YYYY-MM-DD-HHMMSS/
|
|
14
|
+
*
|
|
15
|
+
* What gets backed up:
|
|
16
|
+
* - ~/.claude/settings.json (full file)
|
|
17
|
+
* - ~/.claude.json mcpServers["claude-buddy"] block (only our entry)
|
|
18
|
+
* - ~/.claude/skills/buddy/SKILL.md
|
|
19
|
+
* - ~/.claude-buddy/companion.json
|
|
20
|
+
* - ~/.claude-buddy/status.json
|
|
21
|
+
* - ~/.claude-buddy/reaction.json
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
readFileSync, writeFileSync, mkdirSync, existsSync,
|
|
26
|
+
readdirSync, statSync, rmSync, copyFileSync,
|
|
27
|
+
} from "fs";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
import { homedir } from "os";
|
|
30
|
+
|
|
31
|
+
const HOME = homedir();
|
|
32
|
+
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");
|
|
36
|
+
const STATE_DIR = join(HOME, ".claude-buddy");
|
|
37
|
+
|
|
38
|
+
const RED = "\x1b[31m";
|
|
39
|
+
const GREEN = "\x1b[32m";
|
|
40
|
+
const YELLOW = "\x1b[33m";
|
|
41
|
+
const CYAN = "\x1b[36m";
|
|
42
|
+
const BOLD = "\x1b[1m";
|
|
43
|
+
const DIM = "\x1b[2m";
|
|
44
|
+
const NC = "\x1b[0m";
|
|
45
|
+
|
|
46
|
+
function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
47
|
+
function info(msg: string) { console.log(`${CYAN}→${NC} ${msg}`); }
|
|
48
|
+
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
49
|
+
function err(msg: string) { console.log(`${RED}✗${NC} ${msg}`); }
|
|
50
|
+
|
|
51
|
+
function timestamp(): string {
|
|
52
|
+
const d = new Date();
|
|
53
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
54
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tryRead(path: string): string | null {
|
|
58
|
+
try { return readFileSync(path, "utf8"); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listBackups(): string[] {
|
|
62
|
+
if (!existsSync(BACKUPS_DIR)) return [];
|
|
63
|
+
return readdirSync(BACKUPS_DIR)
|
|
64
|
+
.filter(f => /^\d{4}-\d{2}-\d{2}-\d{6}$/.test(f))
|
|
65
|
+
.filter(f => statSync(join(BACKUPS_DIR, f)).isDirectory())
|
|
66
|
+
.sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Create backup ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function createBackup(): string {
|
|
72
|
+
const ts = timestamp();
|
|
73
|
+
const dir = join(BACKUPS_DIR, ts);
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
const manifest: Record<string, any> = { timestamp: ts, files: [] };
|
|
77
|
+
|
|
78
|
+
// 1. settings.json
|
|
79
|
+
const settings = tryRead(SETTINGS);
|
|
80
|
+
if (settings) {
|
|
81
|
+
writeFileSync(join(dir, "settings.json"), settings);
|
|
82
|
+
manifest.files.push("settings.json");
|
|
83
|
+
ok(`Backed up: ~/.claude/settings.json`);
|
|
84
|
+
} else {
|
|
85
|
+
warn(`Skipped: ~/.claude/settings.json (not found)`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. claude.json mcpServers["claude-buddy"]
|
|
89
|
+
const claudeJsonRaw = tryRead(CLAUDE_JSON);
|
|
90
|
+
if (claudeJsonRaw) {
|
|
91
|
+
try {
|
|
92
|
+
const claudeJson = JSON.parse(claudeJsonRaw);
|
|
93
|
+
const ourMcp = claudeJson.mcpServers?.["claude-buddy"];
|
|
94
|
+
if (ourMcp) {
|
|
95
|
+
writeFileSync(join(dir, "mcpserver.json"), JSON.stringify(ourMcp, null, 2));
|
|
96
|
+
manifest.files.push("mcpserver.json");
|
|
97
|
+
ok(`Backed up: ~/.claude.json → mcpServers["claude-buddy"]`);
|
|
98
|
+
} else {
|
|
99
|
+
warn(`Skipped: ~/.claude.json mcpServers["claude-buddy"] (not registered)`);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
err(`Failed to parse ~/.claude.json`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. SKILL.md
|
|
107
|
+
const skill = tryRead(SKILL);
|
|
108
|
+
if (skill) {
|
|
109
|
+
writeFileSync(join(dir, "SKILL.md"), skill);
|
|
110
|
+
manifest.files.push("SKILL.md");
|
|
111
|
+
ok(`Backed up: ~/.claude/skills/buddy/SKILL.md`);
|
|
112
|
+
} else {
|
|
113
|
+
warn(`Skipped: ~/.claude/skills/buddy/SKILL.md (not found)`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4-6. ~/.claude-buddy/ state files (don't back up the backups dir itself)
|
|
117
|
+
const stateDestDir = join(dir, "claude-buddy");
|
|
118
|
+
mkdirSync(stateDestDir, { recursive: true });
|
|
119
|
+
const stateFiles = ["companion.json", "status.json", "reaction.json"];
|
|
120
|
+
for (const f of stateFiles) {
|
|
121
|
+
const src = join(STATE_DIR, f);
|
|
122
|
+
if (existsSync(src)) {
|
|
123
|
+
copyFileSync(src, join(stateDestDir, f));
|
|
124
|
+
manifest.files.push(`claude-buddy/${f}`);
|
|
125
|
+
ok(`Backed up: ~/.claude-buddy/${f}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
130
|
+
|
|
131
|
+
return ts;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── List backups ───────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function cmdList() {
|
|
137
|
+
const backups = listBackups();
|
|
138
|
+
if (backups.length === 0) {
|
|
139
|
+
info("No backups found.");
|
|
140
|
+
info(`Run '${BOLD}bun run backup${NC}' to create one.`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log(`\n${BOLD}claude-buddy backups${NC}\n`);
|
|
144
|
+
for (const ts of backups) {
|
|
145
|
+
const manifestPath = join(BACKUPS_DIR, ts, "manifest.json");
|
|
146
|
+
const manifest = tryRead(manifestPath);
|
|
147
|
+
let count = "?";
|
|
148
|
+
if (manifest) {
|
|
149
|
+
try {
|
|
150
|
+
count = String(JSON.parse(manifest).files?.length ?? 0);
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
const isLatest = ts === backups[backups.length - 1];
|
|
154
|
+
const tag = isLatest ? `${GREEN}(latest)${NC}` : "";
|
|
155
|
+
console.log(` ${CYAN}${ts}${NC} ${DIM}${count} files${NC} ${tag}`);
|
|
156
|
+
}
|
|
157
|
+
console.log("");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Show backup contents ───────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
function cmdShow(ts: string) {
|
|
163
|
+
const dir = join(BACKUPS_DIR, ts);
|
|
164
|
+
if (!existsSync(dir)) {
|
|
165
|
+
err(`Backup not found: ${ts}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const manifest = tryRead(join(dir, "manifest.json"));
|
|
169
|
+
if (!manifest) {
|
|
170
|
+
err("manifest.json missing");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const data = JSON.parse(manifest);
|
|
174
|
+
console.log(`\n${BOLD}Backup ${ts}${NC}\n`);
|
|
175
|
+
console.log(` ${DIM}Files:${NC}`);
|
|
176
|
+
for (const f of data.files) {
|
|
177
|
+
console.log(` - ${f}`);
|
|
178
|
+
}
|
|
179
|
+
console.log("");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Restore backup ─────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function restoreBackup(ts: string) {
|
|
185
|
+
const dir = join(BACKUPS_DIR, ts);
|
|
186
|
+
if (!existsSync(dir)) {
|
|
187
|
+
err(`Backup not found: ${ts}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
info(`Restoring backup ${ts}...\n`);
|
|
192
|
+
|
|
193
|
+
// 1. settings.json — overwrite
|
|
194
|
+
const settingsBak = join(dir, "settings.json");
|
|
195
|
+
if (existsSync(settingsBak)) {
|
|
196
|
+
mkdirSync(join(HOME, ".claude"), { recursive: true });
|
|
197
|
+
copyFileSync(settingsBak, SETTINGS);
|
|
198
|
+
ok("Restored: ~/.claude/settings.json");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 2. claude.json mcpServers — merge our entry back in
|
|
202
|
+
const mcpBak = join(dir, "mcpserver.json");
|
|
203
|
+
if (existsSync(mcpBak)) {
|
|
204
|
+
const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
|
|
205
|
+
let claudeJson: Record<string, any> = {};
|
|
206
|
+
try {
|
|
207
|
+
claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
|
|
208
|
+
} catch { /* empty */ }
|
|
209
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
210
|
+
claudeJson.mcpServers["claude-buddy"] = ourMcp;
|
|
211
|
+
writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
|
|
212
|
+
ok("Restored: ~/.claude.json → mcpServers[\"claude-buddy\"]");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 3. SKILL.md
|
|
216
|
+
const skillBak = join(dir, "SKILL.md");
|
|
217
|
+
if (existsSync(skillBak)) {
|
|
218
|
+
mkdirSync(join(HOME, ".claude", "skills", "buddy"), { recursive: true });
|
|
219
|
+
copyFileSync(skillBak, SKILL);
|
|
220
|
+
ok("Restored: ~/.claude/skills/buddy/SKILL.md");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 4. ~/.claude-buddy/ state files
|
|
224
|
+
const stateDir = join(dir, "claude-buddy");
|
|
225
|
+
if (existsSync(stateDir)) {
|
|
226
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
227
|
+
for (const f of readdirSync(stateDir)) {
|
|
228
|
+
copyFileSync(join(stateDir, f), join(STATE_DIR, f));
|
|
229
|
+
ok(`Restored: ~/.claude-buddy/${f}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(`\n${GREEN}Restore complete.${NC} Restart Claude Code to apply.\n`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Delete backup ──────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function cmdDelete(ts: string) {
|
|
239
|
+
const dir = join(BACKUPS_DIR, ts);
|
|
240
|
+
if (!existsSync(dir)) {
|
|
241
|
+
err(`Backup not found: ${ts}`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
rmSync(dir, { recursive: true });
|
|
245
|
+
ok(`Deleted backup ${ts}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
const action = process.argv[2] || "create";
|
|
251
|
+
const arg = process.argv[3];
|
|
252
|
+
|
|
253
|
+
switch (action) {
|
|
254
|
+
case "create":
|
|
255
|
+
case undefined: {
|
|
256
|
+
console.log(`\n${BOLD}Creating claude-buddy backup...${NC}\n`);
|
|
257
|
+
const ts = createBackup();
|
|
258
|
+
console.log(`\n${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
|
|
259
|
+
console.log(`${GREEN} Backup created: ${ts}${NC}`);
|
|
260
|
+
console.log(`${GREEN} Location: ${BACKUPS_DIR}/${ts}${NC}`);
|
|
261
|
+
console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n`);
|
|
262
|
+
console.log(`${DIM} Restore with: bun run backup restore${NC}`);
|
|
263
|
+
console.log(`${DIM} Or: bun run backup restore ${ts}${NC}\n`);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
case "list":
|
|
268
|
+
case "ls":
|
|
269
|
+
cmdList();
|
|
270
|
+
break;
|
|
271
|
+
|
|
272
|
+
case "show": {
|
|
273
|
+
if (!arg) {
|
|
274
|
+
err("Usage: bun run backup show <timestamp>");
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
cmdShow(arg);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case "restore": {
|
|
282
|
+
let ts = arg;
|
|
283
|
+
if (!ts) {
|
|
284
|
+
const all = listBackups();
|
|
285
|
+
if (all.length === 0) {
|
|
286
|
+
err("No backups to restore");
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
ts = all[all.length - 1];
|
|
290
|
+
info(`Restoring latest backup: ${ts}`);
|
|
291
|
+
}
|
|
292
|
+
restoreBackup(ts);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case "delete":
|
|
297
|
+
case "rm": {
|
|
298
|
+
if (!arg) {
|
|
299
|
+
err("Usage: bun run backup delete <timestamp>");
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
cmdDelete(arg);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case "--help":
|
|
307
|
+
case "-h":
|
|
308
|
+
console.log(`
|
|
309
|
+
${BOLD}claude-buddy backup${NC} — snapshot and restore all claude-buddy state
|
|
310
|
+
|
|
311
|
+
${BOLD}Commands:${NC}
|
|
312
|
+
bun run backup Create a new snapshot
|
|
313
|
+
bun run backup list List all backups
|
|
314
|
+
bun run backup show <ts> Show what's in a backup
|
|
315
|
+
bun run backup restore Restore the latest backup
|
|
316
|
+
bun run backup restore <ts> Restore a specific backup
|
|
317
|
+
bun run backup delete <ts> Delete a specific backup
|
|
318
|
+
|
|
319
|
+
${BOLD}What gets backed up:${NC}
|
|
320
|
+
- ~/.claude/settings.json (full)
|
|
321
|
+
- ~/.claude.json mcpServers["claude-buddy"] (only our entry)
|
|
322
|
+
- ~/.claude/skills/buddy/SKILL.md
|
|
323
|
+
- ~/.claude-buddy/companion.json
|
|
324
|
+
- ~/.claude-buddy/status.json
|
|
325
|
+
- ~/.claude-buddy/reaction.json
|
|
326
|
+
|
|
327
|
+
${BOLD}Backup location:${NC}
|
|
328
|
+
${BACKUPS_DIR}/<timestamp>/
|
|
329
|
+
`);
|
|
330
|
+
break;
|
|
331
|
+
|
|
332
|
+
default:
|
|
333
|
+
err(`Unknown action: ${action}`);
|
|
334
|
+
console.log(`Run 'bun run backup --help' for usage.`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
package/cli/disable.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* claude-buddy disable — temporarily deactivate buddy without losing data
|
|
4
|
+
*
|
|
5
|
+
* Removes: MCP server, status line, hooks
|
|
6
|
+
* Keeps: companion data, backups, skill files
|
|
7
|
+
*
|
|
8
|
+
* Re-enable with: bun run install-buddy
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
|
|
15
|
+
const GREEN = "\x1b[32m";
|
|
16
|
+
const YELLOW = "\x1b[33m";
|
|
17
|
+
const BOLD = "\x1b[1m";
|
|
18
|
+
const DIM = "\x1b[2m";
|
|
19
|
+
const NC = "\x1b[0m";
|
|
20
|
+
|
|
21
|
+
function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
22
|
+
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
23
|
+
|
|
24
|
+
const HOME = homedir();
|
|
25
|
+
const CLAUDE_JSON = join(HOME, ".claude.json");
|
|
26
|
+
const SETTINGS = join(HOME, ".claude", "settings.json");
|
|
27
|
+
|
|
28
|
+
console.log(`\n${BOLD}Disabling claude-buddy...${NC}\n`);
|
|
29
|
+
|
|
30
|
+
// 1. Remove MCP server from ~/.claude.json
|
|
31
|
+
try {
|
|
32
|
+
const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
|
|
33
|
+
if (claudeJson.mcpServers?.["claude-buddy"]) {
|
|
34
|
+
delete claudeJson.mcpServers["claude-buddy"];
|
|
35
|
+
if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
|
|
36
|
+
writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
|
|
37
|
+
ok("MCP server removed from ~/.claude.json");
|
|
38
|
+
} else {
|
|
39
|
+
warn("MCP server was not registered");
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
warn("Could not update ~/.claude.json");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Remove status line + hooks from settings.json
|
|
46
|
+
try {
|
|
47
|
+
const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
|
|
48
|
+
let changed = false;
|
|
49
|
+
|
|
50
|
+
if (settings.statusLine?.command?.includes("buddy")) {
|
|
51
|
+
delete settings.statusLine;
|
|
52
|
+
ok("Status line removed");
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (settings.hooks) {
|
|
57
|
+
for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd"]) {
|
|
58
|
+
if (settings.hooks[hookType]) {
|
|
59
|
+
const before = settings.hooks[hookType].length;
|
|
60
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
61
|
+
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
|
|
62
|
+
);
|
|
63
|
+
if (settings.hooks[hookType].length < before) changed = true;
|
|
64
|
+
if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (changed) {
|
|
71
|
+
writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + "\n");
|
|
72
|
+
ok("Hooks and status line removed from settings.json");
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
warn("Could not update settings.json");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Stop tmux popup if running
|
|
79
|
+
try {
|
|
80
|
+
if (process.env.TMUX) {
|
|
81
|
+
const { execSync } = await import("child_process");
|
|
82
|
+
execSync("tmux display-popup -C 2>/dev/null", { stdio: "ignore" });
|
|
83
|
+
}
|
|
84
|
+
} catch { /* not in tmux */ }
|
|
85
|
+
|
|
86
|
+
console.log(`
|
|
87
|
+
${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
|
|
88
|
+
${GREEN} Buddy disabled.${NC}
|
|
89
|
+
${GREEN} Companion data is preserved at ~/.claude-buddy/${NC}
|
|
90
|
+
${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
|
|
91
|
+
|
|
92
|
+
${DIM} Restart Claude Code for changes to take effect.
|
|
93
|
+
Re-enable anytime with: bun run install-buddy${NC}
|
|
94
|
+
`);
|
package/cli/doctor.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* claude-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 { homedir } from "os";
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = resolve(dirname(import.meta.dir));
|
|
17
|
+
const HOME = homedir();
|
|
18
|
+
const STATUS_SCRIPT = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
|
|
19
|
+
|
|
20
|
+
const RED = "\x1b[31m";
|
|
21
|
+
const GREEN = "\x1b[32m";
|
|
22
|
+
const YELLOW = "\x1b[33m";
|
|
23
|
+
const CYAN = "\x1b[36m";
|
|
24
|
+
const BOLD = "\x1b[1m";
|
|
25
|
+
const DIM = "\x1b[2m";
|
|
26
|
+
const NC = "\x1b[0m";
|
|
27
|
+
|
|
28
|
+
function section(title: string) {
|
|
29
|
+
console.log(`\n${CYAN}${BOLD}━━━ ${title} ${"━".repeat(Math.max(0, 60 - title.length))}${NC}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function row(label: string, value: string) {
|
|
33
|
+
console.log(` ${DIM}${label.padEnd(28)}${NC} ${value}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ok(msg: string) { console.log(` ${GREEN}✓${NC} ${msg}`); }
|
|
37
|
+
function warn(msg: string) { console.log(` ${YELLOW}⚠${NC} ${msg}`); }
|
|
38
|
+
function err(msg: string) { console.log(` ${RED}✗${NC} ${msg}`); }
|
|
39
|
+
|
|
40
|
+
function tryExec(cmd: string, fallback = "(failed)"): string {
|
|
41
|
+
try {
|
|
42
|
+
return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
43
|
+
} catch {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tryRead(path: string): string | null {
|
|
49
|
+
try { return readFileSync(path, "utf8"); } catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tryParseJson(text: string | null): any | null {
|
|
53
|
+
if (!text) return null;
|
|
54
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Header ─────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
console.log(`${CYAN}${BOLD}
|
|
60
|
+
╔══════════════════════════════════════════════════════════╗
|
|
61
|
+
║ claude-buddy doctor — diagnostic report ║
|
|
62
|
+
╚══════════════════════════════════════════════════════════╝${NC}`);
|
|
63
|
+
|
|
64
|
+
console.log(`\n${DIM}Copy this entire output into your GitHub issue.${NC}`);
|
|
65
|
+
|
|
66
|
+
// ─── Environment ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
section("Environment");
|
|
69
|
+
row("OS", tryExec("uname -srm"));
|
|
70
|
+
row("Hostname", tryExec("uname -n"));
|
|
71
|
+
row("User shell", process.env.SHELL ?? "(unset)");
|
|
72
|
+
row("Bash version", tryExec("bash --version | head -1"));
|
|
73
|
+
row("Bun version", tryExec("bun --version"));
|
|
74
|
+
row("Node version", tryExec("node --version", "(not installed)"));
|
|
75
|
+
row("jq version", tryExec("jq --version", "(not installed)"));
|
|
76
|
+
row("Claude Code version", tryExec("claude --version", "(not in PATH)"));
|
|
77
|
+
|
|
78
|
+
// ─── Terminal ───────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
section("Terminal");
|
|
81
|
+
row("TERM", process.env.TERM ?? "(unset)");
|
|
82
|
+
row("COLORTERM", process.env.COLORTERM ?? "(unset)");
|
|
83
|
+
row("TERM_PROGRAM", process.env.TERM_PROGRAM ?? "(unset)");
|
|
84
|
+
row("LANG", process.env.LANG ?? "(unset)");
|
|
85
|
+
row("COLUMNS env var", process.env.COLUMNS ?? "(unset in subprocess)");
|
|
86
|
+
row("stty size", tryExec("stty size 2>/dev/null", "(no tty)"));
|
|
87
|
+
row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
|
|
88
|
+
|
|
89
|
+
// ─── Filesystem checks ──────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
section("Filesystem");
|
|
92
|
+
const procExists = existsSync("/proc");
|
|
93
|
+
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");
|
|
96
|
+
row("~/.claude-buddy/ exists", existsSync(join(HOME, ".claude-buddy")) ? "yes" : "no");
|
|
97
|
+
row("Project root", PROJECT_ROOT);
|
|
98
|
+
row("Status script exists", existsSync(STATUS_SCRIPT) ? "yes" : `${RED}no${NC}`);
|
|
99
|
+
|
|
100
|
+
// ─── claude-buddy state ─────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
section("claude-buddy state");
|
|
103
|
+
const menagerie = tryParseJson(tryRead(join(HOME, ".claude-buddy", "menagerie.json")));
|
|
104
|
+
const status = tryParseJson(tryRead(join(HOME, ".claude-buddy", "status.json")));
|
|
105
|
+
|
|
106
|
+
if (menagerie) {
|
|
107
|
+
const activeSlot = menagerie.active ?? "buddy";
|
|
108
|
+
const companion = menagerie.companions?.[activeSlot];
|
|
109
|
+
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));
|
|
118
|
+
} else {
|
|
119
|
+
err(`No companion found in active slot "${activeSlot}"`);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
err("No manifest found at ~/.claude-buddy/menagerie.json");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (status) {
|
|
126
|
+
row("Status muted", String(status.muted ?? false));
|
|
127
|
+
row("Current reaction", status.reaction || "(none)");
|
|
128
|
+
} else {
|
|
129
|
+
warn("No status state at ~/.claude-buddy/status.json");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── settings.json ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
section("Claude Code config");
|
|
135
|
+
const settings = tryParseJson(tryRead(join(HOME, ".claude", "settings.json")));
|
|
136
|
+
const claudeJson = tryParseJson(tryRead(join(HOME, ".claude.json")));
|
|
137
|
+
|
|
138
|
+
if (settings?.statusLine) {
|
|
139
|
+
console.log(` ${DIM}statusLine:${NC}`);
|
|
140
|
+
console.log(` ${JSON.stringify(settings.statusLine, null, 2).split("\n").join("\n ")}`);
|
|
141
|
+
} else {
|
|
142
|
+
warn("No statusLine in ~/.claude/settings.json");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (settings?.hooks) {
|
|
146
|
+
console.log(` ${DIM}hooks:${NC}`);
|
|
147
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
148
|
+
const count = settings.hooks[event]?.length ?? 0;
|
|
149
|
+
row(` ${event}`, `${count} entr${count === 1 ? "y" : "ies"}`);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
warn("No hooks configured");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (claudeJson?.mcpServers?.["claude-buddy"]) {
|
|
156
|
+
ok("MCP server registered in ~/.claude.json");
|
|
157
|
+
console.log(` ${JSON.stringify(claudeJson.mcpServers["claude-buddy"], null, 2).split("\n").join("\n ")}`);
|
|
158
|
+
} else {
|
|
159
|
+
err("MCP server NOT registered in ~/.claude.json");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const skillPath = join(HOME, ".claude", "skills", "buddy", "SKILL.md");
|
|
163
|
+
if (existsSync(skillPath)) {
|
|
164
|
+
ok(`Skill installed: ${skillPath}`);
|
|
165
|
+
} else {
|
|
166
|
+
err(`Skill missing: ${skillPath}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Live status line test ──────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
section("Live status line output");
|
|
172
|
+
console.log(` ${DIM}(running: echo '{}' | ${STATUS_SCRIPT})${NC}\n`);
|
|
173
|
+
const liveOutput = tryExec(`echo '{}' | bash "${STATUS_SCRIPT}" 2>&1`, "(script failed)");
|
|
174
|
+
const lines = liveOutput.split("\n");
|
|
175
|
+
console.log(lines.map(l => ` │ ${l}`).join("\n"));
|
|
176
|
+
console.log(` ${DIM}(${lines.length} lines, total ${liveOutput.length} bytes)${NC}`);
|
|
177
|
+
|
|
178
|
+
// ─── Padding strategy test ──────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
section("Padding strategy test");
|
|
181
|
+
console.log(` ${DIM}Each row should appear right-aligned with marker '|END'.${NC}`);
|
|
182
|
+
console.log(` ${DIM}If a row is misaligned, that strategy is broken in this terminal.${NC}\n`);
|
|
183
|
+
|
|
184
|
+
const PAD = 40;
|
|
185
|
+
const strategies: [string, string][] = [
|
|
186
|
+
["space (will be trimmed!)", " "],
|
|
187
|
+
["braille blank U+2800", "\u2800"],
|
|
188
|
+
["non-breaking space U+00A0", "\u00A0"],
|
|
189
|
+
["dot .", "."],
|
|
190
|
+
["middle dot ·", "\u00B7"],
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
for (const [name, ch] of strategies) {
|
|
194
|
+
const padding = ch.repeat(PAD);
|
|
195
|
+
console.log(` ${padding}|END ${DIM}← ${name}${NC}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── string-width comparison ────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
section("Display width vs string-width (npm)");
|
|
201
|
+
console.log(` ${DIM}Most terminals render Braille Blank as 2 columns.${NC}`);
|
|
202
|
+
console.log(` ${DIM}But the npm 'string-width' package counts it as 1.${NC}`);
|
|
203
|
+
console.log(` ${DIM}Claude Code uses string-width for layout calculations.${NC}\n`);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
// Try to load string-width if available
|
|
207
|
+
const sw = require("string-width");
|
|
208
|
+
row("string-width(' ')", String(sw(" ")));
|
|
209
|
+
row("string-width('\\u2800')", String(sw("\u2800")));
|
|
210
|
+
row("string-width('\\u00A0')", String(sw("\u00A0")));
|
|
211
|
+
row("string-width('-o-OO-o-')", String(sw("-o-OO-o-")));
|
|
212
|
+
row("string-width('⠀⠀⠀⠀⠀-o-OO-o-')", String(sw("\u2800\u2800\u2800\u2800\u2800-o-OO-o-")));
|
|
213
|
+
} catch {
|
|
214
|
+
warn("string-width not installed in project — skipping comparison");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Footer ─────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
console.log(`\n${CYAN}${BOLD}━━━ End of report ${"━".repeat(46)}${NC}\n`);
|
|
220
|
+
console.log(`${DIM}Copy everything above into your GitHub issue.${NC}\n`);
|