Add hook system.
This commit is contained in:
@@ -22,6 +22,101 @@ description = "Screenshot playpen in a throwaway headless compositor"
|
||||
depends = ["build-debug"]
|
||||
run = "nix develop --command ./shot.sh"
|
||||
|
||||
[tasks.install-claude-hooks]
|
||||
description = "Teach Claude Code to report its state to the tab it runs in"
|
||||
run = '''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
command -v jq >/dev/null || { echo "install-claude-hooks needs jq" >&2; exit 1; }
|
||||
|
||||
claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
settings="$claude/settings.json"
|
||||
script="$claude/hooks/playpen-status.sh"
|
||||
|
||||
mkdir -p "$claude/hooks"
|
||||
install -m755 hooks/playpen-status.sh "$script"
|
||||
|
||||
# Copied rather than referenced in place: hooks configured to run out of a
|
||||
# checkout break the day the checkout moves, and they break silently, in
|
||||
# every Claude session at once.
|
||||
[ -f "$settings" ] || echo '{}' > "$settings"
|
||||
|
||||
# Every entry carries a marker comment. It is what makes re-running this
|
||||
# replace the previous install instead of stacking a second copy, and it is
|
||||
# what `uninstall-claude-hooks` matches on. The shell ignores it.
|
||||
merged=$(jq --arg script "$script" '
|
||||
def marker: "# playpen-status-hook";
|
||||
|
||||
# Drop what a previous run added, leaving every other tools hooks alone.
|
||||
def clean:
|
||||
map(.hooks |= map(select((.command // "") | contains(marker) | not)))
|
||||
| map(select((.hooks | length) > 0));
|
||||
|
||||
def entry($state; $async):
|
||||
{ hooks: [
|
||||
{ type: "command",
|
||||
command: ($script + " " + $state + " " + marker),
|
||||
timeout: 5 }
|
||||
+ (if $async then { async: true } else {} end)
|
||||
] };
|
||||
|
||||
.hooks //= {}
|
||||
# UserPromptSubmit is the only one left synchronous: it reads the prompt off
|
||||
# stdin to build the tab title. The rest are a single printf and need not
|
||||
# hold the turn up at all.
|
||||
| .hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) | clean) + [entry("busy"; false)]
|
||||
| .hooks.Notification = ((.hooks.Notification // []) | clean) + [entry("input"; true)]
|
||||
| .hooks.Stop = ((.hooks.Stop // []) | clean) + [entry("idle"; true)]
|
||||
| .hooks.SessionStart = ((.hooks.SessionStart // []) | clean) + [entry("idle"; true)]
|
||||
| .hooks.SessionEnd = ((.hooks.SessionEnd // []) | clean) + [entry("idle"; true)]
|
||||
' "$settings")
|
||||
|
||||
# Keep a copy of what was there, and swap the new file in whole. Claude reads
|
||||
# this file constantly; a half-written one is a broken session.
|
||||
cp "$settings" "$settings.playpen-backup"
|
||||
printf '%s\n' "$merged" > "$settings.tmp"
|
||||
mv "$settings.tmp" "$settings"
|
||||
|
||||
echo "installed $script"
|
||||
echo "hooked into $settings (previous version kept at $settings.playpen-backup)"
|
||||
echo
|
||||
echo "Open a new Claude session to pick them up — hooks are read at startup."
|
||||
'''
|
||||
|
||||
[tasks.uninstall-claude-hooks]
|
||||
description = "Remove the Claude Code hooks, leaving any other tools' alone"
|
||||
run = '''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
command -v jq >/dev/null || { echo "uninstall-claude-hooks needs jq" >&2; exit 1; }
|
||||
|
||||
claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
settings="$claude/settings.json"
|
||||
[ -f "$settings" ] || { echo "no $settings; nothing to do"; exit 0; }
|
||||
|
||||
stripped=$(jq '
|
||||
def marker: "# playpen-status-hook";
|
||||
def clean:
|
||||
map(.hooks |= map(select((.command // "") | contains(marker) | not)))
|
||||
| map(select((.hooks | length) > 0));
|
||||
|
||||
.hooks //= {}
|
||||
| reduce ("UserPromptSubmit","Notification","Stop","SessionStart","SessionEnd") as $ev
|
||||
(.; .hooks[$ev] = ((.hooks[$ev] // []) | clean))
|
||||
# An event we emptied was ours alone; drop the key rather than leave "Stop": []
|
||||
| .hooks |= with_entries(select((.value | type) != "array" or (.value | length) > 0))
|
||||
' "$settings")
|
||||
|
||||
cp "$settings" "$settings.playpen-backup"
|
||||
printf '%s\n' "$stripped" > "$settings.tmp"
|
||||
mv "$settings.tmp" "$settings"
|
||||
|
||||
rm -f "$claude/hooks/playpen-status.sh"
|
||||
echo "removed the hooks from $settings (previous version kept at $settings.playpen-backup)"
|
||||
'''
|
||||
|
||||
[tasks.install]
|
||||
description = "Install playpen into ~/.local with a desktop entry and icon"
|
||||
depends = ["build"]
|
||||
|
||||
Reference in New Issue
Block a user