@ramarivera/coding-buddy 0.4.0-alpha.8 โ†’ 0.4.0-alpha.9

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 (36) hide show
  1. package/README.md +18 -39
  2. package/adapters/claude/hooks/buddy-comment.sh +4 -1
  3. package/adapters/claude/hooks/name-react.sh +4 -1
  4. package/adapters/claude/hooks/react.sh +4 -1
  5. package/adapters/claude/install/backup.ts +36 -118
  6. package/adapters/claude/install/disable.ts +9 -14
  7. package/adapters/claude/install/doctor.ts +26 -87
  8. package/adapters/claude/install/install.ts +39 -66
  9. package/adapters/claude/install/test-statusline.ts +8 -18
  10. package/adapters/claude/install/uninstall.ts +18 -26
  11. package/adapters/claude/plugin/marketplace.json +4 -4
  12. package/adapters/claude/plugin/plugin.json +3 -5
  13. package/adapters/claude/server/index.ts +132 -5
  14. package/adapters/claude/server/path.ts +12 -0
  15. package/adapters/claude/skills/buddy/SKILL.md +16 -1
  16. package/adapters/claude/statusline/buddy-status.sh +22 -3
  17. package/adapters/claude/storage/paths.ts +9 -0
  18. package/adapters/claude/storage/settings.ts +53 -3
  19. package/adapters/claude/storage/state.ts +22 -4
  20. package/cli/biomes.ts +309 -0
  21. package/cli/buddy-shell.ts +818 -0
  22. package/cli/index.ts +7 -0
  23. package/cli/tui.tsx +2244 -0
  24. package/cli/upgrade.ts +213 -0
  25. package/package.json +78 -63
  26. package/scripts/paths.sh +40 -0
  27. package/server/achievements.ts +15 -0
  28. package/server/art.ts +1 -0
  29. package/server/engine.ts +1 -0
  30. package/server/mcp-launcher.sh +16 -0
  31. package/server/path.ts +30 -0
  32. package/server/reactions.ts +1 -0
  33. package/server/state.ts +3 -0
  34. package/adapters/claude/popup/buddy-popup.sh +0 -92
  35. package/adapters/claude/popup/buddy-render.sh +0 -540
  36. package/adapters/claude/popup/popup-manager.sh +0 -355
package/README.md CHANGED
@@ -72,6 +72,12 @@
72
72
  <!-- QUICK START -->
73
73
  <!-- ============================================================ -->
74
74
 
