v0.5.0: MANIFEST-driven self-update + OAuth code#state parsing
Self-update overhaul (no more manual reinstalls when lib/ changes):
- New MANIFEST file at repo root lists every file that should auto-sync
(top-level scripts, agents/, lib/, VERSION, MANUAL.md).
- larry.sh self_update() reworked into two phases:
Phase A — local sync: if $LARRY_HOME/.last-sync-version != $LARRY_VERSION,
fetch MANIFEST and refresh every listed file. Stamps version after.
Phase B — remote check: fetch $LARRY_BASE_URL/VERSION; if newer, pull
larry.sh, self-replace, relaunch with LARRY_JUST_UPDATED=1 so phase B
is skipped on the relaunch (phase A then pulls everything else).
- New LARRY_BASE_URL env var (the legacy LARRY_UPDATE_URL / LARRY_AGENTS_URL
still work as overrides).
- Bumped LARRY_VERSION and VERSION to 0.5.0.
OAuth fix (lib/oauth.sh):
- Anthropic's callback returns the code as 'CODE#STATE' (URL fragment, not
query). Previous prompt told users to copy "between code= and the next &"
which produced the wrong substring; the token endpoint then returned a
misleading 'rate_limit_error' on the malformed code.
- Now splits the pasted input on '#', verifies the returned state matches
the one we generated, sends only CODE to the token endpoint.
- Updated user-facing prompt and error hints to describe the real format
and explain the misleading rate_limit_error symptom.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
b141d54847
commit
28622ca40b
62
MANIFEST
Normal file
62
MANIFEST
Normal file
@ -0,0 +1,62 @@
|
||||
# larry-anywhere update manifest
|
||||
# Format: one path per line, relative to the bundle root.
|
||||
# Lines starting with '#' and blank lines are ignored.
|
||||
# Every file listed here is auto-synced by larry.sh's self_update() each time
|
||||
# the running larry.sh version changes (and on first launch of a new version).
|
||||
#
|
||||
# To add a new file to the auto-sync set: list it here and bump VERSION.
|
||||
|
||||
# Top-level scripts
|
||||
larry.sh
|
||||
larry-tunnel.sh
|
||||
larry-auth.sh
|
||||
larry-rollback.sh
|
||||
install-larry.sh
|
||||
|
||||
# Metadata
|
||||
VERSION
|
||||
MANUAL.md
|
||||
|
||||
# Agent personas (system-prompt overlays)
|
||||
agents/larry.md
|
||||
agents/clover.md
|
||||
agents/cloverleaf-cheatsheet.md
|
||||
agents/regress.md
|
||||
|
||||
# Auth implementation
|
||||
lib/oauth.sh
|
||||
|
||||
# Logging / capture
|
||||
lib/lessons.sh
|
||||
lib/journal.sh
|
||||
|
||||
# HL7 utilities
|
||||
lib/hl7-sanitize.sh
|
||||
lib/hl7-desanitize.sh
|
||||
lib/hl7-diff.sh
|
||||
lib/hl7-field.sh
|
||||
|
||||
# Generic helpers
|
||||
lib/each.sh
|
||||
lib/each-site.sh
|
||||
lib/len2nl.sh
|
||||
lib/csv-to-table.sh
|
||||
lib/table-to-csv.sh
|
||||
|
||||
# NetConfig tooling
|
||||
lib/nc-engine.sh
|
||||
lib/nc-status.sh
|
||||
lib/nc-table.sh
|
||||
lib/nc-xlate.sh
|
||||
lib/nc-smat-diff.sh
|
||||
lib/nc-create-thread.sh
|
||||
lib/nc-tclgen.sh
|
||||
lib/nc-parse.sh
|
||||
lib/nc-inbound.sh
|
||||
lib/nc-make-jump.sh
|
||||
lib/nc-msgs.sh
|
||||
lib/nc-document.sh
|
||||
lib/nc-diff-interface.sh
|
||||
lib/nc-find.sh
|
||||
lib/nc-insert-protocol.sh
|
||||
lib/nc-regression.sh
|
||||
141
larry.sh
141
larry.sh
@ -11,8 +11,12 @@
|
||||
#
|
||||
# Env vars:
|
||||
# LARRY_HOME where to cache config/sessions (default: ~/.larry)
|
||||
# LARRY_UPDATE_URL URL of latest larry.sh for self-update (optional)
|
||||
# LARRY_AGENTS_URL base URL for agents/ refresh (optional)
|
||||
# LARRY_BASE_URL root URL of the bundle on the server (default:
|
||||
# https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main)
|
||||
# Self-update pulls VERSION + MANIFEST from here and
|
||||
# refreshes every file listed in MANIFEST.
|
||||
# LARRY_UPDATE_URL (legacy override) full URL of latest larry.sh
|
||||
# LARRY_AGENTS_URL (legacy override) base URL for agents/
|
||||
# LARRY_MODEL Claude model (default: claude-sonnet-4-6)
|
||||
# LARRY_MAX_TOKENS max output tokens per turn (default: 8192)
|
||||
# LARRY_NO_UPDATE set to 1 to disable self-update
|
||||
@ -32,10 +36,11 @@ set -o pipefail
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Config
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
LARRY_VERSION="0.4.3"
|
||||
LARRY_VERSION="0.5.0"
|
||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}"
|
||||
LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/agents}"
|
||||
LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}"
|
||||
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-${LARRY_BASE_URL}/larry.sh}"
|
||||
LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-${LARRY_BASE_URL}/agents}"
|
||||
LARRY_MODEL="${LARRY_MODEL:-claude-sonnet-4-6}"
|
||||
LARRY_MAX_TOKENS="${LARRY_MAX_TOKENS:-8192}"
|
||||
LARRY_API_URL="${LARRY_API_URL:-https://api.anthropic.com/v1/messages}"
|
||||
@ -222,38 +227,114 @@ AGENT_EOF
|
||||
fetch_agents_or_warn
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Self-update
|
||||
# Self-update — two-phase MANIFEST-driven sync.
|
||||
#
|
||||
# Phase A (local sync, no network if up-to-date):
|
||||
# If $LARRY_HOME/.last-sync-version != $LARRY_VERSION, the running larry.sh
|
||||
# is newer than the on-disk lib/agents/etc. files. Fetch MANIFEST from
|
||||
# $LARRY_BASE_URL and refresh every file listed. Stamp .last-sync-version.
|
||||
#
|
||||
# Phase B (remote version check):
|
||||
# Fetch $LARRY_BASE_URL/VERSION. If remote > local, pull new larry.sh,
|
||||
# replace self, relaunch with LARRY_JUST_UPDATED=1 so phase B is skipped
|
||||
# on the relaunch (avoids infinite loop). Phase A on the relaunch then
|
||||
# pulls every other file matching the new version.
|
||||
#
|
||||
# Skip all of it via --no-update or LARRY_NO_UPDATE=1.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
self_update() {
|
||||
[ "$LARRY_NO_UPDATE" = "1" ] && return 0
|
||||
[ -z "$LARRY_UPDATE_URL" ] && return 0
|
||||
sync_from_manifest() {
|
||||
local base="$1"
|
||||
local manifest="$LARRY_HOME/.manifest.new"
|
||||
curl -fsSL --max-time 10 "$base/MANIFEST" -o "$manifest" 2>/dev/null || {
|
||||
rm -f "$manifest"
|
||||
return 1
|
||||
}
|
||||
[ -s "$manifest" ] || { rm -f "$manifest"; return 1; }
|
||||
|
||||
local self="$0"
|
||||
case "$self" in /*) ;; *) self="$PWD/$self" ;; esac
|
||||
|
||||
local count=0 updated=0 failed=0 path tmp dest
|
||||
while IFS= read -r path; do
|
||||
case "$path" in ''|'#'*) continue ;; esac
|
||||
path="${path%%[[:space:]]*}" # strip trailing whitespace/comments
|
||||
[ -z "$path" ] && continue
|
||||
count=$((count + 1))
|
||||
|
||||
# larry.sh is updated by phase B, not here — skip to avoid clobbering
|
||||
# the running script mid-execution.
|
||||
[ "$path" = "larry.sh" ] && continue
|
||||
|
||||
dest="$LARRY_HOME/$path"
|
||||
tmp="$dest.new"
|
||||
mkdir -p "$(dirname "$dest")" 2>/dev/null
|
||||
if curl -fsSL --max-time 15 "$base/$path" -o "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
||||
if [ ! -f "$dest" ] || ! cmp -s "$dest" "$tmp"; then
|
||||
mv "$tmp" "$dest"
|
||||
case "$path" in *.sh) chmod +x "$dest" 2>/dev/null || true ;; esac
|
||||
updated=$((updated + 1))
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
else
|
||||
rm -f "$tmp"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done < "$manifest"
|
||||
rm -f "$manifest"
|
||||
|
||||
if [ "$updated" -gt 0 ] || [ "$failed" -gt 0 ]; then
|
||||
log "manifest sync: $updated updated, $failed failed, $count total (from $base)"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
self_update() {
|
||||
[ "$LARRY_NO_UPDATE" = "1" ] && return 0
|
||||
[ -z "$LARRY_BASE_URL" ] && return 0
|
||||
|
||||
local self="$0"
|
||||
case "$self" in /*) ;; *) self="$PWD/$self" ;; esac
|
||||
|
||||
# Phase A: local file sync. Triggered when on-disk files are out of sync
|
||||
# with the running larry.sh version (e.g. just after a self-replace, or
|
||||
# on first launch after install).
|
||||
local last_sync=""
|
||||
[ -f "$LARRY_HOME/.last-sync-version" ] \
|
||||
&& last_sync=$(tr -d '[:space:]' < "$LARRY_HOME/.last-sync-version" 2>/dev/null)
|
||||
if [ "$last_sync" != "$LARRY_VERSION" ]; then
|
||||
if sync_from_manifest "$LARRY_BASE_URL"; then
|
||||
printf '%s\n' "$LARRY_VERSION" > "$LARRY_HOME/.last-sync-version" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Phase B: skip the network version check on the relaunch right after a
|
||||
# self-replace (we just pulled it; checking again is pointless and risks
|
||||
# loops if curl returns stale/partial content).
|
||||
[ "${LARRY_JUST_UPDATED:-0}" = "1" ] && return 0
|
||||
[ -w "$self" ] || return 0
|
||||
|
||||
local tmp="$LARRY_HOME/larry.sh.new"
|
||||
if curl -fsSL --max-time 5 "$LARRY_UPDATE_URL" -o "$tmp" 2>/dev/null; then
|
||||
if [ -s "$tmp" ] && ! cmp -s "$self" "$tmp"; then
|
||||
local new_ver
|
||||
new_ver=$(grep -m1 '^LARRY_VERSION=' "$tmp" | sed 's/.*"\(.*\)".*/\1/')
|
||||
log "update found: $LARRY_VERSION -> ${new_ver:-?}"
|
||||
cp "$tmp" "$self" && chmod +x "$self"
|
||||
rm -f "$tmp"
|
||||
log "relaunching..."
|
||||
exec "$self" --no-update ${ARG_DIR:+"$ARG_DIR"}
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
local remote_ver
|
||||
remote_ver=$(curl -fsSL --max-time 5 "$LARRY_BASE_URL/VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
[ -z "$remote_ver" ] && return 0
|
||||
[ "$remote_ver" = "$LARRY_VERSION" ] && return 0
|
||||
|
||||
# Also refresh agents
|
||||
if [ -n "$LARRY_AGENTS_URL" ]; then
|
||||
for f in larry.md clover.md; do
|
||||
curl -fsSL --max-time 5 "$LARRY_AGENTS_URL/$f" -o "$LARRY_HOME/agents/$f.new" 2>/dev/null \
|
||||
&& [ -s "$LARRY_HOME/agents/$f.new" ] \
|
||||
&& mv "$LARRY_HOME/agents/$f.new" "$LARRY_HOME/agents/$f" \
|
||||
|| rm -f "$LARRY_HOME/agents/$f.new"
|
||||
done
|
||||
local tmp="$LARRY_HOME/larry.sh.new"
|
||||
curl -fsSL --max-time 15 "$LARRY_BASE_URL/larry.sh" -o "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
||||
[ -s "$tmp" ] || { rm -f "$tmp"; return 0; }
|
||||
if cmp -s "$self" "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
local new_ver
|
||||
new_ver=$(grep -m1 '^LARRY_VERSION=' "$tmp" | sed 's/.*"\(.*\)".*/\1/')
|
||||
[ -z "$new_ver" ] && { rm -f "$tmp"; return 0; }
|
||||
log "update found: $LARRY_VERSION -> $new_ver — relaunching"
|
||||
cp "$tmp" "$self" && chmod +x "$self"
|
||||
rm -f "$tmp"
|
||||
# Force phase A on the next launch by invalidating the sync stamp.
|
||||
rm -f "$LARRY_HOME/.last-sync-version" 2>/dev/null || true
|
||||
exec env LARRY_JUST_UPDATED=1 "$self" ${ARG_DIR:+"$ARG_DIR"}
|
||||
}
|
||||
self_update
|
||||
|
||||
|
||||
40
lib/oauth.sh
40
lib/oauth.sh
@ -87,17 +87,30 @@ Claude Code uses). No API key needed.
|
||||
${url}
|
||||
|
||||
2. Sign in with your Claude account.
|
||||
3. Approve the app. You'll land on a page that displays the authorization
|
||||
code in the URL (it'll look like https://console.anthropic.com/oauth/code/
|
||||
callback?code=<LONG-STRING>&state=...). Copy ONLY the code value (the
|
||||
part between code= and the next &).
|
||||
3. Approve the app. You'll land on a page that displays a string in the form
|
||||
<CODE>#<STATE>
|
||||
(Anthropic uses a URL fragment, not a query param, to deliver them.)
|
||||
Copy the WHOLE string — both halves and the '#' between them.
|
||||
|
||||
4. Paste it here:
|
||||
|
||||
EOF
|
||||
printf 'authorization code: '
|
||||
read -r code
|
||||
[ -z "$code" ] && die "no code entered"
|
||||
printf 'authorization code (CODE#STATE): '
|
||||
read -r code_input
|
||||
[ -z "$code_input" ] && die "no code entered"
|
||||
|
||||
# Split CODE#STATE. If the user pasted only the code (no '#'), keep the
|
||||
# state we generated; otherwise verify the returned state matches.
|
||||
local code returned_state
|
||||
if [[ "$code_input" == *"#"* ]]; then
|
||||
code="${code_input%%#*}"
|
||||
returned_state="${code_input#*#}"
|
||||
if [ -n "$returned_state" ] && [ "$returned_state" != "$state" ]; then
|
||||
die "state mismatch — got '$returned_state', expected '$state' (possible CSRF or stale URL; rerun login)"
|
||||
fi
|
||||
else
|
||||
code="$code_input"
|
||||
fi
|
||||
|
||||
local resp
|
||||
resp=$(curl -sS -X POST "$TOKEN_URL" \
|
||||
@ -117,10 +130,15 @@ EOF
|
||||
cat >&2 <<EOF
|
||||
|
||||
Hints:
|
||||
- Make sure you pasted ONLY the code= value, not the whole URL.
|
||||
- The code is single-use; if you used it already, run 'larry-auth.sh login' again.
|
||||
- If the OAuth endpoint has changed, you can fall back to the API key
|
||||
by deleting any oauth file and creating $LARRY_HOME/.env with
|
||||
- Anthropic's callback delivers the code as CODE#STATE (fragment, not query).
|
||||
Paste the WHOLE string including '#'. Just CODE alone will also work, but
|
||||
if you pasted CODE#STATE#... or trimmed wrong, the token endpoint will
|
||||
return 'rate_limit_error' (misleading — it actually means malformed/used
|
||||
code, not a real rate limit).
|
||||
- The code is single-use; if you used it already (even on a failed attempt),
|
||||
run 'larry-auth.sh login' again to get a fresh URL.
|
||||
- If the OAuth endpoint has genuinely changed, you can fall back to the API
|
||||
key by deleting any oauth file and creating $LARRY_HOME/.env with
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
EOF
|
||||
exit 1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user