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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ chmod +x *.sh
# Interactive setup (recommended)
./setup-claude-cron.sh

# macOS auto-start on login (launchd)
./setup-claude-launchd.sh install

# OR manual daemon start
./claude-daemon-manager.sh start
./claude-daemon-manager.sh start --at "09:00" # with start time
Expand Down Expand Up @@ -140,6 +143,30 @@ chmod +x *.sh
./claude-daemon-manager.sh restart --at "09:00" --stop "17:00" # new schedule
```

### macOS Auto-Start With launchd

For macOS users who want the daemon to start automatically at login:

```bash
# Install auto-start using your current shell PATH
./setup-claude-launchd.sh install

# Install auto-start with daemon start arguments
./setup-claude-launchd.sh install -- --at "09:00" --stop "17:00"
./setup-claude-launchd.sh install -- --disableccusage

# Check launchd + daemon status
./setup-claude-launchd.sh status

# Remove auto-start
./setup-claude-launchd.sh uninstall
```

Notes:
- The launch agent triggers `./claude-daemon-manager.sh start` when you log in.
- It uses the current shell `PATH` when generating the plist, so `claude`, `ccusage`, `npx`, or `bunx` can still be found by launchd.
- If you move those commands to a different location later, run the install command again to refresh the LaunchAgent.

### Live Dashboard 📊

The new live dashboard provides real-time monitoring of your Claude renewal status:
Expand Down Expand Up @@ -319,7 +346,9 @@ cc-autorenew/
├── claude-auto-renew-daemon.sh # Core daemon process
├── claude-auto-renew-advanced.sh # Standalone renewal script
├── claude-auto-renew.sh # Basic renewal script
├── claude-launchd-bootstrap.sh # launchd helper to start the daemon once per login
├── setup-claude-cron.sh # Interactive setup (daemon/cron)
├── setup-claude-launchd.sh # macOS launchd auto-start installer
├── test-start-time-feature.sh # New comprehensive test suite
├── reddit.md # Reddit post about the project
└── README.md # This file
Expand Down Expand Up @@ -362,6 +391,18 @@ The daemon uses smart defaults, but you can modify behavior by editing `claude-a
tail -20 ~/.claude-auto-renew-daemon.log
```

### launchd auto-start not working (macOS)
```bash
# Check launchd + daemon status
./setup-claude-launchd.sh status

# Reinstall the LaunchAgent using your current shell PATH
./setup-claude-launchd.sh install

# Check launchd bootstrap logs
tail -20 ~/.claude-auto-renew-launchd.err.log
```

### ccusage not working
```bash
# Test ccusage directly
Expand Down
23 changes: 23 additions & 0 deletions claude-launchd-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# launchd helper: start the daemon if it is not already running.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DAEMON_MANAGER="$SCRIPT_DIR/claude-daemon-manager.sh"
PID_FILE="$HOME/.claude-auto-renew-daemon.pid"

if [ ! -x "$DAEMON_MANAGER" ]; then
echo "ERROR: claude-daemon-manager.sh not found or not executable at $DAEMON_MANAGER" >&2
exit 1
fi

if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE" 2>/dev/null)
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
exit 0
fi

rm -f "$PID_FILE"
fi

exec "$DAEMON_MANAGER" start "$@"
196 changes: 196 additions & 0 deletions setup-claude-launchd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/bin/bash

# Setup script for macOS launchd auto-start.

set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DAEMON_MANAGER="$SCRIPT_DIR/claude-daemon-manager.sh"
BOOTSTRAP_SCRIPT="$SCRIPT_DIR/claude-launchd-bootstrap.sh"
LABEL="com.ccautorenew.launchd"
PLIST_DIR="$HOME/Library/LaunchAgents"
PLIST_PATH="$PLIST_DIR/$LABEL.plist"
STDOUT_LOG="$HOME/.claude-auto-renew-launchd.out.log"
STDERR_LOG="$HOME/.claude-auto-renew-launchd.err.log"
LAUNCHD_DOMAIN="gui/$(id -u)"
ACTION="install"
EXTRA_ARGS=()

print_help() {
cat <<'EOF'
Usage:
./setup-claude-launchd.sh install [-- daemon start args...]
./setup-claude-launchd.sh status
./setup-claude-launchd.sh uninstall

Examples:
./setup-claude-launchd.sh install
./setup-claude-launchd.sh install -- --at "09:00" --stop "17:00"
./setup-claude-launchd.sh install -- --disableccusage
./setup-claude-launchd.sh status
./setup-claude-launchd.sh uninstall

Notes:
- This is for macOS only.
- The launch agent runs at login and triggers the normal daemon manager.
- Any arguments after -- are passed to:
./claude-daemon-manager.sh start
EOF
}