75
+ ## ๐Ÿ“‹ Requirements
76
+
77
+ - **[bun](https://bun.sh/install)** on `PATH` โ€” claude-buddy's MCP server runs on bun. Install once: `curl -fsSL https://bun.sh/install | bash`
78
+ - **Claude Code v2.1.80+**
79
+ - **Linux or macOS** (Windows is experimental)
80
+
75
81
  ## ๐Ÿš€ Quick Start
76
82
 
77
83
  ```bash
@@ -83,12 +89,21 @@ bun run install-buddy
83
89
 
84
90
  Then restart Claude Code and type `/buddy`. That's it.
85
91
 
86
- <sub>๐Ÿ’ก Need Bun? โ†’ `curl -fsSL https://bun.sh/install | bash`</sub>
87
- <br>
88
92
  <sub>๐Ÿ’ก Want a global `claude-buddy` command? โ†’ `bun link`</sub>
89
93
  <br>
90
94
  <sub>๐Ÿ’ก Need help? โ†’ `bun run help` or `claude-buddy help` (if linked) in terminal ยท `/buddy help` in Claude Code</sub>
91
95
 
96
+ ### Multiple Claude profiles?
97
+
98
+ If you run Claude Code with `CLAUDE_CONFIG_DIR` set (e.g. separate work and personal accounts), pass the same env var to install so buddy lands in the active profile and gets its own menagerie:
99
+
100
+ ```bash
101
+ CLAUDE_CONFIG_DIR=~/.claude-personal bun run install-buddy
102
+ CLAUDE_CONFIG_DIR=~/.claude-personal bun run uninstall
103
+ ```
104
+
105
+ The installer prints `Target profile: <path>` at the top so you can see at a glance which profile you're targeting. Each profile gets its own MCP entry, skill, hooks, status line, and `$CLAUDE_CONFIG_DIR/buddy-state/` menagerie โ€” installs in one profile don't touch another. With `CLAUDE_CONFIG_DIR` unset, behaviour is identical to single-profile (`~/.claude/`, `~/.claude-buddy/`).
106
+
92
107
  <br>
93
108
 
94
109
  ---
@@ -338,41 +353,6 @@ bun run cli/uninstall.ts # full clean removal
338
353
 
339
354
  ---
340
355
 
341
- <details>
342
- <summary><b>๐Ÿ–ฅ๏ธ &nbsp; tmux Popup Mode</b></summary>
343
-
344
- <br>
345
-
346
- Inside tmux, buddy appears as a floating popup overlay in the bottom-right corner instead of the status line.
347
-
348
- **Features:** animated ASCII art with speech bubbles ยท ESC passthrough ยท dynamic resizing ยท full keyboard forwarding
349
-
350
- | tmux version | Support |
351
- |---|---|
352
- | **3.4+** | Full support (borderless) |
353
- | **3.2 โ€“ 3.3** | Supported with border |
354
- | **< 3.2** | Falls back to status line |
355
-
356
- ### Recommended `~/.tmux.conf`
357
-
358
- ```
359
- set -g set-titles on
360
- set -g set-titles-string "#{pane_title}"
361
- set -g mouse on
362
- set -g history-limit 10000
363
- ```
364
-
365
- ### Scrolling
366
-
367
- The popup is modal. To scroll:
368
- 1. Press **F12** โ†’ scroll mode (popup hides, copy-mode activates)
369
- 2. Scroll with **mouse wheel** or **arrows**
370
- 3. Press **q** โ†’ exit scroll mode
371
-
372
- </details>
373
-
374
- ---
375
-
376
356
  <details>
377
357
  <summary><b>๐Ÿ“‹ &nbsp; Requirements</b></summary>
378
358
 
@@ -382,7 +362,7 @@ The popup is modal. To scroll:
382
362
  |---|---|
383
363
  | **[Bun](https://bun.sh)** | `curl -fsSL https://bun.sh/install \| bash` |
384
364
  | **Claude Code** v2.1.80+ | Any version with MCP support |
385
- | **jq** | `apt install jq` / `brew install jq` |
365
+ | **jq** | `apt install jq` / `brew install jq` / [`windows: download and add 'jq.exe' from jqlang/jq to path`](https://github.com/jqlang/jq/releases/latest)|
386
366
 
387
367
  > **Will I get the same buddy I had?** Yes. claude-buddy uses the exact same algorithm as the original (`wyhash + mulberry32`, same salt, same identity resolution). If your `~/.claude.json` still has your `accountUuid`, you'll get the identical species, rarity, stats, and cosmetics.
388
368
 
@@ -395,7 +375,6 @@ The popup is modal. To scroll:
395
375
  ## ๐Ÿ—บ๏ธ Roadmap
396
376
 
397
377
  - [x] **Multi-buddy support** โ€” menagerie system with named slots, interactive TUI picker ๐Ÿ’œ[@doctor-ew](https://github.com/doctor-ew)๐Ÿ’œ
398
- - [x] **tmux popup mode** โ€” floating overlay via `tmux display-popup` ๐Ÿ’œ[@gzenz](https://github.com/gzenz)๐Ÿ’œ
399
378
  - [ ] **Leveling system** โ€” XP from coding sessions, unlockable reactions and upgrades
400
379
  - [ ] **Buddy pair-programming** โ€” active help during sessions, pattern detection
401
380
  - [ ] **Cross-session memory** โ€” remembers past projects and earlier conversations
@@ -5,7 +5,10 @@
5
5
  # This hook extracts it and updates the status line bubble.
6
6
  # The HTML comment is invisible in rendered markdown output.
7
7
 
8
- STATE_DIR="$HOME/.claude-buddy"
8
+ # shellcheck source=../scripts/paths.sh
9
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
10
+
11
+ STATE_DIR="$BUDDY_STATE_DIR"
9
12
  # Session ID: sanitized tmux pane number, or "default" outside tmux
10
13
  SID="${TMUX_PANE#%}"
11
14
  SID="${SID:-default}"
@@ -3,7 +3,10 @@
3
3
  # Detects the buddy's name in the user's message โ†’ status line reaction.
4
4
  # No cooldown โ€” name mentions are intentional.
5
5
 
6
- STATE_DIR="$HOME/.claude-buddy"
6
+ # shellcheck source=../scripts/paths.sh
7
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
8
+
9
+ STATE_DIR="$BUDDY_STATE_DIR"
7
10
  STATUS_FILE="$STATE_DIR/status.json"
8
11
  # Session ID: sanitized tmux pane number, or "default" outside tmux
9
12
  SID="${TMUX_PANE#%}"
@@ -4,7 +4,10 @@
4
4
  #
5
5
  # Combined: PR #4 species reactions + PR #6 session isolation + PR #13 field fix
6
6
 
7
- STATE_DIR="$HOME/.claude-buddy"
7
+ # shellcheck source=../scripts/paths.sh
8
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
9
+
10
+ STATE_DIR="$BUDDY_STATE_DIR"
8
11
  # Session ID: sanitized tmux pane number, or "default" outside tmux
9
12
  SID="${TMUX_PANE#%}"
10
13
  SID="${SID:-default}"
@@ -1,24 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
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
4
  */
23
5
 
24
6
  import {
@@ -26,15 +8,13 @@ import {
26
8
  readdirSync, statSync, rmSync, copyFileSync,
27
9
  } from "fs";
28
10
  import { dirname, join } from "path";
29
- import { homedir } from "os";
30
- import { getBuddySkillDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
11
+ import { getBuddySkillDir, getBuddyStateDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
31
12
 
32
- const HOME = homedir();
33
- const BACKUPS_DIR = join(HOME, ".claude-buddy", "backups");
34
13
  const SETTINGS = getClaudeSettingsPath();
35
14
  const CLAUDE_JSON = getClaudeJsonPath();
36
15
  const SKILL = join(getBuddySkillDir(), "SKILL.md");
37
- const STATE_DIR = join(HOME, ".claude-buddy");
16
+ const STATE_DIR = getBuddyStateDir();
17
+ const BACKUPS_DIR = join(STATE_DIR, "backups");
38
18
 
39
19
  const RED = "\x1b[31m";
40
20
  const GREEN = "\x1b[32m";
@@ -62,13 +42,11 @@ function tryRead(path: string): string | null {
62
42
  function listBackups(): string[] {
63
43
  if (!existsSync(BACKUPS_DIR)) return [];
64
44
  return readdirSync(BACKUPS_DIR)
65
- .filter(f => /^\d{4}-\d{2}-\d{2}-\d{6}$/.test(f))
66
- .filter(f => statSync(join(BACKUPS_DIR, f)).isDirectory())
45
+ .filter((f) => /^\d{4}-\d{2}-\d{2}-\d{6}$/.test(f))
46
+ .filter((f) => statSync(join(BACKUPS_DIR, f)).isDirectory())
67
47
  .sort();
68
48
  }
69
49
 
70
- // โ”€โ”€โ”€ Create backup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
71
-
72
50
  function createBackup(): string {
73
51
  const ts = timestamp();
74
52
  const dir = join(BACKUPS_DIR, ts);
@@ -76,17 +54,15 @@ function createBackup(): string {
76
54
 
77
55
  const manifest: { timestamp: string; files: string[] } = { timestamp: ts, files: [] };
78
56
 
79
- // 1. settings.json
80
57
  const settings = tryRead(SETTINGS);
81
58
  if (settings) {
82
59
  writeFileSync(join(dir, "settings.json"), settings);
83
60
  manifest.files.push("settings.json");
84
- ok(`Backed up: ~/.claude/settings.json`);
61
+ ok(`Backed up: ${SETTINGS}`);
85
62
  } else {
86
- warn(`Skipped: ~/.claude/settings.json (not found)`);
63
+ warn(`Skipped: ${SETTINGS} (not found)`);
87
64
  }
88
65
 
89
- // 2. claude.json mcpServers["claude-buddy"]
90
66
  const claudeJsonRaw = tryRead(CLAUDE_JSON);
91
67
  if (claudeJsonRaw) {
92
68
  try {
@@ -95,45 +71,40 @@ function createBackup(): string {
95
71
  if (ourMcp) {
96
72
  writeFileSync(join(dir, "mcpserver.json"), JSON.stringify(ourMcp, null, 2));
97
73
  manifest.files.push("mcpserver.json");
98
- ok(`Backed up: ~/.claude.json โ†’ mcpServers["claude-buddy"]`);
74
+ ok(`Backed up: ${CLAUDE_JSON} โ†’ mcpServers["claude-buddy"]`);
99
75
  } else {
100
- warn(`Skipped: ~/.claude.json mcpServers["claude-buddy"] (not registered)`);
76
+ warn(`Skipped: ${CLAUDE_JSON} mcpServers["claude-buddy"] (not registered)`);
101
77
  }
102
78
  } catch {
103
- err(`Failed to parse ~/.claude.json`);
79
+ err(`Failed to parse ${CLAUDE_JSON}`);
104
80
  }
105
81
  }
106
82
 
107
- // 3. SKILL.md
108
83
  const skill = tryRead(SKILL);
109
84
  if (skill) {
110
85
  writeFileSync(join(dir, "SKILL.md"), skill);
111
86
  manifest.files.push("SKILL.md");
112
- ok(`Backed up: ~/.claude/skills/buddy/SKILL.md`);
87
+ ok(`Backed up: ${SKILL}`);
113
88
  } else {
114
- warn(`Skipped: ~/.claude/skills/buddy/SKILL.md (not found)`);
89
+ warn(`Skipped: ${SKILL} (not found)`);
115
90
  }
116
91
 
117
- // 4-6. ~/.claude-buddy/ state files (don't back up the backups dir itself)
118
92
  const stateDestDir = join(dir, "claude-buddy");
119
93
  mkdirSync(stateDestDir, { recursive: true });
120
- const stateFiles = ["companion.json", "status.json", "reaction.json"];
121
- for (const f of stateFiles) {
122
- const src = join(STATE_DIR, f);
94
+ 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);
123
97
  if (existsSync(src)) {
124
- copyFileSync(src, join(stateDestDir, f));
125
- manifest.files.push(`claude-buddy/${f}`);
126
- ok(`Backed up: ~/.claude-buddy/${f}`);
98
+ copyFileSync(src, join(stateDestDir, file));
99
+ manifest.files.push(`claude-buddy/${file}`);
100
+ ok(`Backed up: ${join(STATE_DIR, file)}`);
127
101
  }
128
102
  }
129
103
 
130
104
  writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
131
-
132
105
  return ts;
133
106
  }
134
107
 
135
- // โ”€โ”€โ”€ List backups โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
136
-
137
108
  function cmdList() {
138
109
  const backups = listBackups();
139
110
  if (backups.length === 0) {
@@ -147,9 +118,7 @@ function cmdList() {
147
118
  const manifest = tryRead(manifestPath);
148
119
  let count = "?";
149
120
  if (manifest) {
150
- try {
151
- count = String(JSON.parse(manifest).files?.length ?? 0);
152
- } catch {}
121
+ try { count = String(JSON.parse(manifest).files?.length ?? 0); } catch {}
153
122
  }
154
123
  const isLatest = ts === backups[backups.length - 1];
155
124
  const tag = isLatest ? `${GREEN}(latest)${NC}` : "";
@@ -158,8 +127,6 @@ function cmdList() {
158
127
  console.log("");
159
128
  }
160
129
 
161
- // โ”€โ”€โ”€ Show backup contents โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
162
-
163
130
  function cmdShow(ts: string) {
164
131
  const dir = join(BACKUPS_DIR, ts);
165
132
  if (!existsSync(dir)) {
@@ -174,14 +141,10 @@ function cmdShow(ts: string) {
174
141
  const data = JSON.parse(manifest);
175
142
  console.log(`\n${BOLD}Backup ${ts}${NC}\n`);
176
143
  console.log(` ${DIM}Files:${NC}`);
177
- for (const f of data.files) {
178
- console.log(` - ${f}`);
179
- }
144
+ for (const file of data.files) console.log(` - ${file}`);
180
145
  console.log("");
181
146
  }
182
147
 
183
- // โ”€โ”€โ”€ Restore backup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
184
-
185
148
  function restoreBackup(ts: string) {
186
149
  const dir = join(BACKUPS_DIR, ts);
187
150
  if (!existsSync(dir)) {
@@ -191,51 +154,46 @@ function restoreBackup(ts: string) {
191
154
 
192
155
  info(`Restoring backup ${ts}...\n`);
193
156
 
194
- // 1. settings.json โ€” overwrite
195
157
  const settingsBak = join(dir, "settings.json");
196
158
  if (existsSync(settingsBak)) {
197
159
  mkdirSync(dirname(SETTINGS), { recursive: true });
198
160
  copyFileSync(settingsBak, SETTINGS);
199
- ok("Restored: ~/.claude/settings.json");
161
+ ok(`Restored: ${SETTINGS}`);
200
162
  }
201
163
 
202
- // 2. claude.json mcpServers โ€” merge our entry back in
203
164
  const mcpBak = join(dir, "mcpserver.json");
204
165
  if (existsSync(mcpBak)) {
205
166
  const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
206
167
  let claudeJson: { mcpServers?: Record<string, unknown> } = {};
207
168
  try {
208
169
  claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8")) as { mcpServers?: Record<string, unknown> };
209
- } catch { /* empty */ }
170
+ } catch {}
210
171
  if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
211
172
  claudeJson.mcpServers["claude-buddy"] = ourMcp;
173
+ mkdirSync(dirname(CLAUDE_JSON), { recursive: true });
212
174
  writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
213
- ok("Restored: ~/.claude.json โ†’ mcpServers[\"claude-buddy\"]");
175
+ ok(`Restored: ${CLAUDE_JSON} โ†’ mcpServers["claude-buddy"]`);
214
176
  }
215
177
 
216
- // 3. SKILL.md
217
178
  const skillBak = join(dir, "SKILL.md");
218
179
  if (existsSync(skillBak)) {
219
180
  mkdirSync(dirname(SKILL), { recursive: true });
220
181
  copyFileSync(skillBak, SKILL);
221
- ok("Restored: ~/.claude/skills/buddy/SKILL.md");
182
+ ok(`Restored: ${SKILL}`);
222
183
  }
223
184
 
224
- // 4. ~/.claude-buddy/ state files
225
185
  const stateDir = join(dir, "claude-buddy");
226
186
  if (existsSync(stateDir)) {
227
187
  mkdirSync(STATE_DIR, { recursive: true });
228
- for (const f of readdirSync(stateDir)) {
229
- copyFileSync(join(stateDir, f), join(STATE_DIR, f));
230
- ok(`Restored: ~/.claude-buddy/${f}`);
188
+ for (const file of readdirSync(stateDir)) {
189
+ copyFileSync(join(stateDir, file), join(STATE_DIR, file));
190
+ ok(`Restored: ${join(STATE_DIR, file)}`);
231
191
  }
232
192
  }
233
193
 
234
194
  console.log(`\n${GREEN}Restore complete.${NC} Restart Claude Code to apply.\n`);
235
195
  }
236
196
 
237
- // โ”€โ”€โ”€ Delete backup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
238
-
239
197
  function cmdDelete(ts: string) {
240
198
  const dir = join(BACKUPS_DIR, ts);
241
199
  if (!existsSync(dir)) {
@@ -246,14 +204,11 @@ function cmdDelete(ts: string) {
246
204
  ok(`Deleted backup ${ts}`);
247
205
  }
248
206
 
249
- // โ”€โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
250
-
251
207
  const action = process.argv[2] || "create";
252
208
  const arg = process.argv[3];
253
209
 
254
210
  switch (action) {
255
- case "create":
256
- case undefined: {
211
+ case "create": {
257
212
  console.log(`\n${BOLD}Creating claude-buddy backup...${NC}\n`);
258
213
  const ts = createBackup();
259
214
  console.log(`\n${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}`);
@@ -264,74 +219,37 @@ switch (action) {
264
219
  console.log(`${DIM} Or: bun run backup restore ${ts}${NC}\n`);
265
220
  break;
266
221
  }
267
-
268
222
  case "list":
269
223
  case "ls":
270
224
  cmdList();
271
225
  break;
272
-
273
- case "show": {
226
+ case "show":
274
227
  if (!arg) {
275
228
  err("Usage: bun run backup show <timestamp>");
276
229
  process.exit(1);
277
230
  }
278
231
  cmdShow(arg);
279
232
  break;
280
- }
281
-
282
233
  case "restore": {
283
- let ts = arg;
234
+ const backups = listBackups();
235
+ const ts = arg ?? backups[backups.length - 1];
284
236
  if (!ts) {
285
- const all = listBackups();
286
- if (all.length === 0) {
287
- err("No backups to restore");
288
- process.exit(1);
289
- }
290
- ts = all[all.length - 1];
291
- info(`Restoring latest backup: ${ts}`);
237
+ err("No backups found to restore");
238
+ process.exit(1);
292
239
  }
293
240
  restoreBackup(ts);
294
241
  break;
295
242
  }
296
-
297
243
  case "delete":
298
- case "rm": {
244
+ case "rm":
299
245
  if (!arg) {
300
246
  err("Usage: bun run backup delete <timestamp>");
301
247
  process.exit(1);
302
248
  }
303
249
  cmdDelete(arg);
304
250
  break;
305
- }
306
-
307
- case "--help":
308
- case "-h":
309
- console.log(`
310
- ${BOLD}claude-buddy backup${NC} โ€” snapshot and restore all claude-buddy state
311
-
312
- ${BOLD}Commands:${NC}
313
- bun run backup Create a new snapshot
314
- bun run backup list List all backups
315
- bun run backup show <ts> Show what's in a backup
316
- bun run backup restore Restore the latest backup
317
- bun run backup restore <ts> Restore a specific backup
318
- bun run backup delete <ts> Delete a specific backup
319
-
320
- ${BOLD}What gets backed up:${NC}
321
- - ~/.claude/settings.json (full)
322
- - ~/.claude.json mcpServers["claude-buddy"] (only our entry)
323
- - ~/.claude/skills/buddy/SKILL.md
324
- - ~/.claude-buddy/companion.json
325
- - ~/.claude-buddy/status.json
326
- - ~/.claude-buddy/reaction.json
327
-
328
- ${BOLD}Backup location:${NC}
329
- ${BACKUPS_DIR}/<timestamp>/
330
- `);
331
- break;
332
-
333
251
  default:
334
252
  err(`Unknown action: ${action}`);
335
- console.log(`Run 'bun run backup --help' for usage.`);
253
+ console.log("Usage: bun run backup [list|show <ts>|restore [ts]|delete <ts>]");
336
254
  process.exit(1);
337
255
  }
@@ -8,10 +8,8 @@
8
8
  * Re-enable with: bun run install-buddy
9
9
  */
10
10
 
11
- import { readFileSync, writeFileSync, existsSync } from "fs";
12
- import { join } from "path";
13
- import { homedir } from "os";
14
- import { getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
11
+ import { readFileSync, writeFileSync } from "fs";
12
+ import { getBuddyStateDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
15
13
 
16
14
  interface HookCommand {
17
15
  type: "command";
@@ -42,13 +40,12 @@ const NC = "\x1b[0m";
42
40
  function ok(msg: string) { console.log(`${GREEN}โœ“${NC} ${msg}`); }
43
41
  function warn(msg: string) { console.log(`${YELLOW}โš ${NC} ${msg}`); }
44
42
 
45
- const HOME = homedir();
46
43
  const CLAUDE_JSON = getClaudeJsonPath();
47
44
  const SETTINGS = getClaudeSettingsPath();
45
+ const STATE_DIR = getBuddyStateDir();
48
46
 
49
47
  console.log(`\n${BOLD}Disabling claude-buddy...${NC}\n`);
50
48
 
51
- // 1. Remove MCP server from ~/.claude.json
52
49
  try {
53
50
  const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
54
51
  if (claudeJson.mcpServers?.["claude-buddy"]) {
@@ -63,7 +60,6 @@ try {
63
60
  warn(`Could not update ${CLAUDE_JSON}`);
64
61
  }
65
62
 
66
- // 2. Remove status line + hooks from settings.json
67
63
  try {
68
64
  const settings = JSON.parse(readFileSync(SETTINGS, "utf8")) as ClaudeSettings;
69
65
  let changed = false;
@@ -75,12 +71,10 @@ try {
75
71
  }
76
72
 
77
73
  if (settings.hooks) {
78
- for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd"]) {
74
+ for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd", "UserPromptSubmit"]) {
79
75
  if (settings.hooks[hookType]) {
80
76
  const before = settings.hooks[hookType].length;
81
- settings.hooks[hookType] = settings.hooks[hookType].filter(
82
- (h) => !hasBuddyHook(h),
83
- );
77
+ settings.hooks[hookType] = settings.hooks[hookType].filter((h) => !hasBuddyHook(h));
84
78
  if (settings.hooks[hookType].length < before) changed = true;
85
79
  if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
86
80
  }
@@ -96,18 +90,19 @@ try {
96
90
  warn("Could not update settings.json");
97
91
  }
98
92
 
99
- // 3. Stop tmux popup if running
100
93
  try {
101
94
  if (process.env.TMUX) {
102
95
  const { execSync } = await import("child_process");
103
96
  execSync("tmux display-popup -C 2>/dev/null", { stdio: "ignore" });
104
97
  }
105
- } catch { /* not in tmux */ }
98
+ } catch {
99
+ // noop
100
+ }
106
101
 
107
102
  console.log(`
108
103
  ${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}
109
104
  ${GREEN} Buddy disabled.${NC}
110
- ${GREEN} Companion data is preserved at ~/.claude-buddy/${NC}
105
+ ${GREEN} Companion data is preserved at ${STATE_DIR}${NC}
111
106
  ${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}
112
107
 
113
108
  ${DIM} Restart Claude Code for changes to take effect.