@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
- package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
- package/LICENSE +1 -1
- package/README.md +88 -37
- 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 +111 -50
- 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 +2 -2
- 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 +19 -1
- package/hooks/file-type-react.sh +401 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +208 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
- package/hooks/react.sh +1368 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +70 -31
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +17 -1
- 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 +540 -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 +397 -0
- package/statusline/combined-status.sh +134 -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/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
|
@@ -40,7 +40,7 @@ function getCurrentVersion(): string {
|
|
|
40
40
|
function banner() {
|
|
41
41
|
console.log(`
|
|
42
42
|
${CYAN}╔══════════════════════════════════════════════════════════╗${NC}
|
|
43
|
-
${CYAN}║${NC} ${BOLD}
|
|
43
|
+
${CYAN}║${NC} ${BOLD}coding-buddy upgrade${NC} ${CYAN}║${NC}
|
|
44
44
|
${CYAN}╚══════════════════════════════════════════════════════════╝${NC}
|
|
45
45
|
`);
|
|
46
46
|
}
|
|
@@ -48,7 +48,7 @@ ${CYAN}╚═══════════════════════
|
|
|
48
48
|
function checkGitRepo(): boolean {
|
|
49
49
|
const isRepo = tryExec("git rev-parse --is-inside-work-tree 2>/dev/null");
|
|
50
50
|
if (isRepo !== "true") {
|
|
51
|
-
err("Not inside a git repository. Upgrade requires a git clone of
|
|
51
|
+
err("Not inside a git repository. Upgrade requires a git clone of coding-buddy.");
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
return true;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Validate species art format.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run cli/validate-species.ts <path>
|
|
7
|
+
* bun run cli/validate-species.ts --check-species <name>
|
|
8
|
+
*
|
|
9
|
+
* Exits 0 if valid, 1 if invalid (with error messages to stderr).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
|
|
15
|
+
interface ValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
errors: string[];
|
|
18
|
+
warnings: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const EYE_PLACEHOLDER = "{E}";
|
|
22
|
+
|
|
23
|
+
/** Check a string for ANSI escape sequences */
|
|
24
|
+
function hasAnsiCodes(s: string): boolean {
|
|
25
|
+
return s.includes("\x1b[");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Strip ANSI codes from a string */
|
|
29
|
+
function stripAnsi(s: string): string {
|
|
30
|
+
return s.replace(/\x1b\[[^m]*m/g, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Validate a single art frame (must be exactly 5 lines) */
|
|
34
|
+
function validateFrame(
|
|
35
|
+
species: string,
|
|
36
|
+
frameIndex: number,
|
|
37
|
+
lines: string[],
|
|
38
|
+
): ValidationResult {
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
const warnings: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (lines.length !== 5) {
|
|
43
|
+
errors.push(
|
|
44
|
+
` Species "${species}" frame ${frameIndex}: expected 5 lines, got ${lines.length}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
const rawLen = line.length;
|
|
51
|
+
const displayLen = stripAnsi(line).length;
|
|
52
|
+
|
|
53
|
+
if (hasAnsiCodes(line)) {
|
|
54
|
+
errors.push(
|
|
55
|
+
` Species "${species}" frame ${frameIndex} line ${i + 1}: contains ANSI escape sequences (not allowed)`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (displayLen > 14) {
|
|
60
|
+
errors.push(
|
|
61
|
+
` Species "${species}" frame ${frameIndex} line ${i + 1}: display width ${displayLen} exceeds max 14 chars`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Each species art should have the eye placeholder in at least one line
|
|
66
|
+
// (not strictly required but warn if missing)
|
|
67
|
+
if (!line.includes(EYE_PLACEHOLDER) && i === 1) {
|
|
68
|
+
warnings.push(
|
|
69
|
+
` Species "${species}" frame ${frameIndex} line ${i + 1}: missing ${EYE_PLACEHOLDER} eye placeholder (recommended for line 2)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validate art structure from parsed JSON */
|
|
78
|
+
function validateSpeciesArt(
|
|
79
|
+
species: string,
|
|
80
|
+
art: string[][],
|
|
81
|
+
): ValidationResult {
|
|
82
|
+
const errors: string[] = [];
|
|
83
|
+
const warnings: string[] = [];
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(art)) {
|
|
86
|
+
errors.push(`Species "${species}": art must be an array of frames`);
|
|
87
|
+
return { valid: false, errors, warnings };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (art.length !== 3) {
|
|
91
|
+
errors.push(
|
|
92
|
+
`Species "${species}": expected 3 animation frames, got ${art.length}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < art.length; i++) {
|
|
97
|
+
const frame = art[i];
|
|
98
|
+
const result = validateFrame(species, i, frame);
|
|
99
|
+
errors.push(...result.errors);
|
|
100
|
+
warnings.push(...result.warnings);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Validate a face template — must contain at least one eye placeholder */
|
|
107
|
+
function validateFaceTemplate(
|
|
108
|
+
species: string,
|
|
109
|
+
face: string,
|
|
110
|
+
): ValidationResult {
|
|
111
|
+
const errors: string[] = [];
|
|
112
|
+
const warnings: string[] = [];
|
|
113
|
+
|
|
114
|
+
if (face.length > 20) {
|
|
115
|
+
errors.push(
|
|
116
|
+
`Species "${species}" face template: display width ${face.length} exceeds max 20 chars`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!face.includes(EYE_PLACEHOLDER)) {
|
|
121
|
+
errors.push(
|
|
122
|
+
`Species "${species}" face template: missing ${EYE_PLACEHOLDER} eye placeholder`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Main validation function — validates a species JSON file or inline art.
|
|
131
|
+
* Returns exit code (0 = valid, 1 = invalid).
|
|
132
|
+
*/
|
|
133
|
+
export function validateSpeciesFile(filePath: string): ValidationResult {
|
|
134
|
+
const errors: string[] = [];
|
|
135
|
+
const warnings: string[] = [];
|
|
136
|
+
|
|
137
|
+
let data: Record<string, unknown>;
|
|
138
|
+
try {
|
|
139
|
+
const raw = readFileSync(filePath, "utf8");
|
|
140
|
+
data = JSON.parse(raw);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return {
|
|
143
|
+
valid: false,
|
|
144
|
+
errors: [`Failed to read or parse JSON file: ${filePath}`],
|
|
145
|
+
warnings: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const species = data.name as string | undefined;
|
|
150
|
+
if (!species || typeof species !== "string") {
|
|
151
|
+
errors.push("Species must have a 'name' field (string)");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const art = data.art as string[][] | undefined;
|
|
155
|
+
if (!art) {
|
|
156
|
+
errors.push("Species must have an 'art' field (string[][] — 3 frames × 5 lines)");
|
|
157
|
+
} else {
|
|
158
|
+
const artResult = validateSpeciesArt(species ?? "unknown", art);
|
|
159
|
+
errors.push(...artResult.errors);
|
|
160
|
+
warnings.push(...artResult.warnings);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const face = data.face as string | undefined;
|
|
164
|
+
if (face) {
|
|
165
|
+
const faceResult = validateFaceTemplate(species ?? "unknown", face);
|
|
166
|
+
errors.push(...faceResult.errors);
|
|
167
|
+
warnings.push(...faceResult.warnings);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Print results and exit with appropriate code */
|
|
174
|
+
function main(): never {
|
|
175
|
+
const args = process.argv.slice(2);
|
|
176
|
+
|
|
177
|
+
if (args.length === 0) {
|
|
178
|
+
console.error("Usage: bun run cli/validate-species.ts <path-to-species-json>");
|
|
179
|
+
console.error(" Validates art format, ANSI codes, and eye placeholder.");
|
|
180
|
+
console.error("");
|
|
181
|
+
console.error("Example:");
|
|
182
|
+
console.error(" bun run cli/validate-species.ts ./my-species.json");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const filePath = args[0].startsWith("/") ? args[0] : join(process.cwd(), args[0]);
|
|
187
|
+
const result = validateSpeciesFile(filePath);
|
|
188
|
+
|
|
189
|
+
if (result.warnings.length > 0) {
|
|
190
|
+
console.warn("Warnings:");
|
|
191
|
+
for (const w of result.warnings) {
|
|
192
|
+
console.warn(w);
|
|
193
|
+
}
|
|
194
|
+
console.warn("");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (result.valid) {
|
|
198
|
+
console.log(`✓ ${args[0]}: valid`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
} else {
|
|
201
|
+
console.error("Errors:");
|
|
202
|
+
for (const e of result.errors) {
|
|
203
|
+
console.error(e);
|
|
204
|
+
}
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
main();
|
package/cli/verify.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* coding-buddy verify — show what buddy a user ID produces
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { generateBones, renderBuddy, STAT_NAMES } from "../core/engine.ts"
|
|
6
|
-
import { resolveUserId } from "../
|
|
5
|
+
import { generateBones, renderBuddy, STAT_NAMES } from "../core/engine.ts"
|
|
6
|
+
import { resolveUserId } from "../server/state.ts";
|
|
7
7
|
|
|
8
8
|
const userId = process.argv[3] || resolveUserId();
|
|
9
9
|
|