xml_escape() {
local value="$1"
value=${value//&/&amp;}
value=${value//</&lt;}
value=${value//>/&gt;}
value=${value//\"/&quot;}
value=${value//\'/&apos;}
printf '%s' "$value"
}

ensure_supported() {
if [ "$(uname -s)" != "Darwin" ]; then
echo "ERROR: launchd setup is only supported on macOS."
exit 1
fi

if [ ! -x "$DAEMON_MANAGER" ]; then
echo "ERROR: claude-daemon-manager.sh not found or not executable at $DAEMON_MANAGER"
exit 1
fi

if [ ! -x "$BOOTSTRAP_SCRIPT" ]; then
echo "ERROR: claude-launchd-bootstrap.sh not found or not executable at $BOOTSTRAP_SCRIPT"
exit 1
fi
}

is_loaded() {
launchctl print "$LAUNCHD_DOMAIN/$LABEL" >/dev/null 2>&1
}

write_plist() {
mkdir -p "$PLIST_DIR"

{
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
printf '%s\n' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
printf '%s\n' '<plist version="1.0">'
printf '%s\n' '<dict>'
printf ' <key>Label</key>\n'
printf ' <string>%s</string>\n' "$(xml_escape "$LABEL")"
printf '\n'
printf ' <key>ProgramArguments</key>\n'
printf ' <array>\n'
printf ' <string>%s</string>\n' "$(xml_escape "$BOOTSTRAP_SCRIPT")"

if [ ${#EXTRA_ARGS[@]} -gt 0 ]; then
for arg in "${EXTRA_ARGS[@]}"; do
printf ' <string>%s</string>\n' "$(xml_escape "$arg")"
done
fi

printf ' </array>\n'
printf '\n'
printf ' <key>WorkingDirectory</key>\n'
printf ' <string>%s</string>\n' "$(xml_escape "$SCRIPT_DIR")"
printf '\n'
printf ' <key>EnvironmentVariables</key>\n'
printf ' <dict>\n'
printf ' <key>PATH</key>\n'
printf ' <string>%s</string>\n' "$(xml_escape "$PATH")"
printf ' </dict>\n'
printf '\n'
printf ' <key>RunAtLoad</key>\n'
printf ' <true/>\n'
printf '\n'
printf ' <key>StandardOutPath</key>\n'
printf ' <string>%s</string>\n' "$(xml_escape "$STDOUT_LOG")"
printf ' <key>StandardErrorPath</key>\n'
printf ' <string>%s</string>\n' "$(xml_escape "$STDERR_LOG")"
printf '%s\n' '</dict>'
printf '%s\n' '</plist>'
} > "$PLIST_PATH"
}

install_launchd() {
write_plist

launchctl bootout "$LAUNCHD_DOMAIN" "$PLIST_PATH" >/dev/null 2>&1 || true
launchctl bootstrap "$LAUNCHD_DOMAIN" "$PLIST_PATH"

echo "✅ launchd auto-start installed"
echo "Plist: $PLIST_PATH"
echo "The daemon will be started automatically when you log in."
echo ""
"$DAEMON_MANAGER" status || true
}

uninstall_launchd() {
launchctl bootout "$LAUNCHD_DOMAIN" "$PLIST_PATH" >/dev/null 2>&1 || true
rm -f "$PLIST_PATH"

echo "✅ launchd auto-start removed"
echo "The daemon itself is not stopped automatically."
echo "Use ./claude-daemon-manager.sh stop if you also want to stop the current daemon."
}

status_launchd() {
if [ -f "$PLIST_PATH" ]; then
echo "LaunchAgent installed: $PLIST_PATH"
else
echo "LaunchAgent installed: no"
fi

if is_loaded; then
echo "LaunchAgent loaded: yes"
else
echo "LaunchAgent loaded: no"
fi

echo ""
"$DAEMON_MANAGER" status || true
}

while [ $# -gt 0 ]; do
case "$1" in
install|status|uninstall)
ACTION="$1"
shift
;;
help|-h|--help)
ACTION="help"
shift
;;
--)
shift
EXTRA_ARGS=("$@")
break
;;
*)
echo "ERROR: Unknown argument: $1"
echo ""
print_help
exit 1
;;
esac
done

if [ "$ACTION" = "help" ]; then
print_help
exit 0
fi

ensure_supported

case "$ACTION" in
install)
install_launchd
;;
status)
status_launchd
;;
uninstall)
uninstall_launchd
;;
esac