v0.5.4: pipe files to jq via stdin (MobaXterm Windows-jq path-translation fix)

Symptom: OAuth login succeeded on the work box but cmd_status emitted three
'jq: error: Could not open file' lines and showed empty fields. Same pattern
would have hit every subsequent chat turn via larry.sh's MESSAGES_FILE reads.

Root cause: install-larry.sh fetches a Windows-native jq.exe on cygwin/
mobaxterm platforms. Windows jq can't resolve Cygwin paths like
/home/mobaxterm/.larry/.oauth.json when they come in as argv arguments
(it interprets the leading slash as a Windows root). Bash's `>` redirection
worked because bash itself does the path open and hands jq an fd — the
read-side calls were passing the path string directly.

Fix: every read-side jq call now uses stdin redirection (`jq '...' < file`),
where bash does the open. Universal:
- Linux/macOS native jq: identical behavior (was already file-open-from-bash)
- MobaXterm/Cygwin/Git Bash with Windows jq.exe: now works
- WSL: works (Linux-native jq, same as Linux)
- Native PowerShell/cmd: doesn't apply — larry-anywhere is a bash script

Changes:
- lib/oauth.sh: new jqf() helper; 10 sites converted. Refactored cmd_refresh
  to drop --slurpfile (which can only take a path) — pre-reads the previous
  refresh_token, then uses --arg.
- larry.sh: add_user_text / add_assistant_blocks / add_user_tool_results
  now pipe $MESSAGES_FILE via stdin too.

Verified: cmd_status against a real token file produces clean output, no
jq errors. Syntax check passes both files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-27 09:47:06 -07:00
parent cbe15d548f
commit af3f034337
3 changed files with 34 additions and 16 deletions

View File

@ -1 +1 @@
0.5.3 0.5.4

View File

@ -36,7 +36,7 @@ set -o pipefail
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Config # Config
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.5.3" LARRY_VERSION="0.5.4"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}" 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_UPDATE_URL="${LARRY_UPDATE_URL:-${LARRY_BASE_URL}/larry.sh}"
@ -453,22 +453,26 @@ log_append() { printf '%s\n' "$1" >> "$LOG_FILE"; }
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Message store helpers # Message store helpers
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# NOTE on jq file IO: pass files to jq via stdin redirection, not as argv.
# On MobaXterm/Cygwin the bundled jq is a Windows-native binary that can't
# resolve Cygwin paths like /home/mobaxterm/... when they come in as argv.
# Stdin redirection always works because bash does the path open() itself.
add_user_text() { add_user_text() {
local content="$1" local content="$1"
local tmp; tmp=$(mktemp) local tmp; tmp=$(mktemp)
jq --arg c "$content" '. + [{"role":"user","content":[{"type":"text","text":$c}]}]' "$MESSAGES_FILE" > "$tmp" \ jq --arg c "$content" '. + [{"role":"user","content":[{"type":"text","text":$c}]}]' < "$MESSAGES_FILE" > "$tmp" \
&& mv "$tmp" "$MESSAGES_FILE" && mv "$tmp" "$MESSAGES_FILE"
} }
add_assistant_blocks() { add_assistant_blocks() {
local blocks="$1" local blocks="$1"
local tmp; tmp=$(mktemp) local tmp; tmp=$(mktemp)
jq --argjson b "$blocks" '. + [{"role":"assistant","content":$b}]' "$MESSAGES_FILE" > "$tmp" \ jq --argjson b "$blocks" '. + [{"role":"assistant","content":$b}]' < "$MESSAGES_FILE" > "$tmp" \
&& mv "$tmp" "$MESSAGES_FILE" && mv "$tmp" "$MESSAGES_FILE"
} }
add_user_tool_results() { add_user_tool_results() {
local blocks="$1" local blocks="$1"
local tmp; tmp=$(mktemp) local tmp; tmp=$(mktemp)
jq --argjson b "$blocks" '. + [{"role":"user","content":$b}]' "$MESSAGES_FILE" > "$tmp" \ jq --argjson b "$blocks" '. + [{"role":"user","content":$b}]' < "$MESSAGES_FILE" > "$tmp" \
&& mv "$tmp" "$MESSAGES_FILE" && mv "$tmp" "$MESSAGES_FILE"
} }

View File

