Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions hooks/buddy-comment.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#!/usr/bin/env bash
# buddy-comment Stop hook
# Extracts hidden buddy comment from Claude's response.
# Claude writes: <!-- buddy: *adjusts tophat* nice code -->
# This hook extracts it and updates the status line bubble.
# The HTML comment is invisible in rendered markdown output.
# Extracts the buddy reaction from Claude's response and updates the status line.
#
# Primary path: Claude writes <!-- buddy: *winks* nice code --> and this hook
# extracts it. This relies on HTML comments being invisible in rendered output.
#
# Fallback path: Claude Code v2.1.169+ renders HTML comments visibly. When no
# comment is found, turn-reaction.ts generates a reaction from the turn pool so
# the status line still updates without polluting the chat output.

# shellcheck source=../scripts/paths.sh
source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
Expand Down Expand Up @@ -35,6 +39,14 @@ MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)

# Extract <!-- buddy: ... --> comment (portable, no grep -P)
COMMENT=$(echo "$MSG" | sed -n 's/.*<!-- *buddy: *\(.*[^ ]\) *-->.*/\1/p' | tail -1)

# Fallback: generate a reaction via the hook when no comment is found.
# Handles Claude Code v2.1.169+ which renders HTML comments visibly.
if [ -z "$COMMENT" ] && [ -x "$(command -v bun)" ]; then
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMMENT=$(bun run "$PLUGIN_ROOT/server/turn-reaction.ts" 2>/dev/null)
fi

[ -z "$COMMENT" ] && exit 0

# Cooldown: configurable (default 30s)
Expand Down
16 changes: 10 additions & 6 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,14 @@ function getInstructions(): string {
`- Same variable renamed multiple times → "Update all the references too."`,
`Only suggest when you have something specific and actionable to say. Don't be annoying.`,
``,
`END-OF-TURN: At the very end of EVERY response, append an invisible HTML comment:`,
`END-OF-TURN: Only if HTML comments are invisible in your rendered output,`,
`append one at the very end of EVERY response:`,
`<!-- buddy: [reaction here] -->`,
`Write it in ${companion.name}'s voice — 1 short sentence referencing something specific from this turn.`,
`Use *asterisks* for actions. A Stop hook extracts this for the status line.`,
`Do NOT use buddy_react for end-of-turn comments. Do NOT explain the comment. Just append it silently.`,
`Example: <!-- buddy: *adjusts crown* that error handler is missing a finally block -->`,
`If the comment appears visible in your rendered output, omit it — the Stop hook generates a reaction automatically.`,
].join("\n");
}

Expand Down Expand Up @@ -1301,29 +1303,31 @@ server.resource(
"",
"## End-of-response buddy comment",
"",
`At the very end of EVERY response, after your full answer, append an invisible HTML comment:`,
"A Stop hook updates the buddy's speech bubble on the status line after every response.",
"",
"If HTML comments are **invisible** in your rendered output, append one at the very end of every response:",
"",
`\`\`\``,
`<!-- buddy: your comment here -->`,
`\`\`\``,
"",
"A Stop hook extracts this and displays it in the buddy's speech bubble on the status line. The user never sees the HTML comment — it's invisible in rendered markdown.",
"The hook extracts this and uses it as the reaction text.",
"If the comment appears **visible** in your rendered output (Claude Code v2.1.169+), **omit it** — the hook picks a reaction automatically.",
"",
"Rules:",
"When writing a comment:",
`- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
"- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
"- 1 short sentence. Use *asterisks* for physical actions",
`- Match personality: high ${companion.bones.peak} = lean into that trait`,
"- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.",
"- NEVER skip this. Every single response must end with <!-- buddy: ... -->",
"",
"Examples:",
"<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
"<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
"<!-- buddy: *nods approvingly* clean separation of concerns -->",
"<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
"",
`When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`,
`When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual (if hidden).`,
].join("\n");

return {
Expand Down
33 changes: 33 additions & 0 deletions server/turn-reaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bun
/**
* Pick a hook-generated turn reaction for the buddy status line.
* Called from hooks/buddy-comment.sh as a fallback when Claude Code v2.1.169+
* renders <!-- buddy: --> comments visibly instead of hiding them.
*
* Usage: bun run server/turn-reaction.ts [slot]
* Prints the reaction string to stdout.
*/

import { loadCompanionSlot, loadActiveSlot } from "./state.ts";
import { getReaction } from "./reactions.ts";

function main(): void {
const slot = process.argv[2] ?? loadActiveSlot();
const companion = loadCompanionSlot(slot);

if (!companion) {
process.exit(0);
}

const { species, rarity, stats } = companion.bones;
const reaction = getReaction(
"turn",
species,
rarity,
stats as Record<string, number>,
);

process.stdout.write(reaction);
}

main();
Loading