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
7 changes: 6 additions & 1 deletion scripts/session-stop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PROFILE_DIR="$HOME/.code-sensei"
PROFILE_FILE="$PROFILE_DIR/profile.json"
SESSION_LOG="$PROFILE_DIR/sessions.log"
SESSION_STATE="$PROFILE_DIR/session-state.json"
TODAY=$(date -u +%Y-%m-%d)

if [ ! -f "$PROFILE_FILE" ]; then
Expand All @@ -27,8 +28,12 @@ if command -v jq &> /dev/null; then

# Show gentle reminder if they learned things but didn't recap
if [ "$SESSION_CONCEPTS" -gt 0 ]; then
echo "🥋 You encountered $SESSION_CONCEPTS new concepts this session! Use /code-sensei:recap next time for a full summary."
echo "You encountered $SESSION_CONCEPTS new concepts this session! Use /code-sensei:recap next time for a full summary."
fi
fi

# Clean up session state files
rm -f "$SESSION_STATE"
rm -f "$PROFILE_DIR/.jq-warned"

exit 0
53 changes: 50 additions & 3 deletions scripts/track-code-change.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
PROFILE_DIR="$HOME/.code-sensei"
PROFILE_FILE="$PROFILE_DIR/profile.json"
CHANGES_LOG="$PROFILE_DIR/session-changes.jsonl"
SESSION_STATE="$PROFILE_DIR/session-state.json"

# Rate limiting constants
RATE_LIMIT_INTERVAL=30 # minimum seconds between teaching triggers
SESSION_CAP=12 # maximum teaching triggers per session

# Read hook input from stdin
INPUT=$(cat)
Expand Down Expand Up @@ -62,15 +67,57 @@ if command -v jq &> /dev/null; then
fi
fi

# Always inject teaching context after code changes
# --- Rate limiting ---
NOW=$(date +%s)