@ -48,6 +48,17 @@ command -v openssl >/dev/null 2>&1 || die "openssl required (for PKCE sha256)"
b64url() { base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n'; } b64url() { base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n'; }
# jqf — run jq against a file, but pipe the file via stdin so bash handles
# the path translation. Needed because on MobaXterm/Cygwin the bundled jq
# may be a Windows-native binary that doesn't understand Cygwin paths like
# /home/mobaxterm/... when they come in as argv. Stdin redirection always
# works because bash does the open() itself.
# Usage: jqf <file> <jq-args...>
jqf() {
local file="$1"; shift
jq "$@" < "$file"
}
urlenc() { urlenc() {
# Minimal RFC3986-ish URL encoder for the bits we need (spaces, /, :) # Minimal RFC3986-ish URL encoder for the bits we need (spaces, /, :)
local s="$1" local s="$1"
@ -164,7 +175,7 @@ EOF
cmd_refresh() { cmd_refresh() {
[ -f "$OAUTH_FILE" ] || die "no oauth file at $OAUTH_FILE — run 'larry-auth.sh login' first" [ -f "$OAUTH_FILE" ] || die "no oauth file at $OAUTH_FILE — run 'larry-auth.sh login' first"
local refresh_token; refresh_token=$(jq -r '.refresh_token // empty' "$OAUTH_FILE") local refresh_token; refresh_token=$(jqf "$OAUTH_FILE" -r '.refresh_token // empty')
[ -n "$refresh_token" ] || die "no refresh_token in $OAUTH_FILE — please run login again" [ -n "$refresh_token" ] || die "no refresh_token in $OAUTH_FILE — please run login again"
local resp local resp
@ -182,27 +193,30 @@ cmd_refresh() {
fi fi
local now; now=$(date +%s) local now; now=$(date +%s)
# Pre-read the old refresh_token so we don't need --slurpfile (which would
# take a file path argv-style and break on MobaXterm's Windows-native jq).
local prev_refresh; prev_refresh=$(jqf "$OAUTH_FILE" -r '.refresh_token // empty')
printf '%s' "$resp" \ printf '%s' "$resp" \
| jq --arg now "$now" --slurpfile prev "$OAUTH_FILE" \ | jq --arg now "$now" --arg prev "$prev_refresh" \
'. + {fetched_at: ($now|tonumber), refresh_token: (.refresh_token // $prev[0].refresh_token)}' \ '. + {fetched_at: ($now|tonumber), refresh_token: (.refresh_token // $prev)}' \
> "$OAUTH_FILE.new" > "$OAUTH_FILE.new"
mv "$OAUTH_FILE.new" "$OAUTH_FILE" mv "$OAUTH_FILE.new" "$OAUTH_FILE"
chmod 600 "$OAUTH_FILE" chmod 600 "$OAUTH_FILE"
jq -r '.access_token' "$OAUTH_FILE" jqf "$OAUTH_FILE" -r '.access_token'
} }
cmd_ensure() { cmd_ensure() {
[ -f "$OAUTH_FILE" ] || return 1 [ -f "$OAUTH_FILE" ] || return 1
local fetched_at expires_in local fetched_at expires_in
fetched_at=$(jq -r '.fetched_at // 0' "$OAUTH_FILE") fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
expires_in=$(jq -r '.expires_in // 3600' "$OAUTH_FILE") expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
local now; now=$(date +%s) local now; now=$(date +%s)
local expires_at=$((fetched_at + expires_in)) local expires_at=$((fetched_at + expires_in))
if [ "$now" -ge $((expires_at - 300)) ]; then if [ "$now" -ge $((expires_at - 300)) ]; then
cmd_refresh >/dev/null 2>&1 || return 1 cmd_refresh >/dev/null 2>&1 || return 1
jq -r '.access_token' "$OAUTH_FILE" jqf "$OAUTH_FILE" -r '.access_token'
else else
jq -r '.access_token' "$OAUTH_FILE" jqf "$OAUTH_FILE" -r '.access_token'
fi fi
} }
@ -212,9 +226,9 @@ cmd_status() {
return 1 return 1
fi fi
local fetched_at expires_in scope local fetched_at expires_in scope
fetched_at=$(jq -r '.fetched_at // 0' "$OAUTH_FILE") fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
expires_in=$(jq -r '.expires_in // 3600' "$OAUTH_FILE") expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
scope=$(jq -r '.scope // "(unknown)"' "$OAUTH_FILE") scope=$(jqf "$OAUTH_FILE" -r '.scope // "(unknown)"')
local now; now=$(date +%s) local now; now=$(date +%s)
local expires_at=$((fetched_at + expires_in)) local expires_at=$((fetched_at + expires_in))
local left=$((expires_at - now)) local left=$((expires_at - now))