diff --git a/README.md b/README.md index e4de615..1384a2e 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Install or disable them dynamically with the `/plugin` command β€” enabling you - [infrastructure-maintainer](./plugins/infrastructure-maintainer) - [monitoring-observability-specialist](./plugins/monitoring-observability-specialist) - [n8n-workflow-builder](./plugins/n8n-workflow-builder) +- [screenshot-janitor](./plugins/screenshot-janitor) ### Business Sales - [b2b-project-shipper](./plugins/b2b-project-shipper) diff --git a/plugins/screenshot-janitor/.claude-plugin/plugin.json b/plugins/screenshot-janitor/.claude-plugin/plugin.json new file mode 100644 index 0000000..7c3b54c --- /dev/null +++ b/plugins/screenshot-janitor/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "screenshot-janitor", + "version": "1.0.0", + "description": "Finds screenshots that pile up during each Claude Code session and, before you leave, asks to move them to the Trash. macOS + Linux. Per-session scoping, safe (recoverable) deletion, zero dependencies.", + "author": { "name": "MECoban", "url": "https://github.com/MECoban" }, + "homepage": "https://github.com/MECoban/screenshot-janitor", + "repository": "https://github.com/MECoban/screenshot-janitor", + "license": "MIT", + "keywords": [ + "screenshot", + "cleanup", + "housekeeping", + "trash", + "desktop", + "macos", + "linux", + "hooks", + "skill" + ] +} diff --git a/plugins/screenshot-janitor/LICENSE b/plugins/screenshot-janitor/LICENSE new file mode 100644 index 0000000..d5dcb55 --- /dev/null +++ b/plugins/screenshot-janitor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MECoban + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/screenshot-janitor/README.md b/plugins/screenshot-janitor/README.md new file mode 100644 index 0000000..db16eab --- /dev/null +++ b/plugins/screenshot-janitor/README.md @@ -0,0 +1,92 @@ +# 🧹 screenshot-janitor + +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue) +![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-8A2BE2) +[![GitHub stars](https://img.shields.io/github/stars/MECoban/screenshot-janitor?style=social)](https://github.com/MECoban/screenshot-janitor/stargazers) + +A [Claude Code](https://claude.com/claude-code) plugin that keeps your Desktop clean. + +You share a lot of screenshots with Claude during a session β€” and they pile up +forever afterward. **screenshot-janitor** notices the screenshots created during +each session and, before you leave, **asks** whether to move them to the Trash. +Approve, and they're gone (recoverable from the Trash). Decline, and it won't +nag you again. + +> Safe by design: it **never** runs `rm`. Files go to the system Trash and can be restored. + +## Demo + + + + +> πŸŽ₯ _Demo GIF coming soon._ The end-of-session flow: Claude asks **"move these N screenshots to the Trash?"** β†’ you approve β†’ done. + +--- + +## Install + +```text +/plugin marketplace add MECoban/screenshot-janitor +/plugin install screenshot-janitor +``` + +That's it. The skill **and** the hooks are wired up automatically β€” no manual +`settings.json` editing. Restart Claude Code (or open `/hooks` once) to activate. + +## How it works + +| Piece | What it does | +|-------|--------------| +| `SessionStart` hook | Opens a per-session folder and records the start time, so each session only deals with **its own** screenshots. | +| `Stop` hook | If this session created screenshots, reminds you **once** to clean them up before you go. | +| `cleanup-screenshots` skill | Finds the screenshots, shows a clear list (name, size, time), asks for approval, and moves the approved ones to the Trash. | + +You can also run it any time: + +```text +/cleanup-screenshots +``` + +## Platforms + +- **macOS** β€” scans `~/Desktop`, recognizes `Screenshot *`, `Ekran Resmi *`, + `CleanShot *`, etc. Trashes via the `trash` CLI or Finder/AppleScript. +- **Linux** β€” scans `~/Pictures/Screenshots`, `~/Pictures`, `~/Desktop`, + recognizes GNOME/KDE naming. Trashes via `gio trash` or `trash-cli`. + +Zero runtime dependencies (no `jq`, no Python) β€” just `bash`, `find`, `stat`. + +## Configuration + +Don't edit the plugin files (they're overwritten on update). Instead create +`~/.claude/screenshot-janitor/config.sh` and override what you need: + +```bash +# Extra folders to scan +SCAN_DIRS=( "$HOME/Desktop" "$HOME/Pictures/Screenshots" ) + +# Filename globs that count as screenshots +NAME_PATTERNS=( "Screenshot *" "CleanShot *" ) + +# Don't remind until the session is at least this old (seconds). 0 = remind ASAP. +MIN_AGE_SECONDS=300 +``` + +## State & privacy + +Per-session bookkeeping lives in `~/.claude/screenshot-janitor/`. Nothing leaves +your machine. The plugin reads filenames/sizes/timestamps only β€” never the image +contents. + +## Uninstall + +```text +/plugin uninstall screenshot-janitor +``` + +Optionally remove leftover state: `rm -rf ~/.claude/screenshot-janitor`. + +## License + +MIT Β© MECoban diff --git a/plugins/screenshot-janitor/hooks/hooks.json b/plugins/screenshot-janitor/hooks/hooks.json new file mode 100644 index 0000000..748b081 --- /dev/null +++ b/plugins/screenshot-janitor/hooks/hooks.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.sh\"" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-hook.sh\"" + } + ] + } + ] + } +} diff --git a/plugins/screenshot-janitor/scripts/config.sh b/plugins/screenshot-janitor/scripts/config.sh new file mode 100755 index 0000000..d22d486 --- /dev/null +++ b/plugins/screenshot-janitor/scripts/config.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# screenshot-janitor β€” default configuration. +# +# Do NOT edit this file: it is overwritten on plugin updates. +# To customize, create ~/.claude/screenshot-janitor/config.sh and redefine +# any of the variables below (SCAN_DIRS, NAME_PATTERNS, MIN_AGE_SECONDS). + +if [ "$(uname -s)" = "Darwin" ]; then + # macOS: screenshots land on the Desktop by default. + SCAN_DIRS=( "$HOME/Desktop" ) + NAME_PATTERNS=( + "Screenshot *" + "Screen Shot *" + "Ekran Resmi *" + "Ekran GΓΆrΓΌntΓΌsΓΌ *" + "CleanShot *" + ) +else + # Linux: GNOME/KDE/others use a variety of folders and names. + SCAN_DIRS=( + "$HOME/Pictures/Screenshots" + "$HOME/Pictures" + "$HOME/Desktop" + ) + NAME_PATTERNS=( + "Screenshot from *" + "Screenshot_*" + "Screenshot-*" + "Screenshot *" + "screenshot*" + "Bildschirmfoto *" + "Ekran*" + ) +fi + +# The Stop hook only reminds once the session has lived at least this many +# seconds β€” avoids nagging in the very first turns. Set to 0 to remind ASAP. +MIN_AGE_SECONDS=300 + +# Optional user override (kept outside the plugin so updates don't clobber it). +_sj_user_cfg="${SJ_STATE_DIR:-$HOME/.claude/screenshot-janitor}/config.sh" +# shellcheck disable=SC1090 +[ -f "$_sj_user_cfg" ] && source "$_sj_user_cfg" diff --git a/plugins/screenshot-janitor/scripts/find.sh b/plugins/screenshot-janitor/scripts/find.sh new file mode 100755 index 0000000..fbda81a --- /dev/null +++ b/plugins/screenshot-janitor/scripts/find.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Finds the screenshots that belong to the active (or most recent) session. +# Usage: find.sh [session_id] +# - If omitted, uses $CLAUDE_SESSION_ID, else the most recent session. +# Output: +# SESSION_META: +# STARTED_AT: +# followed by scan.sh TSV lines (mtime\tsize\tpath) +# or the single line NO_SESSION if no session state exists. +set -u +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/lib.sh" + +sid="${1:-${CLAUDE_SESSION_ID:-}}" +meta="" +if [ -n "$sid" ] && [ -f "$SJ_STATE_DIR/sessions/$sid/meta.json" ]; then + meta="$SJ_STATE_DIR/sessions/$sid/meta.json" +else + meta="$(ls -t "$SJ_STATE_DIR"/sessions/*/meta.json 2>/dev/null | head -1)" +fi + +[ -n "$meta" ] && [ -f "$meta" ] || { echo "NO_SESSION"; exit 0; } + +started="$(sj_json_num "$(cat "$meta")" started_at)" +echo "SESSION_META:$meta" +echo "STARTED_AT:${started:-0}" +bash "$DIR/scan.sh" "${started:-0}" diff --git a/plugins/screenshot-janitor/scripts/lib.sh b/plugins/screenshot-janitor/scripts/lib.sh new file mode 100755 index 0000000..d12e84e --- /dev/null +++ b/plugins/screenshot-janitor/scripts/lib.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# screenshot-janitor β€” shared helpers. Abstracts macOS/Linux differences. +# No external dependencies (no jq, no python). + +# Where per-session state lives (writable, persistent across the session). +SJ_STATE_DIR="${SJ_STATE_DIR:-$HOME/.claude/screenshot-janitor}" + +sj_os() { + case "$(uname -s)" in + Darwin) echo "macos" ;; + Linux) echo "linux" ;; + *) echo "other" ;; + esac +} + +# File modification time as a Unix epoch. +sj_mtime() { # + if [ "$(uname -s)" = "Darwin" ]; then stat -f '%m' "$1" 2>/dev/null + else stat -c '%Y' "$1" 2>/dev/null; fi +} + +# File size in bytes. +sj_size() { # + if [ "$(uname -s)" = "Darwin" ]; then stat -f '%z' "$1" 2>/dev/null + else stat -c '%s' "$1" 2>/dev/null; fi +} + +# Extract a JSON string field value (best-effort, dependency-free). +sj_json_str() { # + printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' +} + +# Extract a JSON number field value. +sj_json_num() { # + printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*[0-9]*" | grep -o '[0-9]*' | head -1 +} + +# Move files to the Trash (NEVER permanent rm). Returns 0 on success. +# Tries, in order: `trash` CLI -> macOS Finder/AppleScript -> `gio trash` -> `trash-put`. +sj_trash() { # + [ "$#" -gt 0 ] || return 0 + if command -v trash >/dev/null 2>&1; then + trash "$@"; return $? + fi + if [ "$(uname -s)" = "Darwin" ]; then + local f rc=0 + for f in "$@"; do + osascript -e "tell application \"Finder\" to delete (POSIX file \"$f\")" >/dev/null 2>&1 || rc=1 + done + return $rc + fi + if command -v gio >/dev/null 2>&1; then + gio trash "$@"; return $? + fi + if command -v trash-put >/dev/null 2>&1; then + trash-put "$@"; return $? + fi + echo "ERROR: no Trash command found. Install one: macOS 'brew install trash', Linux 'gio' or 'trash-cli'." >&2 + return 2 +} diff --git a/plugins/screenshot-janitor/scripts/scan.sh b/plugins/screenshot-janitor/scripts/scan.sh new file mode 100755 index 0000000..42e8656 --- /dev/null +++ b/plugins/screenshot-janitor/scripts/scan.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Usage: scan.sh +# Prints screenshot files (in configured dirs) modified at/after +# as TSV: \t\t +# since_epoch=0 returns every matching screenshot. +set -u +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/lib.sh" +# shellcheck disable=SC1091 +source "$DIR/config.sh" + +since="${1:-0}" + +for d in "${SCAN_DIRS[@]}"; do + [ -d "$d" ] || continue + args=() + first=1 + for p in "${NAME_PATTERNS[@]}"; do + if [ "$first" -eq 1 ]; then + args+=( -iname "$p" ); first=0 + else + args+=( -o -iname "$p" ) + fi + done + find "$d" -maxdepth 1 -type f \( "${args[@]}" \) -print0 2>/dev/null +done | while IFS= read -r -d '' f; do + m="$(sj_mtime "$f")" + [ -n "$m" ] || continue + if [ "$m" -ge "$since" ]; then + s="$(sj_size "$f")" + printf '%s\t%s\t%s\n' "$m" "${s:-0}" "$f" + fi +done diff --git a/plugins/screenshot-janitor/scripts/session-start.sh b/plugins/screenshot-janitor/scripts/session-start.sh new file mode 100755 index 0000000..c92e994 --- /dev/null +++ b/plugins/screenshot-janitor/scripts/session-start.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# SessionStart hook β€” opens a per-session folder and records the start time. +# That timestamp is how "screenshots created during THIS session" are scoped. +# On resume, the existing start time is preserved. +set -u +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/lib.sh" + +input="$(cat)" +sid="$(sj_json_str "$input" session_id)" +cwd="$(sj_json_str "$input" cwd)" +[ -z "$sid" ] && sid="unknown-$(date +%s)" + +mkdir -p "$SJ_STATE_DIR" +# Record where the scripts live so the skill can find them regardless of how +# it is invoked (works even if CLAUDE_PLUGIN_ROOT is not set in skill context). +printf '%s\n' "$DIR" > "$SJ_STATE_DIR/scripts-path" + +d="$SJ_STATE_DIR/sessions/$sid" +mkdir -p "$d" +if [ ! -f "$d/meta.json" ]; then + printf '{"session_id":"%s","started_at":%s,"cwd":"%s"}\n' "$sid" "$(date +%s)" "$cwd" > "$d/meta.json" +fi +exit 0 diff --git a/plugins/screenshot-janitor/scripts/stop-hook.sh b/plugins/screenshot-janitor/scripts/stop-hook.sh new file mode 100755 index 0000000..99acff0 --- /dev/null +++ b/plugins/screenshot-janitor/scripts/stop-hook.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Stop hook β€” runs at the end of a response turn. If this session produced new +# screenshots, it injects an instruction telling the assistant to offer cleanup. +# Reminds ONCE per session (reminded marker); stays silent after cleanup (handled). +set -u +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/lib.sh" +# shellcheck disable=SC1091 +source "$DIR/config.sh" + +input="$(cat)" +sid="$(sj_json_str "$input" session_id)" +[ -z "$sid" ] && exit 0 + +d="$SJ_STATE_DIR/sessions/$sid" +[ -f "$d/meta.json" ] || exit 0 +[ -f "$d/handled" ] && exit 0 +[ -f "$d/reminded" ] && exit 0 + +started="$(sj_json_num "$(cat "$d/meta.json")" started_at)" +[ -z "$started" ] && exit 0 + +age=$(( $(date +%s) - started )) +[ "$age" -lt "${MIN_AGE_SECONDS:-0}" ] && exit 0 + +count="$(bash "$DIR/scan.sh" "$started" | wc -l | tr -d ' ')" +if [ "${count:-0}" -gt 0 ]; then + touch "$d/reminded" + # ASCII-only (no double quotes / backslashes / newlines) so the JSON stays valid. + reason="This Claude Code session produced $count new screenshot(s). Before the user leaves, ask them in ONE short sentence whether to move these to the Trash. If they agree, run the cleanup-screenshots skill. If they decline, do not ask again; this reminder fires only once per session. Respond in the user configured language." + printf '{"decision":"block","reason":"%s"}\n' "$reason" +fi +exit 0 diff --git a/plugins/screenshot-janitor/scripts/trash.sh b/plugins/screenshot-janitor/scripts/trash.sh new file mode 100755 index 0000000..196f2ce --- /dev/null +++ b/plugins/screenshot-janitor/scripts/trash.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Moves the given files to the Trash (cross-platform, never permanent rm). +# Usage: trash.sh [ ...] +set -u +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/lib.sh" +sj_trash "$@" diff --git a/plugins/screenshot-janitor/skills/cleanup-screenshots/SKILL.md b/plugins/screenshot-janitor/skills/cleanup-screenshots/SKILL.md new file mode 100644 index 0000000..8132b35 --- /dev/null +++ b/plugins/screenshot-janitor/skills/cleanup-screenshots/SKILL.md @@ -0,0 +1,88 @@ +--- +name: cleanup-screenshots +description: Find screenshots shared/accumulated during this Claude Code session and, with the user's approval, move them to the Trash (no permanent delete β€” recoverable). Use before ending a session, when asked to tidy the Desktop/screenshots folder, or when the screenshot-janitor Stop hook suggests cleanup. Triggers include "clean up screenshots", "delete screenshots", "tidy my desktop", "remove the screenshots". +--- + +# Screenshot Janitor β€” Cleanup + +Cleans up screenshots that piled up during this session **safely**: never `rm`; +files are moved to the **Trash** and can be restored. Session scope comes from the +`started_at` timestamp recorded by the SessionStart hook (only screenshots created +during this session). + +Helper scripts live next to this plugin. Resolve their location like this (works +whether or not `CLAUDE_PLUGIN_ROOT` is set in skill context): + +```bash +SCRIPTS="$(cat "$HOME/.claude/screenshot-janitor/scripts-path" 2>/dev/null)" +[ -z "$SCRIPTS" ] && SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts" +``` + +## Flow + +### 1. Find candidates + +```bash +bash "$SCRIPTS/find.sh" +``` + +Interpret the output: +- `NO_SESSION` β†’ no session record. Ask the user whether to scan **all** screenshots; + if yes, run `bash "$SCRIPTS/scan.sh" 0`. +- `SESSION_META:` β†’ **remember this path**; you will write the `handled` + marker into its directory in step 5. +- Following TSV lines are candidates: `\t\t`. +- If there are no TSV lines: say "No screenshots to clean up for this session πŸ‘" and stop. + +### 2. Show a clear list + +Present the candidates as a numbered, human-readable table. Convert bytes to +MB/KB and the mtime epoch to a readable time. Example: + +``` +Found 3 screenshots from this session: + + 1. Screenshot 2026-06-07 at 04.44.07.png 3.6 MB 04:44 + 2. Screenshot 2026-06-07 at 04.44.17.png 3.4 MB 04:44 + 3. Screenshot 2026-06-07 at 15.49.25.png 5 KB 15:49 + +Total ~7.0 MB. +``` + +### 3. Ask for approval + +Offer clear options and **wait** for the answer: +- **All** β†’ move every candidate to the Trash. +- **Some** β†’ "tell me the numbers, e.g. 1,3" β†’ only those. +- **Cancel** β†’ do nothing. + +NEVER move files without approval. + +### 4. Move to Trash + +For the approved files (not permanent deletion!): + +```bash +bash "$SCRIPTS/trash.sh" "/full/path/Screenshot ....png" "/another/file.png" +``` + +`trash.sh` is cross-platform (macOS Trash / Linux `gio trash` / `trash-cli`). +Always double-quote paths (they contain spaces). Never use `rm`. + +### 5. Mark handled and report + +After a successful cleanup, mark the session so it won't remind again (use the +`SESSION_META` path from step 1): + +```bash +touch "$(dirname '')/handled" +``` + +Then give a short summary: how many files, how much space freed, and that they +went to the Trash ("you can restore from Trash if needed"). Mention any skipped files. + +## Notes +- Configuration: create `~/.claude/screenshot-janitor/config.sh` to override + `SCAN_DIRS`, `NAME_PATTERNS`, or `MIN_AGE_SECONDS` (don't edit plugin files). +- This skill also works standalone; it does not require the Stop hook. +- Respond in the user's configured language.