# Read current session state (graceful default if missing)
if [ -f "$SESSION_STATE" ]; then
LAST_TRIGGER=$(jq -r '.last_trigger_time // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
TRIGGER_COUNT=$(jq -r '.trigger_count // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
else
LAST_TRIGGER=0
TRIGGER_COUNT=0
fi

# Enforce session cap (applies to everyone, including first-ever concepts)
if [ "$TRIGGER_COUNT" -ge "$SESSION_CAP" ]; then
echo "{}"
exit 0
fi

# Enforce minimum interval — first-ever concepts bypass this check only
ELAPSED=$(( NOW - LAST_TRIGGER ))
if [ "$ELAPSED" -lt "$RATE_LIMIT_INTERVAL" ] && [ "$IS_FIRST_EVER" != "true" ]; then
echo "{}"
exit 0
fi

# Update session state
NEW_COUNT=$(( TRIGGER_COUNT + 1 ))
SESSION_START_VAL=""
if [ -f "$SESSION_STATE" ]; then
SESSION_START_VAL=$(jq -r '.session_start // ""' "$SESSION_STATE" 2>/dev/null || echo "")
fi
if [ -z "$SESSION_START_VAL" ]; then
SESSION_START_VAL="$TIMESTAMP"
fi

jq -n \
--argjson last "$NOW" \
--argjson count "$NEW_COUNT" \
--arg start "$SESSION_START_VAL" \
'{"last_trigger_time": $last, "trigger_count": $count, "session_start": $start}' \
> "$SESSION_STATE"

# Build teaching context
BELT=$(jq -r '.belt // "white"' "$PROFILE_FILE" 2>/dev/null || echo "white")

if [ "$IS_FIRST_EVER" = "true" ]; then
# First-time encounter: micro-lesson about the technology
CONTEXT="🥋 CodeSensei micro-lesson trigger: The user just encountered '$TECH' for the FIRST TIME (file: $FILE_PATH). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $TECH is and why it matters for their project. Adapt language to their belt level. Keep it concise and non-intrusive — weave it naturally into your response, don't stop everything for a lecture."
CONTEXT="CodeSensei micro-lesson trigger: The user just encountered '$TECH' for the FIRST TIME (file: $FILE_PATH). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $TECH is and why it matters for their project. Adapt language to their belt level. Keep it concise and non-intrusive — weave it naturally into your response, don't stop everything for a lecture."
else
# Already-seen technology: inline insight about the specific change
CONTEXT="🥋 CodeSensei inline insight: Claude just used '$TOOL_NAME' on '$FILE_PATH' ($TECH). The user's belt level is '$BELT'. Provide a brief 1-2 sentence explanation of what this change does and why, adapted to their belt level. Keep it natural and non-intrusive — weave it into your response as a quick teaching moment."
CONTEXT="CodeSensei inline insight: Claude just used '$TOOL_NAME' on '$FILE_PATH' ($TECH). The user's belt level is '$BELT'. Provide a brief 1-2 sentence explanation of what this change does and why, adapted to their belt level. Keep it natural and non-intrusive — weave it into your response as a quick teaching moment."
fi

echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"$CONTEXT\"}}"
Expand Down
77 changes: 73 additions & 4 deletions scripts/track-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
PROFILE_DIR="$HOME/.code-sensei"
PROFILE_FILE="$PROFILE_DIR/profile.json"
COMMANDS_LOG="$PROFILE_DIR/session-commands.jsonl"
SESSION_STATE="$PROFILE_DIR/session-state.json"

# Rate limiting constants
RATE_LIMIT_INTERVAL=30 # minimum seconds between teaching triggers
SESSION_CAP=12 # maximum teaching triggers per session

# Trivial commands that should never generate teaching triggers
TRIVIAL_COMMANDS="cd ls pwd clear echo cat which man help exit history alias type file wc whoami hostname uname true false"

# Read hook input from stdin
INPUT=$(cat)
Expand All @@ -18,6 +26,25 @@ if command -v jq &> /dev/null; then
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // "unknown"')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Extract the base command (first word, ignoring leading whitespace)
BASE_CMD=$(echo "$COMMAND" | sed 's/^[[:space:]]*//' | awk '{print $1}' | sed 's|.*/||')

# Check if the command is trivial — skip teaching triggers entirely
IS_TRIVIAL="false"
for trivial in $TRIVIAL_COMMANDS; do
if [ "$BASE_CMD" = "$trivial" ]; then
IS_TRIVIAL="true"
break
fi
done

if [ "$IS_TRIVIAL" = "true" ]; then
# Log it but emit no teaching context
echo "{\"timestamp\":\"$TIMESTAMP\",\"command\":\"$(echo "$COMMAND" | head -c 200)\",\"concept\":\"\",\"skipped\":\"trivial\"}" >> "$COMMANDS_LOG"
echo "{}"
exit 0
fi

# Detect what kind of command this is for concept tracking
CONCEPT=""
case "$COMMAND" in
Expand Down Expand Up @@ -64,20 +91,62 @@ if command -v jq &> /dev/null; then
fi
fi

# Always inject teaching context after commands
# --- Rate limiting ---
NOW=$(date +%s)

# Read current session state (graceful default if missing)
if [ -f "$SESSION_STATE" ]; then
LAST_TRIGGER=$(jq -r '.last_trigger_time // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
TRIGGER_COUNT=$(jq -r '.trigger_count // 0' "$SESSION_STATE" 2>/dev/null || echo "0")
else
LAST_TRIGGER=0
TRIGGER_COUNT=0
fi

# Enforce session cap (applies to everyone, including first-ever concepts)
if [ "$TRIGGER_COUNT" -ge "$SESSION_CAP" ]; then
echo "{}"
exit 0
fi

# Enforce minimum interval — first-ever concepts bypass this check only
ELAPSED=$(( NOW - LAST_TRIGGER ))
if [ "$ELAPSED" -lt "$RATE_LIMIT_INTERVAL" ] && [ "$IS_FIRST_EVER" != "true" ]; then
echo "{}"
exit 0
fi

# Update session state
NEW_COUNT=$(( TRIGGER_COUNT + 1 ))
SESSION_START_VAL=""
if [ -f "$SESSION_STATE" ]; then
SESSION_START_VAL=$(jq -r '.session_start // ""' "$SESSION_STATE" 2>/dev/null || echo "")
fi
if [ -z "$SESSION_START_VAL" ]; then
SESSION_START_VAL="$TIMESTAMP"
fi

jq -n \
--argjson last "$NOW" \
--argjson count "$NEW_COUNT" \
--arg start "$SESSION_START_VAL" \
'{"last_trigger_time": $last, "trigger_count": $count, "session_start": $start}' \
> "$SESSION_STATE"

# Build teaching context
BELT=$(jq -r '.belt // "white"' "$PROFILE_FILE" 2>/dev/null || echo "white")
# Sanitize command for JSON (remove quotes and special chars)
SAFE_CMD=$(echo "$COMMAND" | head -c 80 | tr '"' "'" | tr '\\' '/')

if [ "$IS_FIRST_EVER" = "true" ] && [ -n "$CONCEPT" ]; then
# First-time encounter: micro-lesson about the concept
CONTEXT="🥋 CodeSensei micro-lesson trigger: The user just encountered '$CONCEPT' for the FIRST TIME (command: $SAFE_CMD). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $CONCEPT means and why it matters. Adapt language to their belt level. Keep it concise and non-intrusive."
CONTEXT="CodeSensei micro-lesson trigger: The user just encountered '$CONCEPT' for the FIRST TIME (command: $SAFE_CMD). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $CONCEPT means and why it matters. Adapt language to their belt level. Keep it concise and non-intrusive."
elif [ -n "$CONCEPT" ]; then
# Already-seen concept: brief inline insight about this specific command
CONTEXT="🥋 CodeSensei inline insight: Claude just ran a '$CONCEPT' command ($SAFE_CMD). The user's belt level is '$BELT'. Provide a brief 1-sentence explanation of what this command does, adapted to their belt level. Keep it natural and non-intrusive."
CONTEXT="CodeSensei inline insight: Claude just ran a '$CONCEPT' command ($SAFE_CMD). The user's belt level is '$BELT'. Provide a brief 1-sentence explanation of what this command does, adapted to their belt level. Keep it natural and non-intrusive."
else
# Unknown command type: still provide a brief hint
CONTEXT="🥋 CodeSensei inline insight: Claude just ran a shell command ($SAFE_CMD). The user's belt level is '$BELT'. If this command is educational, briefly explain what it does in 1 sentence. If trivial, skip the explanation."
CONTEXT="CodeSensei inline insight: Claude just ran a shell command ($SAFE_CMD). The user's belt level is '$BELT'. If this command is educational, briefly explain what it does in 1 sentence. If trivial, skip the explanation."
fi

echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"$CONTEXT\"}}"
Expand Down