@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.7.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 (94) hide show
  1. package/.claude-plugin/marketplace.json +45 -0
  2. package/.claude-plugin/plugin.json +30 -0
  3. package/LICENSE +1 -1
  4. package/README.md +96 -45
  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 +136 -57
  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 +46 -13
  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 +22 -9
  46. package/hooks/file-type-react.sh +403 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +210 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
  50. package/hooks/react.sh +1364 -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 +33 -2
  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 +552 -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 +402 -0
  74. package/statusline/combined-status.sh +135 -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/plugin/marketplace.json +0 -40
  80. package/adapters/claude/plugin/plugin.json +0 -26
  81. package/adapters/claude/rendering/art.ts +0 -264
  82. package/adapters/claude/server/instructions.ts +0 -24
  83. package/adapters/claude/server/path.ts +0 -12
  84. package/adapters/claude/server/resources.ts +0 -38
  85. package/adapters/claude/statusline/buddy-status.sh +0 -408
  86. package/adapters/claude/storage/achievements.ts +0 -253
  87. package/adapters/claude/storage/identity.ts +0 -13
  88. package/adapters/claude/storage/paths.ts +0 -33
  89. package/adapters/claude/storage/settings.ts +0 -91
  90. package/adapters/claude/storage/state.ts +0 -365
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
  93. package/server/engine.ts +0 -1
  94. package/server/reactions.ts +0 -1
package/cli/tui.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * cli/tui.tsx — fullscreen 3-pane dashboard for claude-buddy (Ink/React)
3
+ * cli/tui.tsx — fullscreen 3-pane dashboard for coding-buddy (Ink/React)
4
4
  *
5
5
  * Layout: persistent sidebar | content list | detail preview
6
6
  * The sidebar is always visible. Selecting a section opens the
@@ -35,7 +35,7 @@ import {
35
35
  RARITY_STARS, STAT_NAMES, SPECIES, RARITIES, generateBones, searchBuddy,
36
36
  type Companion, type StatName, type Species, type Rarity,
37
37
  type SearchCriteria, type SearchResult, type BuddyBones,
38
- } from "../server/engine.ts";
38
+ } from "../core/engine.ts";
39
39
  import { getArtFrame, HAT_ART } from "../server/art.ts";
