From f41afa56cee595c041290dc934c544b62a2c51ce Mon Sep 17 00:00:00 2001 From: KeiSei84 <2206745@gmail.com> Date: Thu, 21 May 2026 20:33:16 +0800 Subject: [PATCH] feat(pet): agent emojis, multi-agent display, plan emoji, language icons (#28) --- scripts/keisei-pet-update.sh | 215 ++++++++++++++++++++--------------- scripts/keisei-pet.sh | 122 ++++++++++++-------- settings-snippet.json | 19 +++- 3 files changed, 216 insertions(+), 140 deletions(-) diff --git a/scripts/keisei-pet-update.sh b/scripts/keisei-pet-update.sh index 3726cff..cc66f8a 100644 --- a/scripts/keisei-pet-update.sh +++ b/scripts/keisei-pet-update.sh @@ -1,109 +1,143 @@ #!/usr/bin/env bash -# KeiSei pet state updater — called by hooks to change the pet's mood. +# KeiSei pet state updater — called by hooks to change the pet's mood and to +# track running sub-agents, current language, and plan completion. # Usage: keisei-pet-update.sh -# Events: prompt | rust_write | github_block | python_no_reason | -# modal_cost | patent_filed | concept_saved | secret_leak | -# test_pass | test_fail | sleep | rule_violation | idle +# Mood events: prompt | rust_write | github_block | python_no_reason | +# modal_cost | patent_filed | concept_saved | secret_leak | +# test_pass | test_fail | sleep | rule_violation | idle +# Agent events: agent_start | agent_done (read tool JSON on stdin) +# Plan event: plan (ExitPlanMode finished) +# Language: lang (reads .tool_input.file_path) # -# The hook may also pipe JSON tool-context on stdin; we ignore it for now -# (future: parse tool_input to make reactions smarter). +# State lives under ~/.claude/pet/: +# state — sourced shell vars (mood/message/since/day/counters/lang/plan) +# agents/ — one file per running sub-agent: "emoji|name|start_epoch" +# agent_tokens — cumulative tokens spent by sub-agents this session set -u STATE_DIR="${HOME}/.claude/pet" STATE="${STATE_DIR}/state" HISTORY="${STATE_DIR}/history.log" -mkdir -p "$STATE_DIR" +AGENTS_DIR="${STATE_DIR}/agents" +TOKENS_FILE="${STATE_DIR}/agent_tokens" +mkdir -p "$STATE_DIR" "$AGENTS_DIR" -# Load current state -mood="neutral" -message="" -since=$(date +%s) -rust_today=0 -patents_today=0 -violations=0 - -# shellcheck source=/dev/null -[ -f "$STATE" ] && source "$STATE" 2>/dev/null || true - -# Daily counter reset (if state last updated yesterday) -last_day=${day:-} -today=$(date +%Y-%m-%d) -if [ "$last_day" != "$today" ]; then - rust_today=0 - patents_today=0 - violations=0 -fi +# Slurp stdin once (hook JSON). Non-blocking; never hang. +INPUT="" +if [ ! -t 0 ]; then INPUT="$(cat 2>/dev/null || true)"; fi event="${1:-}" now=$(date +%s) -# Discard stdin quickly so hook doesn't block -if [ ! -t 0 ]; then - cat >/dev/null 2>&1 || true -fi +# ── emoji maps ────────────────────────────────────────────────────────────── +_agent_emoji() { + case "$1" in + *researcher*) echo "🔬" ;; + *architect*) echo "🏗️" ;; + *critic*) echo "🔪" ;; + *security*) echo "🛡️" ;; + *validator*) echo "✅" ;; + *cost*) echo "💰" ;; + *modal*) echo "☁️" ;; + *fal*ai*|*fal_ai*) echo "🎨" ;; + *ml-implementer*|*ml_implementer*) echo "🧠" ;; + *ml-researcher*|*ml_researcher*) echo "📚" ;; + *infra*) echo "🔧" ;; + *implementer*) echo "⚙️" ;; + *patent*) echo "📜" ;; + Explore|*explore*) echo "🔭" ;; + Plan|*plan*) echo "📐" ;; + *general*) echo "🤖" ;; + *) echo "🤖" ;; + esac +} +_lang_emoji() { + case "$1" in + rs) echo "🦀" ;; + py) echo "🐍" ;; + go) echo "🐹" ;; + ts|tsx) echo "📘" ;; + js|jsx|mjs|cjs) echo "🟨" ;; + swift) echo "🦅" ;; + c|h|cc|cpp|cxx|hpp|hh) echo "⚙️" ;; + java|kt) echo "☕" ;; + rb) echo "💎" ;; + sh|bash|zsh) echo "🐚" ;; + md|mdx) echo "📝" ;; + toml|json|yaml|yml|ini|cfg|conf) echo "🧾" ;; + html|htm) echo "🌐" ;; + css|scss|sass) echo "🎨" ;; + sql) echo "🗄️" ;; + lua) echo "🌙" ;; + php) echo "🐘" ;; + *) echo "📄" ;; + esac +} + +# ── load current state ────────────────────────────────────────────────────── +mood="neutral"; message=""; since="$now"; day="" +rust_today=0; patents_today=0; violations=0; lang=""; plan="" +# shellcheck source=/dev/null +[ -f "$STATE" ] && source "$STATE" 2>/dev/null || true + +# Daily counter reset +today=$(date +%Y-%m-%d) +if [ "${day:-}" != "$today" ]; then rust_today=0; patents_today=0; violations=0; fi + +# ── agent / plan / language events (do not change mood face) ───────────────── case "$event" in - prompt) - mood="thinking" - message="考えてる..." + agent_start) + sub="$(printf '%s' "$INPUT" | jq -r '.tool_input.subagent_type // .tool_input.description // "agent"' 2>/dev/null)" + [ -z "$sub" ] && sub="agent" + em="$(_agent_emoji "$sub")" + short="$(printf '%s' "$sub" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9].*$//' | cut -c1-12)" + [ -z "$short" ] && short="agent" + id="${now}-$$-${RANDOM}" + printf '%s|%s|%s\n' "$em" "$short" "$now" > "$AGENTS_DIR/$id" 2>/dev/null || true + exit 0 ;; - rust_write) - rust_today=$((rust_today + 1)) - mood="happy" - message="構造式 ✓ Rust" + agent_done) + # extract tokens from tool_response if present (Agent results carry usage) + tok="$(printf '%s' "$INPUT" | grep -oE 'total_tokens["[:space:]]*[:=]?[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1)" + if [ -n "${tok:-}" ]; then + prev=0; [ -f "$TOKENS_FILE" ] && prev="$(cat "$TOKENS_FILE" 2>/dev/null || echo 0)" + echo $(( prev + tok )) > "$TOKENS_FILE" 2>/dev/null || true + fi + # remove the oldest running-agent file (FIFO approximation — hooks give no + # stable per-agent id to match start↔done exactly). + oldest="$(ls -1tr "$AGENTS_DIR" 2>/dev/null | head -1)" + [ -n "$oldest" ] && rm -f "$AGENTS_DIR/$oldest" 2>/dev/null || true + exit 0 ;; - github_block) - mood="angry" - message="RULE 0.1! no github" - violations=$((violations + 1)) + plan) + plan="📋"; mood="proud"; message="план готов" ;; - python_no_reason) - mood="alert" - message="Python? 理由は? (RULE 0.2)" - ;; - modal_cost) - mood="alert" - message="\$\$ compute check" - ;; - patent_filed) - mood="proud" - patents_today=$((patents_today + 1)) - message="特許 filed!" - ;; - concept_saved) - mood="happy" - message="💡 concept saved" - ;; - secret_leak) - mood="angry" - message="SECRET! RULE 0.8" - violations=$((violations + 1)) - ;; - test_pass) - mood="happy" - message="テスト ✓" - ;; - test_fail) - mood="sad" - message="テスト ✗" - ;; - rule_violation) - mood="angry" - message="rule violation ⚠" - violations=$((violations + 1)) - ;; - sleep) - mood="sleep" - message="zzz" - ;; - *) - # unknown event — no-op, keep current state - : + lang) + fp="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)" + if [ -n "$fp" ]; then + ext="${fp##*.}"; ext="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')" + lang="$(_lang_emoji "$ext")" + if [ "$ext" = "rs" ]; then rust_today=$((rust_today + 1)); mood="happy"; message="構造式 ✓ Rust"; fi + fi ;; + prompt) mood="thinking"; message="考えてる..." ;; + rust_write) rust_today=$((rust_today + 1)); mood="happy"; message="構造式 ✓ Rust"; lang="🦀" ;; + github_block) mood="angry"; message="RULE 0.1! no github"; violations=$((violations + 1)) ;; + python_no_reason) mood="alert"; message="Python? 理由は? (RULE 0.2)" ;; + modal_cost) mood="alert"; message="\$\$ compute check" ;; + patent_filed) mood="proud"; patents_today=$((patents_today + 1)); message="特許 filed!" ;; + concept_saved) mood="happy"; message="💡 concept saved" ;; + secret_leak) mood="angry"; message="SECRET! RULE 0.8"; violations=$((violations + 1)) ;; + test_pass) mood="happy"; message="テスト ✓" ;; + test_fail) mood="sad"; message="テスト ✗" ;; + rule_violation) mood="angry"; message="rule violation ⚠"; violations=$((violations + 1)) ;; + sleep) mood="sleep"; message="zzz"; plan="" ;; + *) : ;; esac -# Write state atomically +# ── write state atomically ────────────────────────────────────────────────── tmp="${STATE}.tmp.$$" cat > "$tmp" </dev/null || true -# Rolling history (last 50 events) -printf "%s %s\n" "$(date -u +%FT%TZ)" "$event" >> "$HISTORY" -if [ -f "$HISTORY" ] && [ "$(wc -l < "$HISTORY")" -gt 50 ]; then - tail -50 "$HISTORY" > "${HISTORY}.tmp" && mv "${HISTORY}.tmp" "$HISTORY" +printf "%s %s\n" "$(date -u +%FT%TZ)" "$event" >> "$HISTORY" 2>/dev/null || true +if [ -f "$HISTORY" ] && [ "$(wc -l < "$HISTORY" 2>/dev/null || echo 0)" -gt 50 ]; then + tail -50 "$HISTORY" > "${HISTORY}.tmp" 2>/dev/null && mv "${HISTORY}.tmp" "$HISTORY" 2>/dev/null || true fi - -# Hooks in Claude Code expect exit 0 to pass through exit 0 diff --git a/scripts/keisei-pet.sh b/scripts/keisei-pet.sh index 33ffac1..ce8f501 100644 --- a/scripts/keisei-pet.sh +++ b/scripts/keisei-pet.sh @@ -1,67 +1,93 @@ #!/usr/bin/env bash -# KeiSei tamagotchi — statusline renderer. -# Called by Claude Code on every prompt render. Outputs ONE line. -# Reads state from ~/.claude/pet/state (written by keisei-pet-update.sh). +# KeiSei tamagotchi — statusline renderer. Outputs ONE line. +# Shows: running sub-agents (emoji·name·elapsed) + agent token spend + +# plan state + mood face + message + counters + current language + project. +# State written by keisei-pet-update.sh under ~/.claude/pet/. set -u -# Discard any stdin (Claude Code may pipe session JSON to statusLine) -if [ ! -t 0 ]; then - cat >/dev/null 2>&1 || true -fi +# Discard any stdin (Claude Code pipes session JSON to statusLine). +if [ ! -t 0 ]; then cat >/dev/null 2>&1 || true; fi -STATE="${HOME}/.claude/pet/state" - -# Defaults (if state file missing/stale) -mood="neutral" -message="" -since=$(date +%s) -rust_today=0 -patents_today=0 -violations=0 +STATE_DIR="${HOME}/.claude/pet" +STATE="${STATE_DIR}/state" +AGENTS_DIR="${STATE_DIR}/agents" +TOKENS_FILE="${STATE_DIR}/agent_tokens" +mood="neutral"; message=""; since=$(date +%s) +rust_today=0; patents_today=0; violations=0; lang=""; plan="" # shellcheck source=/dev/null [ -f "$STATE" ] && source "$STATE" 2>/dev/null || true now=$(date +%s) -idle=$((now - since)) -# Idle >5 min → pet sleeps (unless it's angry/alert about something) -if [ "$idle" -gt 300 ] && [ "$mood" != "angry" ] && [ "$mood" != "alert" ]; then - mood="sleep" - message="zzz" +dim=$'\033[2m'; reset=$'\033[0m' + +# ── elapsed pretty-printer (s / m / h) ────────────────────────────────────── +_elapsed() { + local s=$1 + if [ "$s" -lt 60 ] ; then printf '%ds' "$s" + elif [ "$s" -lt 3600 ] ; then printf '%dm' $(( s / 60 )) + else printf '%dh%dm' $(( s / 3600 )) $(( (s % 3600) / 60 )) + fi +} + +# ── running sub-agents (self-clean stale > 1h) ────────────────────────────── +agents="" +if [ -d "$AGENTS_DIR" ]; then + for f in "$AGENTS_DIR"/*; do + [ -f "$f" ] || continue + IFS='|' read -r em name start < "$f" + [ -z "${start:-}" ] && { rm -f "$f" 2>/dev/null; continue; } + age=$(( now - start )) + if [ "$age" -gt 3600 ]; then rm -f "$f" 2>/dev/null; continue; fi + agents+=" ${em}${name}·$(_elapsed "$age")" + done fi -# Face + color by mood +# ── agent token spend this session ────────────────────────────────────────── +toks="" +if [ -f "$TOKENS_FILE" ]; then + t=$(cat "$TOKENS_FILE" 2>/dev/null || echo 0) + if [ "${t:-0}" -gt 0 ] 2>/dev/null; then + if [ "$t" -ge 1000000 ]; then toks=" 🪙$(( t / 1000000 ))M" + elif [ "$t" -ge 1000 ] ; then toks=" 🪙$(( t / 1000 ))k" + else toks=" 🪙${t}" + fi + fi +fi + +# ── mood face + color ─────────────────────────────────────────────────────── +idle=$(( now - since )) +if [ "$idle" -gt 300 ] && [ "$mood" != "angry" ] && [ "$mood" != "alert" ] && [ -z "${agents// }" ]; then + mood="sleep"; message="zzz" +fi case "$mood" in - happy) face="(ᵔᴥᵔ)"; color=$'\033[32m' ;; # green - proud) face="(•̀ᴗ•́)و"; color=$'\033[1;32m';; # bright green - thinking) face="(⊙.⊙)"; color=$'\033[36m' ;; # cyan - alert) face="(ʘᴗʘ)"; color=$'\033[33m' ;; # yellow - angry) face="(ò_ó)"; color=$'\033[31m' ;; # red - sad) face="(╥﹏╥)"; color=$'\033[34m' ;; # blue - sleep) face="(-.-)"; color=$'\033[2;37m';; # dim gray - *) face="(•ᴗ•)"; color=$'\033[37m' ;; # white (neutral) + happy) face="(ᵔᴥᵔ)"; color=$'\033[32m' ;; + proud) face="(•̀ᴗ•́)و"; color=$'\033[1;32m';; + thinking) face="(⊙.⊙)"; color=$'\033[36m' ;; + alert) face="(ʘᴗʘ)"; color=$'\033[33m' ;; + angry) face="(ò_ó)"; color=$'\033[31m' ;; + sad) face="(╥﹏╥)"; color=$'\033[34m' ;; + sleep) face="(-.-)"; color=$'\033[2;37m';; + *) face="(•ᴗ•)"; color=$'\033[37m' ;; esac -dim=$'\033[2m' -reset=$'\033[0m' - -# stats line (compact) +# ── counters ──────────────────────────────────────────────────────────────── stats="" -[ "$rust_today" -gt 0 ] && stats+=" 🦀${rust_today}" -[ "$patents_today" -gt 0 ] && stats+=" 📜${patents_today}" -[ "$violations" -gt 0 ] && stats+=" ⚠${violations}" +[ "${rust_today:-0}" -gt 0 ] 2>/dev/null && stats+=" 🦀${rust_today}" +[ "${patents_today:-0}" -gt 0 ] 2>/dev/null && stats+=" 📜${patents_today}" +[ "${violations:-0}" -gt 0 ] 2>/dev/null && stats+=" ⚠${violations}" -# Project name from PWD -proj="${PWD##*/}" -[ -z "$proj" ] && proj="~" +proj="${PWD##*/}"; [ -z "$proj" ] && proj="~" -# Render: face | message | stats | project -# Keep it ≤ one line -printf "%s%s%s %s%s%s%s%s %s%s%s" \ - "$color" "$face" "$reset" \ - "$dim" "$message" "$reset" \ - "$stats" \ - "" \ - "$dim" "📁 $proj" "$reset" +# ── render ONE line: [agents][tokens] [plan] face msg stats [lang] 📁proj ──── +out="" +[ -n "${agents// }" ] && out+="${agents# }${toks} " +[ -n "$plan" ] && out+="${plan} " +out+="${color}${face}${reset}" +[ -n "$message" ] && out+=" ${dim}${message}${reset}" +out+="${stats}" +[ -n "$lang" ] && out+=" ${lang}" +out+=" ${dim}📁 ${proj}${reset}" +printf '%s' "$out" diff --git a/settings-snippet.json b/settings-snippet.json index c0b383e..715fe22 100644 --- a/settings-snippet.json +++ b/settings-snippet.json @@ -26,7 +26,7 @@ }, { "type": "command", - "command": "FILE=$(cat | jq -r '.tool_input.file_path // empty'); [ -n \"$FILE\" ] && [ \"${FILE##*.}\" = 'rs' ] && ~/.claude/scripts/keisei-pet-update.sh rust_write; exit 0" + "command": "~/.claude/scripts/keisei-pet-update.sh lang" } ] }, @@ -62,6 +62,19 @@ "type": "command", "command": "~/.claude/hooks/agent-stub-scan.sh", "statusMessage": "STATUS-TRUTH marker scan (RULE 0.16)..." + }, + { + "type": "command", + "command": "~/.claude/scripts/keisei-pet-update.sh agent_done" + } + ] + }, + { + "matcher": "ExitPlanMode", + "hooks": [ + { + "type": "command", + "command": "~/.claude/scripts/keisei-pet-update.sh plan" } ] }, @@ -189,6 +202,10 @@ { "type": "command", "command": "~/.claude/hooks/task-timer.sh" + }, + { + "type": "command", + "command": "~/.claude/scripts/keisei-pet-update.sh agent_start" } ] }