Portable AI agent for Cloverleaf integration work. Pure bash + curl + jq. Zero dependency on v1 wrapper scripts or v2 cloverleaf-tools.pyz. 27 native Anthropic tools: NetConfig parsing (read) nc_list_protocols, nc_list_processes, nc_protocol_block, nc_protocol_field, nc_protocol_nested, nc_protocol_summary, nc_destinations, nc_sources, nc_xlate_refs, nc_tclproc_refs NetConfig modification (journal-backed writes with rollback) nc_insert_protocol, nc_add_route, larry_rollback_list Workflows nc_find_inbound, nc_make_jump (3-thread jump pattern), nc_find (tbn/tbp/tbh/tbpr/where replacements), nc_document, nc_diff_interface, nc_regression Messages hl7_field, nc_msgs (smat is SQLite!), hl7_diff (with --ignore MSH.7) File system read_file, list_dir, grep_files, glob_files, write_file, bash_exec Validated against a 22-site real Cloverleaf test install. Five worked examples end-to-end: jump-thread generation, smat MRN search, system documentation, interface+connected diff, HL7-aware regression diff. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
4.4 KiB
Bash
Executable File
130 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# larry-rollback.sh — restore files modified by Larry-Anywhere from journal backups.
|
|
#
|
|
# Usage:
|
|
# larry-rollback.sh --list # show every journal entry, newest first
|
|
# larry-rollback.sh --list --session SESSION # show entries for one session
|
|
# larry-rollback.sh --session SESSION # roll back ALL entries in a session (newest first)
|
|
# larry-rollback.sh --last N # roll back the N most recent entries
|
|
# larry-rollback.sh --entry ENTRY_ID # roll back one specific entry (e.g. 2026-05-26-.../004)
|
|
# larry-rollback.sh --target /path/to/file # roll back all entries that touched this file (newest first)
|
|
# larry-rollback.sh --dry-run # show what would be rolled back without doing it
|
|
#
|
|
# Y/N confirm on every restoration unless --yes is passed.
|
|
set -u
|
|
set -o pipefail
|
|
|
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
|
JOURNAL_ROOT="$LARRY_HOME/journal"
|
|
JOURNAL_INDEX="$JOURNAL_ROOT/index.tsv"
|
|
|
|
DRY=0
|
|
YES=0
|
|
MODE=""
|
|
SESSION=""
|
|
N=""
|
|
ENTRY_ID=""
|
|
TARGET=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--list) MODE="list" ;;
|
|
--session) shift; SESSION="$1"; [ -z "$MODE" ] && MODE="session" ;;
|
|
--last) shift; N="$1"; MODE="last" ;;
|
|
--entry) shift; ENTRY_ID="$1"; MODE="entry" ;;
|
|
--target) shift; TARGET="$1"; MODE="target" ;;
|
|
--dry-run) DRY=1 ;;
|
|
--yes|-y) YES=1 ;;
|
|
-h|--help) sed -n '2,16p' "$0"; exit 0 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -f "$JOURNAL_INDEX" ] || { echo "no journal (no writes have been made yet)" >&2; exit 1; }
|
|
|
|
C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RESET=$'\033[0m'
|
|
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'
|
|
|
|
_list_filter() {
|
|
local ses="$1"
|
|
awk -F'\t' -v s="$ses" '
|
|
NR==1 { next }
|
|
s=="" || $2==s { print }
|
|
' "$JOURNAL_INDEX" | tail -r 2>/dev/null || awk -F'\t' -v s="$ses" '
|
|
NR==1 { next }
|
|
s=="" || $2==s { print }
|
|
' "$JOURNAL_INDEX" | awk '{a[NR]=$0} END{for(i=NR;i>=1;i--) print a[i]}'
|
|
}
|
|
|
|
if [ "$MODE" = "list" ]; then
|
|
printf '%stimestamp session-id seq target%s\n' "$C_BOLD" "$C_RESET"
|
|
_list_filter "$SESSION" | awk -F'\t' '{printf " %-26s %-28s %-4s %s\n", $1, $2, $3, $4}'
|
|
exit 0
|
|
fi
|
|
|
|
build_targets() {
|
|
case "$MODE" in
|
|
session)
|
|
[ -n "$SESSION" ] || { echo "--session needs a value" >&2; exit 2; }
|
|
_list_filter "$SESSION"
|
|
;;
|
|
last)
|
|
[ -n "$N" ] || { echo "--last needs N" >&2; exit 2; }
|
|
_list_filter "" | head -n "$N"
|
|
;;
|
|
entry)
|
|
local ses seq
|
|
ses="${ENTRY_ID%/*}"; seq="${ENTRY_ID##*/}"; seq="${seq%%_*}"
|
|
awk -F'\t' -v s="$ses" -v sq="$seq" '$2==s && $3==sq' "$JOURNAL_INDEX"
|
|
;;
|
|
target)
|
|
awk -F'\t' -v t="$TARGET" '$4==t' "$JOURNAL_INDEX" | awk '{a[NR]=$0} END{for(i=NR;i>=1;i--) print a[i]}'
|
|
;;
|
|
*)
|
|
echo "specify --list, --session, --last N, --entry ID, or --target PATH" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
ENTRIES=$(build_targets)
|
|
[ -n "$ENTRIES" ] || { echo "(no matching journal entries)"; exit 0; }
|
|
|
|
printf '%sWill roll back %s entr(y/ies):%s\n' "$C_BOLD" "$(printf '%s\n' "$ENTRIES" | wc -l | tr -d ' ')" "$C_RESET"
|
|
printf '%s' "$ENTRIES" | awk -F'\t' '{printf " %s %-26s %s\n", $3, $1, $4}'
|
|
echo ""
|
|
|
|
if [ "$DRY" = "1" ]; then
|
|
echo "(dry-run; no changes)"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$YES" != "1" ]; then
|
|
printf '%sProceed?%s [y/N]: ' "$C_BOLD" "$C_RESET"
|
|
read -r ans </dev/tty || ans=""
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; exit 1; }
|
|
fi
|
|
|
|
printf '%s\n' "$ENTRIES" | while IFS=$'\t' read -r ts ses seq target action orig_sha new_sha backup diffp; do
|
|
[ -z "$target" ] && continue
|
|
if [ "$action" = "create" ]; then
|
|
if [ -e "$target" ]; then
|
|
cp -p "$target" "$target.larry-prerollback.$(date +%s)"
|
|
rm -f "$target" && printf ' %s✓ deleted%s %s (was newly created)\n' "$C_GREEN" "$C_RESET" "$target"
|
|
else
|
|
printf ' %s○ already absent%s %s\n' "$C_DIM" "$C_RESET" "$target"
|
|
fi
|
|
else
|
|
if [ -f "$backup" ]; then
|
|
cp -p "$target" "$target.larry-prerollback.$(date +%s)" 2>/dev/null || true
|
|
cp -p "$backup" "$target" && printf ' %s✓ restored%s %s ← %s\n' "$C_GREEN" "$C_RESET" "$target" "$backup"
|
|
else
|
|
printf ' %s✗ backup missing for%s %s (looked at %s)\n' "$C_RED" "$C_RESET" "$target" "$backup"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Pre-rollback copies left at <target>.larry-prerollback.<ts> in case you want to redo. Clean up at your leisure."
|