Merge pull request 'feat(crunch): voice-notified reminders and AI crunch jobs' (#20) from feat/crunch-package into master

Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
2026-07-02 19:39:39 +02:00
2 changed files with 265 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
{
lib,
stdenv,
writeShellScriptBin,
coreutils,
bash,
}: let
script = writeShellScriptBin "crunch" ''
#!/usr/bin/env bash
set -euo pipefail
#
# crunch Voice-notified reminders & AI crunch jobs
# via systemd transient timers.
#
# Requires: talk (ElevenLabs TTS) in PATH, ELEVENLABS_API_KEY env.
# Optional: pi, opencode for AI crunch mode.
#
TALK="''${TALK:-talk}"
SELF="$(readlink -f "''${BASH_SOURCE[0]:-$0}")"
# Helpers
usage() {
cat <<'EOF'
crunch voice-notified reminders and AI jobs via systemd timers
Usage:
crunch at <time> <message> One-shot (14:30 or "2026-07-03 14:30")
crunch in <duration> <message> One-shot after delay (30m, 2h, 1h30m)
crunch daily <time> <message> Recurring daily at time
crunch weekly <day> <time> <message> Recurring weekly (Mon or Montag)
crunch ai <engine> <type> <time> <prompt> AI crunch (pi|opencode)
crunch list List active jobs
crunch cancel <name> Cancel a job
crunch purge Remove fired one-shot jobs
Examples:
crunch at "14:30" "Müll rausbringen"
crunch at "2026-07-03 10:00" "Zahnarzttermin"
crunch in "30m" "Build checken"
crunch daily "09:00" "Daily standup"
crunch weekly "Mon 09:00" "Weekly review"
crunch weekly "Montag 09:00" "Weekly review"
crunch ai pi in "1h" "Fasse neueste Commits zusammen"
crunch ai opencode daily "08:00" "Review offene Issues"
crunch list
crunch cancel mll-rausbringen-12345
Environment:
VOICE_NOTIFY=0 Silence voice output (passed to talk)
ELEVENLABS_API_KEY Required by talk
TALK Override path to talk binary
EOF
}
# German systemd day abbreviations
norm_day() {
local d="''${1,,}"
d="''${d%%.*}" # strip trailing dot (German abbreviation)
case "$d" in
mo|mon|monday|montag) echo "Mon" ;;
di|tue|tues|tuesday|dienstag) echo "Tue" ;;
mi|wed|weds|wednesday|mittwoch) echo "Wed" ;;
do|thu|thur|thurs|thursday|donnerstag) echo "Thu" ;;
fr|fri|friday|freitag) echo "Fri" ;;
sa|sat|saturday|samstag) echo "Sat" ;;
so|su|sun|sunday|sonntag) echo "Sun" ;;
*) echo "''${1^}" ;;
esac
}
make_name() {
local msg="''${1:?No message}"
local slug
slug="''${msg,,}"
slug="''${slug//[^a-z0-9 ]/}"
slug="''${slug// /-}"
slug="''${slug:0:25}"
slug="''${slug%-}" # trim trailing dash
echo "crunch-''${slug:-job}-$RANDOM"
}
# schedule <type> <time_spec> <exec_cmd> <name>
schedule() {
local type="$1" time_spec="$2" exec_cmd="$3" name="$4"
local timer_flag=""
case "$type" in
at)
if [[ "$time_spec" == *' '* ]]; then
# "2026-07-03 14:30" specific date
timer_flag="--on-calendar=''${time_spec}:00"
else
# "14:30" next matching time
timer_flag="--on-calendar=*-*-* ''${time_spec}:00"
fi
;;
in)
timer_flag="--on-active=''${time_spec}"
;;
daily)
timer_flag="--on-calendar=*-*-* ''${time_spec}:00"
;;
weekly)
local day time_part
day="$(norm_day "''${time_spec%% *}")"
time_part="''${time_spec##* }"
timer_flag="--on-calendar=''${day} *-*-* ''${time_part}:00"
;;
*)
echo "Unknown schedule type: $type" >&2
exit 1
;;
esac
systemd-run --user \
--unit="''${name}" \
--description="crunch job" \
"''${timer_flag}" \
bash -lc "''${exec_cmd}" \
&& echo " Scheduled: $name"
}
# Internal execution (called by systemd)
case "''${1:-}" in
_say)
shift
exec "$TALK" "$*"
;;
_ai)
engine="$2"; shift 2
case "$engine" in
pi|pi-agent)
pi -p "$*" | "$TALK"
;;
opencode|oc)
opencode run "$*" -q | "$TALK"
;;
*)
echo "Unknown engine: $engine (use: pi, opencode)" >&2
exit 1
;;
esac
exit $?
;;
esac
# CLI
[ $# -lt 1 ] && { usage; exit 0; }
cmd="$1"; shift
case "$cmd" in
# Simple voice reminders
at)
[ $# -lt 2 ] && { echo "Usage: crunch at <time> <message>" >&2; exit 1; }
time_spec="$1"; shift
msg="$*"
name="$(make_name "$msg")"
schedule at "$time_spec" "$SELF _say $(printf '%q' "$msg")" "$name"
;;
in)
[ $# -lt 2 ] && { echo "Usage: crunch in <duration> <message>" >&2; exit 1; }
duration="$1"; shift
msg="$*"
name="$(make_name "$msg")"
schedule in "$duration" "$SELF _say $(printf '%q' "$msg")" "$name"
;;
daily)
[ $# -lt 2 ] && { echo "Usage: crunch daily <time> <message>" >&2; exit 1; }
time_spec="$1"; shift
msg="$*"
name="$(make_name "$msg")"
schedule daily "$time_spec" "$SELF _say $(printf '%q' "$msg")" "$name"
;;
weekly)
[ $# -lt 2 ] && { echo "Usage: crunch weekly <day> <time> <message>" >&2; exit 1; }
day_time="$1"; shift
msg="$*"
name="$(make_name "$msg")"
schedule weekly "$day_time" "$SELF _say $(printf '%q' "$msg")" "$name"
;;
# AI crunch jobs
ai)
[ $# -lt 4 ] && { echo "Usage: crunch ai <pi|opencode> <at|in|daily|weekly> <time> <prompt>" >&2; exit 1; }
engine="$1"; sub_type="$2"; time_spec="$3"; shift 3
prompt="$*"
name="$(make_name "$prompt")"
schedule "$sub_type" "$time_spec" \
"$SELF _ai $engine $(printf '%q' "$prompt")" "$name"
;;
# Management
list)
echo "Active crunch jobs:"
echo ""
systemctl --user list-timers 'crunch-*' --no-pager 2>/dev/null \
| grep -E 'crunch-|^NEXT|^NEXT|^$' || echo " (none)"
;;
cancel)
[ $# -lt 1 ] && { echo "Usage: crunch cancel <name>" >&2; exit 1; }
name="$1"
# Accept with or without crunch- prefix
[[ "$name" != crunch-* ]] && name="crunch-$name"
systemctl --user stop "''${name}.timer" "''${name}.service" 2>/dev/null \
&& echo " Cancelled: $name" \
|| echo " Not found: $name"
;;
purge)
echo "Purging fired one-shot jobs..."
local purged=0
for unit in $(systemctl --user list-units 'crunch-*' --all --plain --no-legend --no-pager 2>/dev/null | awk '{print $1}'); do
if systemctl --user is-failed "$unit" &>/dev/null; then
systemctl --user stop "$unit" 2>/dev/null || true
systemctl --user reset-failed "$unit" 2>/dev/null || true
echo " Purged: $unit"
purged=$((purged + 1))
fi
done
echo " Purged $purged unit(s)"
;;
-h|--help|help)
usage
;;
*)
echo "Unknown command: $cmd" >&2
echo ""
usage
exit 1
;;
esac
'';
in
stdenv.mkDerivation {
pname = "crunch";
version = "0.1.0";
dontUnpack = true;
installPhase = ''
mkdir -p "$out/bin"
ln -s ${script}/bin/crunch "$out/bin/crunch"
'';
meta = with lib; {
description = "Voice-notified reminders and AI crunch jobs via systemd transient timers";
homepage = "https://code.m3ta.dev/m3tam3re/nixpkgs";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "crunch";
};
}
+1
View File
@@ -34,6 +34,7 @@ in {
zellij-ps = pkgs.callPackage ./zellij-ps {};
vibetyper = pkgs.callPackage ./vibetyper {};
talk = pkgs.callPackage ./talk {};
crunch = pkgs.callPackage ./crunch {};
# ── Pass-through packages ──────────────────────────────────────────
# Imported directly from flake inputs. No local modifications.