@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.
- package/.claude-plugin/marketplace.json +45 -0
- package/.claude-plugin/plugin.json +30 -0
- package/LICENSE +1 -1
- package/README.md +96 -45
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +130 -33
- package/cli/buddy-shell.ts +1 -1
- package/{adapters/claude/install → cli}/disable.ts +19 -32
- package/cli/doctor.ts +231 -0
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +30 -26
- package/{adapters/claude/install → cli}/install.ts +65 -83
- package/{adapters/claude/install → cli}/pick.ts +136 -57
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
- package/cli/tui.tsx +106 -17
- package/{adapters/claude/install → cli}/uninstall.ts +40 -47
- package/cli/upgrade.ts +46 -13
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +22 -9
- package/hooks/file-type-react.sh +403 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +210 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
- package/hooks/react.sh +1364 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +70 -31
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +33 -2
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -15
- package/server/art.ts +450 -1
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/{adapters/claude/server → server}/index.ts +556 -95
- package/server/mcp-launcher.sh +16 -6
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +70 -18
- package/server/shift-mood.ts +43 -0
- package/server/state.ts +552 -3
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
- package/statusline/buddy-status.sh +402 -0
- package/statusline/combined-status.sh +135 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -207
- package/adapters/claude/install/doctor.ts +0 -172
- package/adapters/claude/plugin/marketplace.json +0 -40
- package/adapters/claude/plugin/plugin.json +0 -26
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/path.ts +0 -12
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -408
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -33
- package/adapters/claude/storage/settings.ts +0 -91
- package/adapters/claude/storage/state.ts +0 -365
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
- package/server/engine.ts +0 -1
- 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
|
|
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 "../
|
|
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
|
-
"
|
|
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
|
|
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">{" 🐢
|
|
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
|
-
{
|
|
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 &&
|
|
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>
|
|
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
|
|
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
|
|
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
|
|
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>{"─
|
|
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("
|
|
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
|
-
*
|
|
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 {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 =
|
|
39
|
-
const SKILL_DIR =
|
|
40
|
-
const STATE_DIR =
|
|
41
|
-
const
|
|
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("\
|
|
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
|
-
|
|
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(
|
|
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(
|
|
71
|
-
ok(`MCP server removed from ${
|
|
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 ${
|
|
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"))
|
|
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(
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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 =
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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");
|