Skip to content

Commit 149b9f4

Browse files
committed
feat(plugin): add buddy character customization in config (#1007)
Add get_buddy_config() to read custom name/face/greeting/farewell from codingbuddy.config.json buddy section. All render functions now accept optional buddy_config with validated unicode face field. Falls back to defaults when config is not set.
1 parent 8e12f47 commit 149b9f4

2 files changed

Lines changed: 262 additions & 13 deletions

File tree

packages/claude-code-plugin/hooks/lib/buddy_renderer.py

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
Renders buddy face greeting, project scan results, agent recommendations,
44
and session summary with tone/language support and ANSI color output.
55
"""
6-
from typing import Any, Dict, List
6+
import re
7+
from typing import Any, Dict, List, Optional
78

89
# ANSI color codes for terminal output
910
ANSI_COLORS: Dict[str, str] = {
@@ -64,6 +65,61 @@
6465
BUDDY_WRAP_FACE = "\u25d5\u2304\u25d5" # ◕⌄◕
6566
BUDDY_WINK_FACE = "\u25d5\u2040\u25d5" # ◕⁀◕
6667

68+
# Default buddy character configuration
69+
DEFAULT_BUDDY_CONFIG: Dict[str, str] = {
70+
"name": "Buddy",
71+
"face": BUDDY_FACE,
72+
"greeting": "",
73+
"farewell": "",
74+
}
75+
76+
# Max length for custom face field (unicode characters)
77+
_MAX_FACE_LENGTH = 10
78+
79+
# Pattern: only printable unicode, no control chars or ASCII letters/digits
80+
_FACE_PATTERN = re.compile(
81+
r"^[^\x00-\x1f\x7f A-Za-z0-9]{1,%d}$" % _MAX_FACE_LENGTH
82+
)
83+
84+
85+
def get_buddy_config(config: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
86+
"""Extract and validate buddy customization from config dict.
87+
88+
Args:
89+
config: Parsed codingbuddy.config.json dict. May contain a 'buddy' key.
90+
91+
Returns:
92+
Validated buddy config with defaults for missing/invalid fields.
93+
"""
94+
result = dict(DEFAULT_BUDDY_CONFIG)
95+
96+
if not config or not isinstance(config.get("buddy"), dict):
97+
return result
98+
99+
buddy = config["buddy"]
100+
101+
# name: string, non-empty, max 30 chars
102+
name = buddy.get("name")
103+
if isinstance(name, str) and 0 < len(name.strip()) <= 30:
104+
result["name"] = name.strip()
105+
106+
# face: unicode-only, no ASCII alphanumerics, max _MAX_FACE_LENGTH chars
107+
face = buddy.get("face")
108+
if isinstance(face, str) and _FACE_PATTERN.match(face):
109+
result["face"] = face
110+
111+
# greeting: string, max 100 chars
112+
greeting = buddy.get("greeting")
113+
if isinstance(greeting, str) and 0 < len(greeting.strip()) <= 100:
114+
result["greeting"] = greeting.strip()
115+
116+
# farewell: string, max 100 chars
117+
farewell = buddy.get("farewell")
118+
if isinstance(farewell, str) and 0 < len(farewell.strip()) <= 100:
119+
result["farewell"] = farewell.strip()
120+
121+
return result
122+
67123
# Returning session greetings by tone and language
68124
RETURNING_GREETINGS: Dict[str, Dict[str, str]] = {
69125
"casual": {
@@ -154,20 +210,28 @@ def _colorize(text: str, color: str) -> str:
154210
return f"{ansi}{text}{reset}"
155211

156212

157-
def render_buddy_face(tone: str, language: str) -> str:
213+
def render_buddy_face(
214+
tone: str,
215+
language: str,
216+
buddy_config: Optional[Dict[str, str]] = None,
217+
) -> str:
158218
"""Render the buddy character face with greeting.
159219
160220
Args:
161221
tone: 'casual' or 'formal'
162222
language: Language code (en, ko, ja, zh, es)
223+
buddy_config: Optional buddy customization from get_buddy_config().
163224
164225
Returns:
165226
ASCII art buddy face with greeting message.
166227
"""
167-
greeting = _get_greeting(tone, language)
228+
bc = buddy_config or DEFAULT_BUDDY_CONFIG
229+
face = bc.get("face", BUDDY_FACE)
230+
custom_greeting = bc.get("greeting", "")
231+
greeting = custom_greeting if custom_greeting else _get_greeting(tone, language)
168232
lines = [
169233
"\u256d\u2501\u2501\u2501\u256e",
170-
f"\u2503 {BUDDY_FACE} \u2503 {greeting}",
234+
f"\u2503 {face} \u2503 {greeting}",
171235
"\u2570\u2501\u2501\u2501\u256f",
172236
]
173237
return "\n".join(lines)
@@ -255,6 +319,7 @@ def render_session_summary(
255319
agents: List[Dict[str, Any]],
256320
tone: str,
257321
language: str,
322+
buddy_config: Optional[Dict[str, str]] = None,
258323
) -> str:
259324
"""Render session summary with buddy character for stop hook.
260325
@@ -267,18 +332,29 @@ def render_session_summary(
267332
eye (str), colorAnsi (str)
268333
tone: 'casual' or 'formal'
269334
language: Language code (en, ko, ja, zh, es)
335+
buddy_config: Optional buddy customization from get_buddy_config().
270336
271337
Returns:
272338
Formatted session summary string.
273339
"""
340+
bc = buddy_config or DEFAULT_BUDDY_CONFIG
341+
custom_farewell = bc.get("farewell", "")
342+
274343
greeting = _get_farewell_greeting(tone, language)
275-
farewell = _get_farewell_message(tone, language)
344+
farewell = custom_farewell if custom_farewell else _get_farewell_message(tone, language)
345+
346+
# Use custom face for wrap variant: take first char of custom face if set
347+
face = bc.get("face", BUDDY_FACE)
348+
if face != BUDDY_FACE:
349+
wrap_face = face # custom face used as-is
350+
else:
351+
wrap_face = BUDDY_WRAP_FACE
276352

277353
parts = []
278354

279355
# Buddy face with farewell greeting
280356
parts.append("\u256d\u2501\u2501\u2501\u256e")
281-
parts.append(f"\u2503 {BUDDY_WRAP_FACE} \u2503 {greeting}")
357+
parts.append(f"\u2503 {wrap_face} \u2503 {greeting}")
282358
parts.append("\u2570\u2501\u2501\u2501\u256f")
283359

284360
# Session stats section (only if data available)
@@ -320,7 +396,7 @@ def render_session_summary(
320396

321397
# Farewell
322398
parts.append("")
323-
parts.append(f"{BUDDY_FACE} {farewell}")
399+
parts.append(f"{face} {farewell}")
324400

325401
return "\n".join(parts)
326402

@@ -358,6 +434,7 @@ def render_returning_session(
358434
pending_context: "Dict[str, Any] | None",
359435
tone: str,
360436
language: str,
437+
buddy_config: Optional[Dict[str, str]] = None,
361438
) -> str:
362439
"""Render returning session welcome-back display.
363440
@@ -369,17 +446,27 @@ def render_returning_session(
369446
from docs/codingbuddy/context.md parsing. None if no context.
370447
tone: 'casual' or 'formal'
371448
language: Language code (en, ko, ja, zh, es)
449+
buddy_config: Optional buddy customization from get_buddy_config().
372450
373451
Returns:
374452
Formatted returning session greeting string.
375453
"""
376-
greeting = _get_returning_greeting(tone, language)
454+
bc = buddy_config or DEFAULT_BUDDY_CONFIG
455+
face = bc.get("face", BUDDY_FACE)
456+
custom_greeting = bc.get("greeting", "")
457+
greeting = custom_greeting if custom_greeting else _get_returning_greeting(tone, language)
458+
459+
# Wink face variant for continue prompt
460+
if face != BUDDY_FACE:
461+
wink_face = face
462+
else:
463+
wink_face = BUDDY_WINK_FACE
377464

378465
parts = []
379466

380467
# Buddy face with welcome-back greeting
381468
parts.append("\u256d\u2501\u2501\u2501\u256e")
382-
parts.append(f"\u2503 {BUDDY_FACE} \u2503 {greeting}")
469+
parts.append(f"\u2503 {face} \u2503 {greeting}")
383470
parts.append("\u2570\u2501\u2501\u2501\u256f")
384471

385472
# Last session summary
@@ -427,9 +514,9 @@ def render_returning_session(
427514
if pending_context and pending_context.get("mode"):
428515
mode = pending_context["mode"]
429516
next_mode = "ACT" if mode == "PLAN" else mode
430-
parts.append(f"{BUDDY_WINK_FACE} Type \"{next_mode}\" to continue!")
517+
parts.append(f"{wink_face} Type \"{next_mode}\" to continue!")
431518
else:
432-
parts.append(f"{BUDDY_WINK_FACE} Ready when you are!")
519+
parts.append(f"{wink_face} Ready when you are!")
433520

434521
return "\n".join(parts)
435522

@@ -441,6 +528,7 @@ def render_session_start(
441528
language: str,
442529
previous_session: "Dict[str, Any] | None" = None,
443530
pending_context: "Dict[str, Any] | None" = None,
531+
buddy_config: Optional[Dict[str, str]] = None,
444532
) -> str:
445533
"""Render complete session-start output.
446534
@@ -455,21 +543,25 @@ def render_session_start(
455543
language: Language code.
456544
previous_session: Optional previous session data for returning users.
457545
pending_context: Optional pending work context from context.md.
546+
buddy_config: Optional buddy customization from get_buddy_config().
458547
459548
Returns:
460549
Complete formatted session-start output.
461550
"""
462551
# Returning session path
463552
if previous_session:
464553
parts = [
465-
render_returning_session(previous_session, pending_context, tone, language),
554+
render_returning_session(
555+
previous_session, pending_context, tone, language,
556+
buddy_config=buddy_config,
557+
),
466558
"",
467559
render_scan_results(scan),
468560
]
469561
else:
470562
# First visit path (default)
471563
parts = [
472-
render_buddy_face(tone, language),
564+
render_buddy_face(tone, language, buddy_config=buddy_config),
473565
"",
474566
render_scan_results(scan),
475567
]

0 commit comments

Comments
 (0)