add alias to avoid explicit python commands, scheduled runs and bug fixes

This commit is contained in:
JesseMarkowitz
2026-08-18 09:59:15 -04:00
parent fe5ed341ad
commit 999429f61e
9 changed files with 737 additions and 8 deletions
+83
View File
@@ -0,0 +1,83 @@
<#
.SYNOPSIS
Register a Windows scheduled task that runs `ai-chat-exporter sync` daily.
.DESCRIPTION
The Windows counterpart to install-systemd-timer.sh. Creates a per-user task
(no admin rights needed) that runs the exporter from this repository, with
the working directory set to the repo so .env, cache\ and exports\ resolve
exactly as they do for an interactive run.
-Provider is repeatable. The CLI takes one provider per run, so each becomes
its own action, executed in order. List only the providers that work
unattended on this machine: a web provider whose session token has expired
fails the task every day, which trains you to ignore the failures you
actually want to notice.
.EXAMPLE
.\scheduling\Register-AiChatSyncTask.ps1 -Provider chatgpt,claude
.EXAMPLE
.\scheduling\Register-AiChatSyncTask.ps1 -Provider all -Time 21:30
.EXAMPLE
.\scheduling\Register-AiChatSyncTask.ps1 -Unregister
#>
[CmdletBinding()]
param(
[string[]]$Provider = @('all'),
[string]$Time = '09:00',
[string]$TaskName = 'AiChatExporterSync',
[switch]$Unregister
)
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
$launcher = Join-Path $repo 'ai-chat-exporter.cmd'
if ($Unregister) {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Host "Removed scheduled task '$TaskName'."
return
}
if (-not (Test-Path $launcher)) {
throw "Launcher not found at $launcher"
}
# A single action looping over the providers, rather than one action each.
# Task Scheduler runs multiple actions in order but reports only the last one's
# result, so a failure in an earlier provider would be invisible. The loop keeps
# going after a failure and propagates a non-zero exit code.
$loop = ($Provider | ForEach-Object { "`"$launcher`" sync --provider $_ --joplin-optional || set RC=1" }) -join ' & '
$actions = New-ScheduledTaskAction -Execute 'cmd.exe' `
-Argument "/c set RC=0 & $loop & exit /b %RC%" `
-WorkingDirectory $repo
$trigger = New-ScheduledTaskTrigger -Daily -At $Time
# StartWhenAvailable is the counterpart of systemd's Persistent=true: a machine
# that was asleep at the scheduled time runs the archive when it wakes, rather
# than skipping the day entirely.
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-DontStopIfGoingOnBatteries `
-AllowStartIfOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
Register-ScheduledTask -TaskName $TaskName `
-Action $actions `
-Trigger $trigger `
-Settings $settings `
-Description 'Export AI chat history and sync it to Joplin' `
-Force | Out-Null
Write-Host "Registered '$TaskName' - daily at $Time for: $($Provider -join ', ')"
Write-Host ''
Write-Host 'Next steps:'
Write-Host " * Run it once now: Start-ScheduledTask -TaskName $TaskName"
Write-Host " * Check the result: Get-ScheduledTaskInfo -TaskName $TaskName"
Write-Host " * Read the log: Get-Content '$repo\cache\logs\exporter.log' -Tail 50"
Write-Host ' * The terms-of-service notice must have been acknowledged'
Write-Host ' interactively once on this machine, or the task exits 1.'
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Install a systemd *user* timer that runs `ai-chat-exporter sync` on a schedule.
#
# ./scheduling/install-systemd-timer.sh --provider claude-code --provider codex
# ./scheduling/install-systemd-timer.sh --provider all --time 21:30
# ./scheduling/install-systemd-timer.sh --uninstall
#
# User units (not system units) are the right scope: the archive is per-user,
# the .env holds that user's session tokens, and Joplin runs in their session.
#
# --provider is repeatable. The CLI takes one provider per run, so each one
# becomes its own ExecStart line, executed in order. Prefer listing only the
# providers that actually work unattended on this machine — a web provider with
# an expired session token will fail the unit every single day, which trains you
# to ignore the failure you actually want to see.
set -euo pipefail
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
UNIT_DIR="$HOME/.config/systemd/user"
NAME="aichat-sync"
TIME="09:00"
PROVIDERS=()
UNINSTALL=0
while [ $# -gt 0 ]; do
case "$1" in
--provider) PROVIDERS+=("$2"); shift 2 ;;
--time) TIME="$2"; shift 2 ;;
--name) NAME="$2"; shift 2 ;;
--uninstall) UNINSTALL=1; shift ;;
-h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}"; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 1 ;;
esac
done
if [ "$UNINSTALL" -eq 1 ]; then
systemctl --user disable --now "$NAME.timer" 2>/dev/null || true
rm -f "$UNIT_DIR/$NAME.timer" "$UNIT_DIR/$NAME.service"
systemctl --user daemon-reload
echo "Removed $NAME.timer and $NAME.service."
exit 0
fi
[ ${#PROVIDERS[@]} -eq 0 ] && PROVIDERS=("all")
if [ ! -x "$REPO/ai-chat-exporter" ]; then
echo "error: $REPO/ai-chat-exporter is missing or not executable." >&2
exit 1
fi
mkdir -p "$UNIT_DIR"
# WorkingDirectory is the point of the whole unit: .env, cache/ and exports/ all
# resolve against it, so the scheduled run writes to the same archive an
# interactive run from this directory would.
{
echo "[Unit]"
echo "Description=AI chat archive sync"
echo "Documentation=file://$REPO/README.md"
echo "After=network-online.target"
echo "Wants=network-online.target"
echo
echo "[Service]"
echo "Type=oneshot"
echo "WorkingDirectory=$REPO"
echo "Environment=AI_CHAT_EXPORTER_QUIET_CWD=1"
# One ExecStart per provider would stop at the first failure, silently
# skipping the rest — an expired ChatGPT token would mean codex never runs.
# Loop instead, so every provider is attempted and the unit still reports
# failure if any of them failed.
printf 'ExecStart=/bin/sh -c '\''rc=0; for p in %s; do "$0" sync --provider "$p" --joplin-optional || rc=1; done; exit $rc'\'' %s\n' \
"${PROVIDERS[*]}" "$REPO/ai-chat-exporter"
} > "$UNIT_DIR/$NAME.service"
# Persistent=true runs a missed schedule at the next boot — the machine being
# off at 09:00 should delay the archive, not skip it. RandomizedDelaySec keeps
# the web providers from being hit at exactly the same second every day.
cat > "$UNIT_DIR/$NAME.timer" <<EOF
[Unit]
Description=Run the AI chat archive sync daily
[Timer]
OnCalendar=*-*-* $TIME:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now "$NAME.timer"
echo "Installed $NAME.timer — daily at $TIME for: ${PROVIDERS[*]}"
echo
systemctl --user list-timers "$NAME.timer" --no-pager || true
cat <<EOF
Next steps:
• Timers only run while you have a session. To archive when logged out:
loginctl enable-linger $USER
• Run it once by hand to confirm:
systemctl --user start $NAME.service
• Read the log:
journalctl --user -u $NAME.service -n 50
• The terms-of-service notice must have been acknowledged interactively at
least once on this machine, or the unit exits 1 with an explanation.
EOF