40
40
  import {
41
41
  ACHIEVEMENTS, loadUnlocked, loadEvents,
@@ -139,7 +139,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
139
139
  key: "backup", icon: "💾", label: "Backup",
140
140
  description: [
141
141
  "Create and browse snapshots of your",
142
- "claude-buddy state — settings, hooks,",
142
+ "coding-buddy state — settings, hooks,",
143
143
  "skill, menagerie, status, and config.",
144
144
  "",
145
145
  "Restore is currently manual (copy from",
@@ -149,7 +149,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
149
149
  {
150
150
  key: "system", icon: "🚨", label: "System",
151
151
  description: [
152
- "Manage claude-buddy's installation:",
152
+ "Manage coding-buddy's installation:",
153
153
  "",
154
154
  "• Re-Enable — runs install-buddy",
155
155
  "• Disable — keeps data, removes MCP",
@@ -167,7 +167,7 @@ function Sidebar({ cursor, section, focus }: {
167
167
  const isFocused = focus === "sidebar";
168
168
  return (
169
169
  <Box flexDirection="column" paddingX={1}>
170
- <Text bold color="cyan">{" 🐢 claude-buddy"}</Text>
170
+ <Text bold color="cyan">{" 🐢 coding-buddy"}</Text>
171
171
  <Text>{""}</Text>
172
172
  {SIDEBAR_ITEMS.map((item, i) => {
173
173
  const isActive = item.key === section && focus !== "sidebar";
@@ -306,35 +306,123 @@ const ACHIEVEMENT_PROGRESS: Record<string, { counter: keyof EventCounters; thres
306
306
  first_steps: null,
307
307
  good_boy: { counter: "pets", threshold: 10 },
308
308
  best_friend: { counter: "pets", threshold: 50 },
309
+ pet_overflow: { counter: "pets", threshold: 100 },
310
+ pet_legend: { counter: "pets", threshold: 250 },
311
+ pet_obsessed: { counter: "pets", threshold: 500 },
312
+ pet_god: { counter: "pets", threshold: 1000 },
309
313
  bug_spotter: { counter: "errors_seen", threshold: 1 },
310
314
  error_whisperer: { counter: "errors_seen", threshold: 25 },
311
315
  battle_scarred: { counter: "errors_seen", threshold: 100 },
316
+ error_titan: { counter: "errors_seen", threshold: 500 },
317
+ error_god: { counter: "errors_seen", threshold: 1000 },
318
+ error_apocalypse: { counter: "errors_seen", threshold: 5000 },
312
319
  test_witness: { counter: "tests_failed", threshold: 1 },
313
320
  test_veteran: { counter: "tests_failed", threshold: 50 },
321
+ test_survivor: { counter: "tests_failed", threshold: 200 },
322
+ test_masochist: { counter: "tests_failed", threshold: 500 },
323
+ test_immortal: { counter: "tests_failed", threshold: 1000 },
314
324
  big_mover: { counter: "large_diffs", threshold: 1 },
315
325
  refactor_machine: { counter: "large_diffs", threshold: 10 },
326
+ massive_mover: { counter: "large_diffs", threshold: 25 },
327
+ earth_mover: { counter: "large_diffs", threshold: 50 },
328
+ continental_drift: { counter: "large_diffs", threshold: 100 },
329
+ tectonic_shift: { counter: "large_diffs", threshold: 250 },
316
330
  chatterbox: { counter: "reactions_given", threshold: 100 },
331
+ social_butterfly: { counter: "reactions_given", threshold: 250 },
332
+ hypersocial: { counter: "reactions_given", threshold: 500 },
333
+ never_shuts_up: { counter: "reactions_given", threshold: 1000 },
334
+ chatterbox_elite: { counter: "reactions_given", threshold: 2500 },
335
+ no_off_switch: { counter: "reactions_given", threshold: 5000 },
317
336
  week_streak: { counter: "days_active", threshold: 7 },
337
+ two_week_streak: { counter: "days_active", threshold: 14 },
318
338
  month_streak: { counter: "days_active", threshold: 30 },
339
+ quarter_streak: { counter: "days_active", threshold: 90 },
340
+ hundred_days: { counter: "days_active", threshold: 100 },
341
+ year_streak: { counter: "days_active", threshold: 365 },
319
342
  power_user: { counter: "commands_run", threshold: 50 },
343
+ commander: { counter: "commands_run", threshold: 200 },
344
+ command_overlord: { counter: "commands_run", threshold: 500 },
345
+ command_addict: { counter: "commands_run", threshold: 1000 },
346
+ command_deity: { counter: "commands_run", threshold: 2500 },
320
347
  dedicated: { counter: "turns", threshold: 200 },
321
348
  thousand_turns: { counter: "turns", threshold: 1000 },
349
+ five_thousand_turns: { counter: "turns", threshold: 5000 },
350
+ ten_thousand_turns: { counter: "turns", threshold: 10000 },
351
+ twenty_five_k_turns: { counter: "turns", threshold: 25000 },
352
+ fifty_k_turns: { counter: "turns", threshold: 50000 },
353
+ session_regular: { counter: "sessions", threshold: 10 },
354
+ session_veteran: { counter: "sessions", threshold: 50 },
355
+ session_centurion: { counter: "sessions", threshold: 100 },
356
+ session_addict: { counter: "sessions", threshold: 250 },
357
+ session_machine: { counter: "sessions", threshold: 500 },
358
+ collector: { counter: "buddies_collected", threshold: 3 },
359
+ zookeeper: { counter: "buddies_collected", threshold: 5 },
360
+ menagerie: { counter: "buddies_collected", threshold: 10 },
361
+ buddy_hoarder: { counter: "buddies_collected", threshold: 20 },
362
+ buddy_tycoon: { counter: "buddies_collected", threshold: 50 },
363
+ identity_crisis: { counter: "renames", threshold: 1 },
364
+ name_chameleon: { counter: "renames", threshold: 5 },
365
+ serial_renamer: { counter: "renames", threshold: 10 },
366
+ identity_thief: { counter: "renames", threshold: 25 },
367
+ method_acting: { counter: "personalities_set", threshold: 1 },
368
+ fashionista: { counter: "personalities_set", threshold: 3 },
369
+ personality_crisis: { counter: "personalities_set", threshold: 10 },
370
+ silent_treatment: { counter: "mutes", threshold: 1 },
371
+ prodigal: { counter: "summons", threshold: 1 },
372
+ menagerie_hop: { counter: "summons", threshold: 10 },
373
+ menagerie_hopper: { counter: "summons", threshold: 25 },
374
+ summoner: { counter: "summons", threshold: 50 },
375
+ heartbreaker: { counter: "dismissals", threshold: 1 },
376
+ serial_dumper: { counter: "dismissals", threshold: 5 },
377
+ cold_blooded: { counter: "dismissals", threshold: 10 },
378
+ show_off: { counter: "shows", threshold: 10 },
379
+ exhibitionist: { counter: "shows", threshold: 50 },
380
+ help_me: { counter: "helps", threshold: 1 },
381
+ help_addict: { counter: "helps", threshold: 10 },
382
+ achievement_hunter: { counter: "achievement_views", threshold: 5 },
383
+ achievement_stalker: { counter: "achievement_views", threshold: 25 },
384
+ pack_rat: { counter: "saves", threshold: 1 },
385
+ compulsive_saver: { counter: "saves", threshold: 10 },
386
+ roster_check: { counter: "lists", threshold: 1 },
387
+ roster_obsessed: { counter: "lists", threshold: 10 },
388
+ completionist: { counter: "achievements_unlocked", threshold: ACHIEVEMENTS.length - 1 },
389
+ // Compound checks (multiple counters) — no single-counter progress bar.
390
+ on_off: null,
391
+ indecisive: null,
392
+ troubled: null,
393
+ disaster_zone: null,
394
+ apocalypse_survivor: null,
395
+ well_rounded: null,
396
+ renaissance: null,
397
+ big_and_broken: null,
398
+ collector_and_destroyer: null,
322
399
  };
323
400
 
324
- function AchievementsListPane({ cursor, unlockedIds, focused }: {
325
- cursor: number; unlockedIds: Set<string>; focused: boolean;
401
+ function AchievementsListPane({ cursor, unlockedIds, focused, rows }: {
402
+ cursor: number; unlockedIds: Set<string>; focused: boolean; rows: number;
326
403
  }) {
327
404
  const total = ACHIEVEMENTS.length;
328
405
  const done = ACHIEVEMENTS.filter(a => unlockedIds.has(a.id)).length;
406
+
407
+ // Each bordered row costs ~3 lines; header + indicators + chrome ~8.
408
+ const visibleCount = Math.max(3, Math.min(total, Math.floor((rows - 8) / 3)));
409
+ let start = cursor - Math.floor(visibleCount / 2);
410
+ start = Math.max(0, Math.min(start, total - visibleCount));
411
+ const end = Math.min(total, start + visibleCount);
412
+ const above = start;
413
+ const below = total - end;
414
+
329
415
  return (
330
416
  <Box flexDirection="column" paddingX={1}>
331
417
  <Text bold color={focused ? "cyan" : "gray"}>{" 🏆 Achievements"}</Text>
332
418
  <Text dimColor>{" "}{done}/{total} unlocked</Text>
333
419
  <Text>{""}</Text>
334
- {ACHIEVEMENTS.map((a, i) => {
420
+ {above > 0 ? <Text dimColor>{" ▲ "}{above}{" more"}</Text> : null}
421
+ {ACHIEVEMENTS.slice(start, end).map((a, i) => {
422
+ const absIdx = start + i;
335
423
  const isUnlocked = unlockedIds.has(a.id);
336
424
  const isHidden = a.secret && !isUnlocked;
337
- const isCursor = focused && i === cursor;
425
+ const isCursor = focused && absIdx === cursor;
338
426
  const name = isHidden ? "???" : a.name;
339
427
  const icon = isHidden ? "🔒" : a.icon;
340
428
  return (
@@ -350,6 +438,7 @@ function AchievementsListPane({ cursor, unlockedIds, focused }: {
350
438
  </Box>
351
439
  );
352
440
  })}
441
+ {below > 0 ? <Text dimColor>{" ▼ "}{below}{" more"}</Text> : null}
353
442
  </Box>
354
443
  );
355
444
  }
@@ -695,7 +784,7 @@ function BackupDetailPane({ backups, cursor }: {
695
784
  <Text bold color="cyan">➕ Create Backup</Text>
696
785
  <Text>{""}</Text>
697
786
  <Text dimColor>Creates a snapshot of all</Text>
698
- <Text dimColor>claude-buddy state files:</Text>
787
+ <Text dimColor>coding-buddy state files:</Text>
699
788
  <Text>{""}</Text>
700
789
  <Text>{" "}• settings.json</Text>
701
790
  <Text>{" "}• MCP server config</Text>
@@ -857,7 +946,7 @@ function EnableDetailPane({ result, running }: {
857
946
  return (
858
947
  <Box flexDirection="column" paddingLeft={1}>
859
948
  <Text>{""}</Text>
860
- <Text bold color="green">🔄 Re-Enable claude-buddy</Text>
949
+ <Text bold color="green">🔄 Re-Enable coding-buddy</Text>
861
950
  <Text>{""}</Text>
862
951
  <Text dimColor>This will register:</Text>
863
952
  <Text>{" "}• MCP server in {CLAUDE_JSON}</Text>
@@ -913,7 +1002,7 @@ function UninstallDetailPane({ stage, typed, result, keepState }: {
913
1002
  return (
914
1003
  <Box flexDirection="column" paddingLeft={1}>
915
1004
  <Text>{""}</Text>
916
- <Text bold color="red">💥 Uninstall claude-buddy</Text>
1005
+ <Text bold color="red">💥 Uninstall coding-buddy</Text>
917
1006
  <Text>{""}</Text>
918
1007
  <Text color="yellow">⚠ This will remove:</Text>
919
1008
  <Text>{" "}• MCP server registration</Text>
@@ -956,7 +1045,7 @@ function DisableConfirmPane({ result, confirming }: {
956
1045
  return (
957
1046
  <Box flexDirection="column" paddingLeft={1}>
958
1047
  <Text>{""}</Text>
959
- <Text bold color="red">☠ Disable claude-buddy</Text>
1048
+ <Text bold color="red">☠ Disable coding-buddy</Text>
960
1049
  <Text>{""}</Text>
961
1050
  <Text dimColor>This will remove:</Text>
962
1051
  <Text>{" "}• MCP server from {CLAUDE_JSON}</Text>
@@ -1018,7 +1107,7 @@ function disableBuddy(): DisableResult {
1018
1107
  if (settings.hooks[hookType]) {
1019
1108
  const before = settings.hooks[hookType].length;
1020
1109
  settings.hooks[hookType] = settings.hooks[hookType].filter(
1021
- (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
1110
+ (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
1022
1111
  );
1023
1112
  if (settings.hooks[hookType].length < before) changed = true;
1024
1113
  if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
@@ -2100,7 +2189,7 @@ function App() {
2100
2189
  middlePane = <SettingsListPane cursor={settCursor} config={config} focused={focus === "list"} />;
2101
2190
  rightPane = <SettingDetailPane settingIndex={settCursor} config={config} editing={focus === "edit"} numInput={numInput} optCursor={optCursor} />;
2102
2191
  } else if (section === "achievements") {
2103
- middlePane = <AchievementsListPane cursor={listCursor} unlockedIds={unlockedIds} focused={focus === "list"} />;
2192
+ middlePane = <AchievementsListPane cursor={listCursor} unlockedIds={unlockedIds} focused={focus === "list"} rows={rows} />;
2104
2193
  if (ACHIEVEMENTS[listCursor]) {
2105
2194
  rightPane = <AchievementDetailPane
2106
2195
  achievement={ACHIEVEMENTS[listCursor]}
@@ -2205,7 +2294,7 @@ function App() {
2205
2294
  return (
2206
2295
  <Box flexDirection="column" height={rows}>
2207
2296
  <Box>
2208
- <Text color="cyan" bold>{"─ claude-buddy "}{"─".repeat(Math.max(0, cols - 17))}</Text>
2297
+ <Text color="cyan" bold>{"─ coding-buddy "}{"─".repeat(Math.max(0, cols - 17))}</Text>
2209
2298
  </Box>
2210
2299
  <Box flexGrow={1}>
2211
2300
  <Box width={sidebarWidth} flexDirection="column" borderStyle="single" borderColor={focus === "sidebar" ? "cyan" : "gray"}>
@@ -2237,7 +2326,7 @@ function App() {
2237
2326
  // ─── Entry ──────────────────────────────────────────────────────────────────
2238
2327
 
2239
2328
  if (!process.stdin.isTTY) {
2240
- console.error("claude-buddy tui requires an interactive terminal (TTY)");
2329
+ console.error("coding-buddy tui requires an interactive terminal (TTY)");
2241
2330
  process.exit(1);
2242
2331
  }
2243
2332
 
@@ -1,32 +1,15 @@
1
- #!/usr/bin/env bun
2
1
  /**
3
- * claude-buddy uninstall — remove all integrations
2
+ * coding-buddy uninstall — remove all integrations
4
3
  */
5
4
 
6
5
  import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync } from "fs";
7
6
  import { join } from "path";
8
- import { cleanupPluginState } from "../storage/settings.ts";
9
- import { getBuddySkillDir, getBuddyStateDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
10
-
11
- interface HookCommand {
12
- type: "command";
13
- command: string;
14
- }
15
-
16
- interface HookMatcherEntry {
17
- matcher?: string;
18
- hooks?: HookCommand[];
19
- }
20
-
21
- interface ClaudeSettings {
22
- statusLine?: { command?: string };
23
- hooks?: Record<string, HookMatcherEntry[]>;
24
- [key: string]: unknown;
25
- }
26
-
27
- function hasBuddyHook(entry: HookMatcherEntry): boolean {
28
- return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
29
- }
7
+ import {
8
+ buddyStateDir,
9
+ claudeSettingsPath,
10
+ claudeSkillDir,
11
+ claudeUserConfigPath,
12
+ } from "../server/path.ts";
30
13
 
31
14
  const GREEN = "\x1b[32m";
32
15
  const YELLOW = "\x1b[33m";
@@ -35,47 +18,56 @@ const NC = "\x1b[0m";
35
18
  function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
36
19
  function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
37
20
 
38
- const SETTINGS_FILE = getClaudeSettingsPath();
39
- const SKILL_DIR = getBuddySkillDir();
40
- const STATE_DIR = getBuddyStateDir();
41
- const CLAUDE_JSON = getClaudeJsonPath();
21
+ const SETTINGS_FILE = claudeSettingsPath();
22
+ const SKILL_DIR = claudeSkillDir("buddy");
23
+ const STATE_DIR = buddyStateDir();
24
+ const CLAUDE_JSON_PATH = claudeUserConfigPath();
42
25
 
43
- console.log("\nclaude-buddy uninstall\n");
26
+ console.log("\ncoding-buddy uninstall\n");
44
27
 
28
+ // Stop all popup reopen loops and close any running popup
45
29
  try {
46
30
  if (existsSync(STATE_DIR)) {
47
- for (const f of readdirSync(STATE_DIR).filter((f) => f.startsWith("popup-reopen-pid."))) {
31
+ // Kill all session popup loops (popup-reopen-pid.*)
32
+ for (const f of readdirSync(STATE_DIR).filter(f => f.startsWith("popup-reopen-pid."))) {
48
33
  const pidPath = join(STATE_DIR, f);
49
34
  const pid = parseInt(readFileSync(pidPath, "utf8").trim(), 10);
50
- if (pid > 0) {
51
- try { process.kill(pid); } catch {}
52
- }
35
+ if (pid > 0) { try { process.kill(pid); } catch { /* already dead */ } }
53
36
  rmSync(pidPath, { force: true });
54
37
  }
38
+ // Clean up all session-scoped files
39
+ const patterns = ["popup-stop.", "popup-resize.", "popup-env.", "popup-scroll.",
40
+ "reaction.", ".last_reaction.", ".last_comment."];
41
+ for (const f of readdirSync(STATE_DIR)) {
42
+ if (patterns.some(p => f.startsWith(p))) {
43
+ rmSync(join(STATE_DIR, f), { force: true });
44
+ }
45
+ }
55
46
  }
47
+ // Close any open popup
56
48
  if (process.env.TMUX) {
57
49
  const { execSync } = await import("child_process");
58
50
  execSync("tmux display-popup -C 2>/dev/null", { stdio: "ignore" });
59
51
  }
60
52
  ok("Popup stopped");
61
- } catch {
62
- // noop
63
- }
53
+ } catch { /* not in tmux or no popup */ }
64
54
 
55
+ // Remove MCP server from Claude's user config
65
56
  try {
66
- const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
57
+ const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON_PATH, "utf8"));
67
58
  if (claudeJson.mcpServers?.["claude-buddy"]) {
68
59
  delete claudeJson.mcpServers["claude-buddy"];
69
60
  if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
70
- writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
71
- ok(`MCP server removed from ${CLAUDE_JSON}`);
61
+ writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
62
+ ok(`MCP server removed from ${CLAUDE_JSON_PATH}`);
72
63
  }
73
64
  } catch {
74
- warn(`Could not update ${CLAUDE_JSON}`);
65
+ warn(`Could not update ${CLAUDE_JSON_PATH}`);
75
66
  }
76
67
 
68
+ // Remove hooks and statusline from settings.json
77
69
  try {
78
- const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as ClaudeSettings;
70
+ const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
79
71
  let changed = false;
80
72
 
81
73
  if (settings.statusLine?.command?.includes("buddy")) {
@@ -87,7 +79,9 @@ try {
87
79
  for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd", "UserPromptSubmit"] as const) {
88
80
  if (settings.hooks?.[hookType]) {
89
81
  const before = settings.hooks[hookType].length;
90
- settings.hooks[hookType] = settings.hooks[hookType].filter((h) => !hasBuddyHook(h));
82
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
83
+ (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
84
+ );
91
85
  if (settings.hooks[hookType].length < before) {
92
86
  ok(`${hookType} hooks removed`);
93
87
  changed = true;
@@ -104,11 +98,7 @@ try {
104
98
  warn("Could not update settings.json");
105
99
  }
106
100
 
107
- const cleanup = cleanupPluginState(SETTINGS_FILE, STATE_DIR);
108
- if (cleanup.transientFilesRemoved > 0) {
109
- ok(`Removed ${cleanup.transientFilesRemoved} transient session files`);
110
- }
111
-
101
+ // Remove skill
112
102
  if (existsSync(SKILL_DIR)) {
113
103
  rmSync(SKILL_DIR, { recursive: true });
114
104
  ok("Skill removed");
@@ -116,6 +106,9 @@ if (existsSync(SKILL_DIR)) {
116
106
  warn("Skill not found (already removed)");
117
107
  }
118
108
 
109
+ // Keep state dir (companion data) — user might want it back.
110
+ // This is profile-scoped when CLAUDE_CONFIG_DIR is set, so other
111
+ // profiles' buddies are unaffected.
119
112
  if (existsSync(STATE_DIR)) {
120
113
  warn(`Companion data kept at ${STATE_DIR} — delete manually if not needed`);
121
114
  }
package/cli/upgrade.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { readFileSync } from "fs";
4
- import { execSync } from "child_process";
4
+ import { execSync, spawnSync } from "child_process";
5
5
  import { join, resolve, dirname } from "path";
6
- import { homedir } from "os";
7
6
 
8
7
  const CYAN = "\x1b[36m";
9
8
  const GREEN = "\x1b[32m";
@@ -28,6 +27,37 @@ function tryExec(cmd: string, fallback = ""): string {
28
27
  }
29
28
  }
30
29
 
30
+ function trySpawn(
31
+ command: string,
32
+ args: string[],
33
+ fallback = "",
34
+ cwd: string = PROJECT_ROOT,
35
+ ): string {
36
+ const result = spawnSync(command, args, {
37
+ cwd,
38
+ encoding: "utf8",
39
+ stdio: ["ignore", "pipe", "ignore"],
40
+ });
41
+ if (result.status !== 0) return fallback;
42
+ return result.stdout.trim();
43
+ }
44
+
45
+ function runCommand(
46
+ command: string,
47
+ args: string[],
48
+ options: {
49
+ cwd?: string;
50
+ stdio?: "ignore" | "inherit";
51
+ } = {},
52
+ ): boolean {
53
+ const result = spawnSync(command, args, {
54
+ cwd: options.cwd ?? PROJECT_ROOT,
55
+ encoding: "utf8",
56
+ stdio: ["ignore", options.stdio ?? "ignore", options.stdio ?? "ignore"],
57
+ });
58
+ return result.status === 0;
59
+ }
60
+
31
61
  function getCurrentVersion(): string {
32
62
  try {
33
63
  const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf8"));
@@ -40,7 +70,7 @@ function getCurrentVersion(): string {
40
70
  function banner() {
41
71
  console.log(`
42
72
  ${CYAN}╔══════════════════════════════════════════════════════════╗${NC}
43
- ${CYAN}║${NC} ${BOLD}claude-buddy upgrade${NC} ${CYAN}║${NC}
73
+ ${CYAN}║${NC} ${BOLD}coding-buddy upgrade${NC} ${CYAN}║${NC}
44
74
  ${CYAN}╚══════════════════════════════════════════════════════════╝${NC}
45
75
  `);
46
76
  }
@@ -48,7 +78,7 @@ ${CYAN}╚═══════════════════════
48
78
  function checkGitRepo(): boolean {
49
79
  const isRepo = tryExec("git rev-parse --is-inside-work-tree 2>/dev/null");
50
80
  if (isRepo !== "true") {
51
- err("Not inside a git repository. Upgrade requires a git clone of claude-buddy.");
81
+ err("Not inside a git repository. Upgrade requires a git clone of coding-buddy.");
52
82
  return false;
53
83
  }
54
84
  return true;
@@ -61,15 +91,13 @@ function getRemoteBranch(): string {
61
91
 
62
92
  function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; remote: string; commits: string[] } {
63
93
  info("Fetching latest from remote...\n");
64
- try {
65
- execSync("git fetch --quiet 2>/dev/null", { cwd: PROJECT_ROOT, stdio: "ignore" });
66
- } catch {
94
+ if (!runCommand("git", ["fetch", "--quiet"])) {
67
95
  warn("git fetch failed — proceeding with cached remote state");
68
96
  }
69
97
 
70
98
  const local = tryExec("git rev-parse HEAD 2>/dev/null");
71
99
  const upstream = tryExec(`git rev-parse '@{upstream}' 2>/dev/null`);
72
- const remote = upstream || tryExec(`git rev-parse origin/${branch} 2>/dev/null`);
100
+ const remote = upstream || trySpawn("git", ["rev-parse", `origin/${branch}`]);
73
101
 
74
102
  if (!local || !remote) {
75
103
  warn("Could not determine remote HEAD — no tracking branch configured");
@@ -81,8 +109,9 @@ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; r
81
109
  return { hasUpdate: false, local, remote, commits: [] };
82
110
  }
83
111
 
84
- const commits = tryExec(
85
- `git log --oneline ${local}..origin/${branch} 2>/dev/null`,
112
+ const commits = trySpawn(
113
+ "git",
114
+ ["log", "--oneline", `${local}..origin/${branch}`],
86
115
  ).split("\n").filter(Boolean);
87
116
 
88
117
  return { hasUpdate: true, local, remote, commits };
@@ -91,10 +120,12 @@ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; r
91
120
  function pullLatest(branch: string): boolean {
92
121
  info(`Pulling latest from origin/${branch}...`);
93
122
  try {
94
- const output = execSync(`git pull --ff-only origin ${branch} 2>&1`, {
123
+ const result = spawnSync("git", ["pull", "--ff-only", "origin", branch], {
95
124
  cwd: PROJECT_ROOT,
96
125
  encoding: "utf8",
126
+ stdio: ["ignore", "pipe", "pipe"],
97
127
  });
128
+ if (result.status !== 0) throw new Error(result.stderr || result.stdout || "unknown error");
98
129
  ok("Git pull successful");
99
130
  return true;
100
131
  } catch (e: any) {
@@ -108,7 +139,7 @@ function pullLatest(branch: string): boolean {
108
139
  function installDeps(): boolean {
109
140
  info("Installing dependencies...");
110
141
  try {
111
- execSync("bun install 2>&1", { cwd: PROJECT_ROOT, stdio: "ignore" });
142
+ if (!runCommand("bun", ["install"])) throw new Error("bun install failed");
112
143
  ok("Dependencies installed");
113
144
  return true;
114
145
  } catch {
@@ -120,7 +151,9 @@ function installDeps(): boolean {
120
151
  function reinstallBuddy(): boolean {
121
152
  info("Re-running install-buddy to update integrations...\n");
122
153
  try {
123
- execSync("bun run install-buddy 2>&1", { cwd: PROJECT_ROOT, stdio: "inherit" });
154
+ if (!runCommand("bun", ["run", "install-buddy"], { stdio: "inherit" })) {
155
+ throw new Error("install-buddy failed");
156
+ }
124
157
  return true;
125
158
  } catch {
126
159
  err("install-buddy failed");