diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..77ccf5c --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,479 @@ +# Larry-Anywhere — Manual Tool Cheat Sheet (no-Larry / offline mode) + +Every Larry-Anywhere capability is also a standalone bash script under `lib/`. When the internet is down, you're on a plane, or the Anthropic API is unreachable, you can drive these tools directly from the shell. **Larry just sequences them — the tools work without Larry.** + +This page documents every command with copy-paste examples. Print it. + +--- + +## Conventions + +- `$LARRY_HOME` defaults to `~/.larry/`. Lib scripts live at `$LARRY_HOME/lib/`. +- For brevity, examples below use `lib/.sh`. From `$LARRY_HOME/`, that's `./lib/.sh`. Or add `$LARRY_HOME/lib` to your PATH. +- `$HCIROOT` = Cloverleaf install root (e.g. `/opt/cloverleaf/cis2025/integrator`). +- `$HCISITE` = current site name (e.g. `adt`). +- `$HCISITEDIR` = `$HCIROOT/$HCISITE`. + +Set these before running anything site-specific: + +```bash +export HCIROOT=/opt/cloverleaf/cis2025/integrator +export HCISITE=adt +export HCISITEDIR="$HCIROOT/$HCISITE" +``` + +--- + +## Authentication (`larry-auth.sh`, `lib/oauth.sh`) + +Only needed if you're running the Larry REPL (`larry.sh`). The lib/ tools themselves never call Anthropic — they're pure local bash. + +```bash +larry-auth.sh login # OAuth via Claude.ai subscription +larry-auth.sh status # show current auth state + expiry +larry-auth.sh refresh # force-refresh the access token +larry-auth.sh logout # delete tokens (revert to API key) +``` + +Falling back to API key: edit `$LARRY_HOME/.env` with `ANTHROPIC_API_KEY=sk-ant-...`, chmod 600. Larry uses API key whenever `$LARRY_HOME/.oauth.json` is absent. + +--- + +## NetConfig parsing — read (`lib/nc-parse.sh`) + +The foundational reader. Every other NetConfig tool calls this. + +```bash +# List every protocol (thread) in a NetConfig file +lib/nc-parse.sh list-protocols "$HCISITEDIR/NetConfig" + +# List every process +lib/nc-parse.sh list-processes "$HCISITEDIR/NetConfig" + +# Find the line where a thread is declared +lib/nc-parse.sh protocol-line "$HCISITEDIR/NetConfig" ADTto_3m +# → 488 + +# Get the FULL TCL block for one protocol +lib/nc-parse.sh protocol-block "$HCISITEDIR/NetConfig" IB_ADT_muxS + +# Extract a top-level field value +lib/nc-parse.sh protocol-field "$HCISITEDIR/NetConfig" IB_ADT_muxS PROCESSNAME +# → ADT +lib/nc-parse.sh protocol-field "$HCISITEDIR/NetConfig" IB_ADT_muxS OBWORKASIB +# → 1 + +# Drill into nested blocks via dotted path — HOST/PORT/TYPE/ISSERVER live inside PROTOCOL{} +lib/nc-parse.sh protocol-nested "$HCISITEDIR/NetConfig" ADTto_3m PROTOCOL.PORT +# → 51006 +lib/nc-parse.sh protocol-nested "$HCISITEDIR/NetConfig" ADTto_3m PROTOCOL.HOST +# → SHD360ENCINT02T +lib/nc-parse.sh protocol-nested "$HCISITEDIR/NetConfig" ORU_fr_OPACS PROTOCOL.ISSERVER +# → 1 (it's a TCP listener) + +# TSV summary of every protocol — direction, port, host, type at a glance +lib/nc-parse.sh protocol-summary "$HCISITEDIR/NetConfig" +lib/nc-parse.sh protocol-summary "$HCISITEDIR/NetConfig" --filter adt # only ADT-ish names + +# Routing destinations (what does X route to?) +lib/nc-parse.sh destinations "$HCISITEDIR/NetConfig" IB_ADT_muxS + +# Routing sources (what routes INTO X?) — inverse +lib/nc-parse.sh sources "$HCISITEDIR/NetConfig" ADTto_CodaMetrix + +# Xlate files referenced (one protocol, or all) +lib/nc-parse.sh xlate-refs "$HCISITEDIR/NetConfig" IB_ADT_muxS +lib/nc-parse.sh xlate-refs "$HCISITEDIR/NetConfig" # all in file + +# TCL procs referenced +lib/nc-parse.sh tclproc-refs "$HCISITEDIR/NetConfig" IB_ADT_muxS + +# Get just the DATAXLATE routing block (the heart of routing config) +lib/nc-parse.sh route-block "$HCISITEDIR/NetConfig" IB_ADT_muxS +``` + +--- + +## Inbound thread classifier (`lib/nc-inbound.sh`) + +Identifies inbound threads — TCP listeners directly fed by upstream clients (Epic etc.), or ICL/file inbounds fed via Cloverleaf's internal link. + +```bash +# Every inbound thread (both classes), table format +lib/nc-inbound.sh "$HCISITEDIR/NetConfig" --format table + +# Just real TCP listeners (the "directly fed by upstream" subset) +lib/nc-inbound.sh "$HCISITEDIR/NetConfig" --mode tcp-listen --format table + +# Just ICL/file inbounds +lib/nc-inbound.sh "$HCISITEDIR/NetConfig" --mode icl-or-file --format table + +# JSONL for piping into other tools +lib/nc-inbound.sh "$HCISITEDIR/NetConfig" --mode tcp-listen --format jsonl +``` + +--- + +## Cross-site finder (`lib/nc-find.sh`) — the v1 tbn/tbp/tbh/tbpr/where replacements + +Walks every NetConfig under `$HCIROOT` (or a passed list) and returns matches. + +```bash +# tbn equivalent: partial name match +lib/nc-find.sh --name adt --format table + +# tbp equivalent: exact port +lib/nc-find.sh --port 51204 --format table + +# tbh equivalent: substring on host +lib/nc-find.sh --host SHD360 --format table + +# tbpr equivalent: substring on PROCESSNAME +lib/nc-find.sh --process codametrix --format table + +# v1 ` where` — locate a thread across all sites +lib/nc-find.sh --where IB_ADT_muxS --format table + +# Threads referencing a specific xlate file +lib/nc-find.sh --xlate Epic_ADT_CodaMetrix --format table + +# Threads referencing a specific TCL proc +lib/nc-find.sh --tclproc trxId_IB_ADT_muxS --format table + +# Override HCIROOT or pass explicit netconfigs +lib/nc-find.sh --name adt --hciroot /other/install/integrator --format table +lib/nc-find.sh --name adt --netconfigs "/a/NetConfig:/b/NetConfig" --format jsonl +``` + +--- + +## Message search — smat queries (`lib/nc-msgs.sh`) + +Smat databases are **SQLite 3**. Reads via native `sqlite3 -ascii` — no Cloverleaf binary involved. + +```bash +# Count messages in a thread's smat +lib/nc-msgs.sh ADTto_3m --format count + +# Recent 5 messages (text format with metadata + parsed segments) +lib/nc-msgs.sh ADTto_3m --limit 5 --format text + +# Time range — supports human expressions +lib/nc-msgs.sh ADTto_3m --after "3 days ago" --format count +lib/nc-msgs.sh ADTto_3m --after "2026-05-20" --before "2026-05-26 12:00:00" + +# Filter by HL7 field — find messages where PID.3 equals an MRN +lib/nc-msgs.sh ADTto_3m --field PID.3=5720501458 --limit 20 --format text + +# Account-number search at PID.18 +lib/nc-msgs.sh ADTto_3m --field PID.18=623000286 --format text + +# JSON output for piping +lib/nc-msgs.sh ADTto_3m --field PID.3=5720501458 --format json | jq + +# Explicit smatdb path (skip auto-locate) +lib/nc-msgs.sh ADTto_3m --db "$HCISITEDIR/exec/processes/3M/ADTto_3m.smatdb" --format count + +# Raw format (for piping into route-test inputs) — messages separated by 0x1c +lib/nc-msgs.sh ADTto_3m --limit 10 --format raw > inputs.msgs +``` + +--- + +## HL7 field extraction (`lib/hl7-field.sh`) + +Extract specific fields from a single HL7 message. + +```bash +# Read message from file, extract MRN +lib/hl7-field.sh PID.3 /path/to/message.hl7 +# → 5720501458 + +# From stdin +cat msg.hl7 | lib/hl7-field.sh MSH.10 +# → 27175 (message control ID) + +# Component extraction +lib/hl7-field.sh MSH.9 msg.hl7 # → ADT^A08 +lib/hl7-field.sh MSH.9.1 msg.hl7 # → ADT +lib/hl7-field.sh MSH.9.2 msg.hl7 # → A08 + +# Patient name (whole field + components) +lib/hl7-field.sh PID.5 msg.hl7 # → MORRIS^SALLY^^^^^^LHS^^^^^LEH^M +lib/hl7-field.sh PID.5.1 msg.hl7 # → MORRIS (family name) +lib/hl7-field.sh PID.5.2 msg.hl7 # → SALLY (given name) + +# Pipe smat dump → extract → sort | uniq +lib/nc-msgs.sh ADTto_3m --limit 100 --format raw \ + | awk -v RS=$'\x1c' '{print $0 > "/tmp/m"NR; system("lib/hl7-field.sh PID.3 /tmp/m"NR)}' +``` + +--- + +## HL7-aware diff (`lib/hl7-diff.sh`) + +Compare two HL7 files (or multi-message dumps) with field-level normalization. + +```bash +# Default — ignores MSH.7 (timestamp); shows everything else +lib/hl7-diff.sh left.hl7 right.hl7 + +# Add more fields to ignore +lib/hl7-diff.sh --ignore "MSH.7,MSH.10,EVN.6" left.hl7 right.hl7 + +# Inverse: ONLY compare specific fields +lib/hl7-diff.sh --include-fields "PID.3,PID.18,MSH.9" left.hl7 right.hl7 + +# Just count the differences +lib/hl7-diff.sh --format count left.hl7 right.hl7 +# → 5 + +# TSV output for parsing +lib/hl7-diff.sh --format tsv left.hl7 right.hl7 +# columns: msg_idx \t field_path \t left_value \t right_value +``` + +--- + +## Jump thread generation (`lib/nc-make-jump.sh`) + +Generates the 3-thread cross-environment data-replay pattern: `linux__out` on OLD, `windows__in` + `windows__out` in NEW's `server_jump` site. Output is plain TCL text — no file writes. + +```bash +# Generate for one inbound, target new linux host:port, output to stdout +lib/nc-make-jump.sh "$HCISITEDIR/NetConfig" \ + --inbound ORU_fr_OPACS \ + --new-host newlinux01.test \ + --jump-port 61204 + +# Write each artifact to separate files +lib/nc-make-jump.sh "$HCISITEDIR/NetConfig" \ + --inbound ORU_fr_OPACS \ + --new-host newlinux01.test \ + --jump-port 61204 \ + --out-prefix /tmp/oru_jump +# Produces: +# /tmp/oru_jump.old_out.tcl — paste into OLD env NetConfig +# /tmp/oru_jump.new_in.tcl — paste into NEW server_jump NetConfig +# /tmp/oru_jump.new_out.tcl — paste into NEW server_jump NetConfig +# /tmp/oru_jump.route_add.tcl — splice into OLD inbound's DATAXLATE + +# Override defaults +lib/nc-make-jump.sh "$HCISITEDIR/NetConfig" \ + --inbound ORU_fr_OPACS --new-host newlinux01 --jump-port 61204 \ + --inbound-host 10.0.0.5 # NEW-side outbound dials this instead of 127.0.0.1 + --process-jump migration_jump # different process on NEW than default "server_jump" + --encoding UTF8 # override if not ASCII +``` + +--- + +## NetConfig modification — journaled writes (`lib/nc-insert-protocol.sh`) + +Inserts new protocol blocks and splices route entries. **Every write is journaled** — backup, diff, atomic replace. + +```bash +# Insert a new protocol at end of file +lib/nc-insert-protocol.sh insert "$HCISITEDIR/NetConfig" /tmp/oru_jump.old_out.tcl +# → journal entry: 2026-05-26-09xxxx/001_NetConfig +# rollback: larry-rollback.sh --entry 2026-05-26-09xxxx/001_NetConfig + +# Insert before/after a named anchor protocol +lib/nc-insert-protocol.sh insert "$HCISITEDIR/NetConfig" /tmp/new_block.tcl --mode after --anchor IB_ADT_muxS +lib/nc-insert-protocol.sh insert "$HCISITEDIR/NetConfig" /tmp/new_block.tcl --mode before --anchor ADTto_3m + +# Splice a route entry into an existing protocol's DATAXLATE block +lib/nc-insert-protocol.sh add-route "$HCISITEDIR/NetConfig" ORU_fr_OPACS /tmp/oru_jump.route_add.tcl +``` + +After any write, see what changed: + +```bash +larry-rollback.sh --list # all journal entries +larry-rollback.sh --list --session # one session +cat "$LARRY_HOME/journal//manifest.md" # human-readable summary +cat "$LARRY_HOME/journal//files/NNN_*.diff" # the unified diff +``` + +Roll back: + +```bash +larry-rollback.sh --target "$HCISITEDIR/NetConfig" # all changes to this file, newest first +larry-rollback.sh --session 2026-05-26-09xxxx --yes # whole session, no prompt +larry-rollback.sh --last 1 # just the most recent write +larry-rollback.sh --entry 2026-05-26-09xxxx/001_NetConfig # one specific entry +larry-rollback.sh --dry-run --session ... # preview without changing anything +``` + +Pre-rollback copies land at `.larry-prerollback.` so you can redo. + +--- + +## System documentation (`lib/nc-document.sh`) + +Walks every NetConfig under `$HCIROOT`, finds threads matching a pattern, composes a markdown knowledge entry. + +```bash +# Auto-derived doc to stdout +lib/nc-document.sh --name codametrix + +# Write to a file with context fields +lib/nc-document.sh --name codametrix \ + --out "$LARRY_HOME/knowledge/codametrix.md" \ + --title "CodaMetrix Coding System" \ + --status "production" \ + --poc-vendor "John Doe at CodaMetrix, jdoe@codametrix.com" \ + --poc-internal "Sarah Smith, Integration Team" \ + --escalation "Page #integration-oncall in Slack" \ + --open-items "- Renewal Q3 2026" \ + --notes "Lives in epic site mostly; 1 thread in ancout" + +# Different scope sources +lib/nc-document.sh --name "3M" --hciroot /other/integrator --out /tmp/3m.md +lib/nc-document.sh --name epic_adt --netconfigs "$HCIROOT/epic/NetConfig:$HCIROOT/ancout/NetConfig" +``` + +Output: a complete markdown doc with cluster threads, sources, destinations, xlates, tclprocs, plus the placeholder context sections for the team. + +--- + +## Interface diff (`lib/nc-diff-interface.sh`) + +Compares one interface (and connected threads) between two NetConfigs. + +```bash +# Diff ADTto_3m + 1 hop of connected threads between test and prod +lib/nc-diff-interface.sh \ + --interface ADTto_3m \ + --left /test/integrator/ancout/NetConfig \ + --right /prod/integrator/ancout/NetConfig \ + --left-label TEST --right-label PROD \ + --depth 1 \ + --out /tmp/adt_diff.md + +# Walk further out +lib/nc-diff-interface.sh --interface ADTto_3m \ + --left /test/integrator/ancout/NetConfig \ + --right /prod/integrator/ancout/NetConfig \ + --depth 3 \ + --out /tmp/adt_chain_diff.md + +# Include table file diffs too (.tbl referenced by xlates/tclprocs) +lib/nc-diff-interface.sh --interface ADTto_3m ... --include-tables +``` + +Output: markdown report with cluster overview, per-thread protocol-block diff, per-xlate file diff, per-tclproc file diff. + +--- + +## Regression testing — end to end (`lib/nc-regression.sh`) + +Full Example 6 orchestrator. Six phases: discover → sample → route-test A → route-test B → diff → summary. + +```bash +# Phase-by-phase walk through (Bryan's house pattern) + +# Phase 1+2: discover inbounds and sample messages from env-A only +lib/nc-regression.sh \ + --scope site \ + --env-a /opt/cloverleaf/test/integrator --site-a adt \ + --env-b /opt/cloverleaf/prod/integrator --site-b adt \ + --out /tmp/reg-2026-05-26 \ + --count 10 \ + --phase 2 + +# Inspect what would be sampled (dry-run) +lib/nc-regression.sh --scope site --env-a /test --env-b /prod \ + --site-a adt --site-b adt --out /tmp/reg --count 10 --phase 2 --dry-run + +# Full run — needs Cloverleaf route_test command supplied: +lib/nc-regression.sh \ + --scope site --site-a adt --site-b adt \ + --env-a /opt/cloverleaf/test/integrator \ + --env-b /opt/cloverleaf/prod/integrator \ + --out /tmp/reg-2026-05-26 \ + --count 10 \ + --route-test-cmd 'cd {HCIROOT}/{HCISITE} && . ./.profile && {THREAD} route_test {INPUT} && cp *.out.* {OUTPUT_DIR}/' \ + --phase all + +# Just diff existing outputs (you ran route_test manually before) +lib/nc-regression.sh --scope site --site-a adt --site-b adt \ + --env-a /opt/cloverleaf/test/integrator \ + --env-b /opt/cloverleaf/prod/integrator \ + --out /tmp/reg-2026-05-26 \ + --phase 5 + +# Other scopes +--scope thread:ADTto_3m # one thread +--scope threads:ADTto_3m,MFNto_3m,DFTto_3m # specific list +--scope server # every inbound in every site under HCIROOT +``` + +Output tree: +``` +/tmp/reg-2026-05-26/ +├── inbounds.txt # the scope +├── inputs/.msgs # sampled inputs (1 per inbound) +├── outputs/env-a//... # env-A route_test outputs +├── outputs/env-b//... # env-B route_test outputs (using same inputs) +├── diff/..md # per-pair hl7_diff report +├── diff/_index.md # diff summary table +└── regression-summary.md # master report +``` + +Tip: if env-B is remote, pass `--env-b-host --env-b-user ` and Phase 4 scp's inputs over before invoking route_test there. Or run on env-B separately and skip Phase 4 with `--phase 5` for diff-only. + +--- + +## Reverse SSH tunnel (`larry-tunnel.sh`) + +If you want a home Larry to SSH into the client box (when client → outbound SSH is allowed): + +```bash +# Zero-config: serveo.net (third-party, NOT for sensitive sessions) +larry-tunnel.sh --serveo + +# Your own hop (needs hop sshd configured with GatewayPorts) +LARRY_HOP_USER=larry-tunnel \ +LARRY_HOP_HOST=bjnoela.com \ +LARRY_HOP_KEY=~/.ssh/id_ed25519 \ + larry-tunnel.sh + +# Inspect / stop +larry-tunnel.sh --status +larry-tunnel.sh --stop +``` + +--- + +## Quick recipe: "I have to do X without internet" + +| Task | Command | +|---|---| +| "what threads are here?" | `lib/nc-parse.sh list-protocols $HCISITEDIR/NetConfig` | +| "find threads named *3m*" | `lib/nc-find.sh --name 3m --format table` | +| "where does d_foo live?" | `lib/nc-find.sh --where d_foo --format table` | +| "what feeds d_foo?" | `lib/nc-parse.sh sources $HCISITEDIR/NetConfig d_foo` | +| "what does d_foo route to?" | `lib/nc-parse.sh destinations $HCISITEDIR/NetConfig d_foo` | +| "what xlates does d_foo use?" | `lib/nc-parse.sh xlate-refs $HCISITEDIR/NetConfig d_foo` | +| "find messages for MRN X" | `lib/nc-msgs.sh d_foo --field PID.3=X --format text` | +| "diff this interface across two envs" | `lib/nc-diff-interface.sh --interface NAME --left A/NC --right B/NC --depth 1 --out out.md` | +| "generate jump threads for a migration" | `lib/nc-make-jump.sh ... --inbound X --new-host Y --jump-port Z --out-prefix /tmp/jump` | +| "insert a new protocol with rollback" | `lib/nc-insert-protocol.sh insert NC /tmp/block.tcl` then `larry-rollback.sh --target NC` if needed | +| "what changes did I make recently?" | `larry-rollback.sh --list` | +| "undo my last change" | `larry-rollback.sh --last 1 --dry-run` then `--last 1 --yes` | +| "document a system" | `lib/nc-document.sh --name PATTERN --out FILE` | +| "full regression test between two envs" | `lib/nc-regression.sh --scope site --site-a SA --site-b SB --env-a EA --env-b EB --out DIR --count N --route-test-cmd '...' --phase all` | + +--- + +## Where to look when something breaks + +- `~/.larry/sessions/.log.md` — every Larry session is logged as markdown. +- `~/.larry/journal//manifest.md` — every journaled write in that session, with diffs. +- `~/.larry/journal/index.tsv` — flat index of every write across all sessions. +- `~/.larry/.env` — your API key (if using API-key auth). +- `~/.larry/.oauth.json` — OAuth tokens (if using subscription auth). +- `~/.larry/agents/*.md` — the personas loaded into Larry's system prompt. Editable; reloaded each launch. + +All lib/ scripts accept `--help` (or `-h`) to print usage. diff --git a/install-larry.sh b/install-larry.sh index db8b03c..b816af5 100755 --- a/install-larry.sh +++ b/install-larry.sh @@ -89,6 +89,8 @@ fetch agents/clover.md "$LARRY_HOME/agents/clover.md" fetch agents/cloverleaf-cheatsheet.md "$LARRY_HOME/agents/cloverleaf-cheatsheet.md" fetch agents/regress.md "$LARRY_HOME/agents/regress.md" fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh" +fetch larry-auth.sh "$LARRY_HOME/larry-auth.sh" +fetch lib/oauth.sh "$LARRY_HOME/lib/oauth.sh" fetch lib/nc-parse.sh "$LARRY_HOME/lib/nc-parse.sh" fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.sh" fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh" @@ -102,7 +104,8 @@ fetch lib/hl7-diff.sh "$LARRY_HOME/lib/hl7-diff.sh" fetch lib/nc-regression.sh "$LARRY_HOME/lib/nc-regression.sh" fetch lib/journal.sh "$LARRY_HOME/lib/journal.sh" fetch VERSION "$LARRY_HOME/VERSION" -chmod +x "$LARRY_HOME/larry.sh" "$LARRY_HOME/larry-tunnel.sh" "$LARRY_HOME/larry-rollback.sh" "$LARRY_HOME/lib/"*.sh +fetch MANUAL.md "$LARRY_HOME/MANUAL.md" +chmod +x "$LARRY_HOME/larry.sh" "$LARRY_HOME/larry-tunnel.sh" "$LARRY_HOME/larry-rollback.sh" "$LARRY_HOME/larry-auth.sh" "$LARRY_HOME/lib/"*.sh # ───────────────────────────────────────────────────────────────────────────── # jq fallback — download static binary into $LARRY_HOME/bin/ if missing diff --git a/larry-auth.sh b/larry-auth.sh new file mode 100755 index 0000000..74a0028 --- /dev/null +++ b/larry-auth.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# larry-auth.sh — top-level wrapper for OAuth subscription auth. +# Forwards to lib/oauth.sh, which contains the actual implementation. +set -e + +SELF_DIR="$(cd "$(dirname "$0")" && pwd)" +LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" + +# Locate oauth.sh: prefer sibling-of-this-script, then $LARRY_HOME/lib +OAUTH="" +for c in "$SELF_DIR/lib/oauth.sh" "$LARRY_HOME/lib/oauth.sh"; do + [ -x "$c" ] && { OAUTH="$c"; break; } +done +[ -n "$OAUTH" ] || { echo "larry-auth: cannot find lib/oauth.sh — reinstall larry-anywhere" >&2; exit 1; } + +exec "$OAUTH" "$@" diff --git a/larry.sh b/larry.sh index f175288..c00fdcb 100755 --- a/larry.sh +++ b/larry.sh @@ -98,15 +98,63 @@ mkdir -p "$LARRY_HOME/agents" "$LARRY_HOME/sessions" "$LARRY_HOME/bin" 2>/dev/nu } chmod 700 "$LARRY_HOME" 2>/dev/null || true -if [ -z "${ANTHROPIC_API_KEY:-}" ]; then +# ───────────────────────────────────────────────────────────────────────────── +# Authentication — two modes, OAuth preferred when available: +# 1. OAuth subscription auth (bills against your Claude Max/Pro subscription). +# Token file at $LARRY_HOME/.oauth.json — managed by larry-auth.sh. +# 2. API key (separate pay-as-you-go API billing). Stored in $LARRY_HOME/.env. +# ───────────────────────────────────────────────────────────────────────────── +LARRY_AUTH_MODE="" # set later: "oauth" or "apikey" + +if [ -f "$LARRY_HOME/.oauth.json" ]; then + LARRY_AUTH_MODE="oauth" +elif [ -z "${ANTHROPIC_API_KEY:-}" ]; then if [ -f "$LARRY_HOME/.env" ]; then # shellcheck disable=SC1091 set -a; . "$LARRY_HOME/.env"; set +a fi + [ -n "${ANTHROPIC_API_KEY:-}" ] && LARRY_AUTH_MODE="apikey" +else + LARRY_AUTH_MODE="apikey" fi +prompt_first_run_auth() { + printf '%sFirst-run authentication setup%s\n\n' "$C_BOLD" "$C_RESET" + cat < load file contents as your next user message /sys print the active system prompt /env print detected Cloverleaf env (HCIROOT, HCISITE, tools) + /auth show OAuth status (or "not authenticated") + /login run OAuth login flow (switch from API-key to subscription auth) + /logout delete OAuth tokens (revert to API-key auth) /redetect re-scan for HCIROOT/HCISITE/tools /sites list site dirs under HCIROOT /site switch HCISITE for this session @@ -876,6 +942,9 @@ main_loop() { /sys) printf '%s\n' "$system_prompt"; continue ;; /pwd) echo "$(pwd)"; continue ;; /env) printf '%s\n' "$CLOVERLEAF_CTX"; continue ;; + /auth) if [ -x "$LARRY_LIB_DIR/oauth.sh" ]; then "$LARRY_LIB_DIR/oauth.sh" status; else echo "(oauth.sh not installed)"; fi; continue ;; + /login) if [ -x "$LARRY_LIB_DIR/oauth.sh" ]; then "$LARRY_LIB_DIR/oauth.sh" login && LARRY_AUTH_MODE="oauth" && larry_say "switched to OAuth subscription auth"; else err "oauth.sh not installed"; fi; continue ;; + /logout) if [ -x "$LARRY_LIB_DIR/oauth.sh" ]; then "$LARRY_LIB_DIR/oauth.sh" logout; LARRY_AUTH_MODE="apikey"; fi; continue ;; /redetect) detect_cloverleaf_env system_prompt=$(build_system_prompt) larry_say "re-detected. /env to view." diff --git a/lib/oauth.sh b/lib/oauth.sh new file mode 100755 index 0000000..e258b47 --- /dev/null +++ b/lib/oauth.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# oauth.sh — OAuth login flow against Claude.ai for Larry-Anywhere. +# +# Uses the same OAuth client/flow Anthropic's Claude Code CLI uses, so calls +# bill against your Claude Max / Pro subscription quota instead of pay-as-you-go +# API metering. Public client_id; PKCE; out-of-band code paste (no localhost +# server required, works behind any firewall). +# +# Subcommands: +# login start the auth flow; print URL; prompt for code +# refresh refresh the access token using the stored refresh token +# ensure print a valid access token (auto-refreshes if near-expired) +# status show current auth state + expiry +# logout delete the stored tokens +# +# Storage: $LARRY_HOME/.oauth.json (mode 0600) +# +# This is community/unofficial use of Anthropic's OAuth flow. Anthropic could +# tighten it at any time. If OAuth stops working, Larry transparently falls +# back to the API-key path stored in $LARRY_HOME/.env. +set -u +set -o pipefail + +LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" +OAUTH_FILE="$LARRY_HOME/.oauth.json" + +# Anthropic Claude Code's publicly-visible OAuth client_id. Used by claude-code +# and several community CLI tools. +CLIENT_ID="${LARRY_OAUTH_CLIENT_ID:-9d1c250a-e61b-44d9-88ed-5944d1962f5e}" +AUTHORIZE_URL="${LARRY_OAUTH_AUTHORIZE_URL:-https://claude.ai/oauth/authorize}" +TOKEN_URL="${LARRY_OAUTH_TOKEN_URL:-https://console.anthropic.com/v1/oauth/token}" +REDIRECT_URI="${LARRY_OAUTH_REDIRECT_URI:-https://console.anthropic.com/oauth/code/callback}" +SCOPE="${LARRY_OAUTH_SCOPE:-org:create_api_key user:profile user:inference}" + +die() { printf 'oauth: %s\n' "$*" >&2; exit 1; } + +# Dependency check +command -v curl >/dev/null 2>&1 || die "curl required" +command -v jq >/dev/null 2>&1 || die "jq required" +command -v openssl >/dev/null 2>&1 || die "openssl required (for PKCE sha256)" + +b64url() { base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n'; } + +urlenc() { + # Minimal RFC3986-ish URL encoder for the bits we need (spaces, /, :) + local s="$1" + s="${s// /%20}" + s="${s//:/%3A}" + s="${s//\//%2F}" + printf '%s' "$s" +} + +gen_pkce() { + local verifier challenge + verifier=$(LC_ALL=C tr -dc 'a-zA-Z0-9-._~' &state=...). Copy ONLY the code value (the + part between code= and the next &). + +4. Paste it here: + +EOF + printf 'authorization code: ' + read -r code + [ -z "$code" ] && die "no code entered" + + local resp + resp=$(curl -sS -X POST "$TOKEN_URL" \ + -H "Content-Type: application/json" \ + -H "anthropic-beta: oauth-2025-04-20" \ + -d "$(jq -n \ + --arg cid "$CLIENT_ID" \ + --arg code "$code" \ + --arg verifier "$verifier" \ + --arg redirect "$REDIRECT_URI" \ + --arg state "$state" \ + '{client_id:$cid, grant_type:"authorization_code", code:$code, code_verifier:$verifier, redirect_uri:$redirect, state:$state}')") + + if ! printf '%s' "$resp" | jq -e '.access_token' >/dev/null 2>&1; then + printf '\nauth failed. server response:\n' >&2 + printf '%s\n' "$resp" | jq . >&2 2>/dev/null || printf '%s\n' "$resp" >&2 + cat >&2 < "$OAUTH_FILE" + chmod 600 "$OAUTH_FILE" + printf '\n✓ logged in. Tokens saved to %s (mode 0600).\n' "$OAUTH_FILE" + cmd_status +} + +cmd_refresh() { + [ -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") + [ -n "$refresh_token" ] || die "no refresh_token in $OAUTH_FILE — please run login again" + + local resp + resp=$(curl -sS -X POST "$TOKEN_URL" \ + -H "Content-Type: application/json" \ + -H "anthropic-beta: oauth-2025-04-20" \ + -d "$(jq -n --arg cid "$CLIENT_ID" --arg rt "$refresh_token" \ + '{client_id:$cid, grant_type:"refresh_token", refresh_token:$rt}')") + + if ! printf '%s' "$resp" | jq -e '.access_token' >/dev/null 2>&1; then + printf 'refresh failed:\n%s\n' "$resp" >&2 + return 1 + fi + + local now; now=$(date +%s) + printf '%s' "$resp" \ + | jq --arg now "$now" --slurpfile prev "$OAUTH_FILE" \ + '. + {fetched_at: ($now|tonumber), refresh_token: (.refresh_token // $prev[0].refresh_token)}' \ + > "$OAUTH_FILE.new" + mv "$OAUTH_FILE.new" "$OAUTH_FILE" + chmod 600 "$OAUTH_FILE" + jq -r '.access_token' "$OAUTH_FILE" +} + +cmd_ensure() { + [ -f "$OAUTH_FILE" ] || return 1 + local fetched_at expires_in + fetched_at=$(jq -r '.fetched_at // 0' "$OAUTH_FILE") + expires_in=$(jq -r '.expires_in // 3600' "$OAUTH_FILE") + local now; now=$(date +%s) + local expires_at=$((fetched_at + expires_in)) + if [ "$now" -ge $((expires_at - 300)) ]; then + cmd_refresh >/dev/null 2>&1 || return 1 + jq -r '.access_token' "$OAUTH_FILE" + else + jq -r '.access_token' "$OAUTH_FILE" + fi +} + +cmd_status() { + if [ ! -f "$OAUTH_FILE" ]; then + echo "OAuth: not authenticated (no $OAUTH_FILE)" + return 1 + fi + local fetched_at expires_in scope + fetched_at=$(jq -r '.fetched_at // 0' "$OAUTH_FILE") + expires_in=$(jq -r '.expires_in // 3600' "$OAUTH_FILE") + scope=$(jq -r '.scope // "(unknown)"' "$OAUTH_FILE") + local now; now=$(date +%s) + local expires_at=$((fetched_at + expires_in)) + local left=$((expires_at - now)) + printf 'OAuth status:\n' + printf ' file: %s\n' "$OAUTH_FILE" + printf ' scope: %s\n' "$scope" + printf ' fetched_at: %s\n' "$(date -r "$fetched_at" 2>/dev/null || date -d "@$fetched_at" 2>/dev/null)" + printf ' expires_in: %d s\n' "$expires_in" + if [ "$left" -le 0 ]; then + printf ' state: EXPIRED (%ds ago) — will auto-refresh on next call\n' "$((-left))" + else + printf ' state: valid for %d more seconds (~%d min)\n' "$left" "$((left/60))" + fi +} + +cmd_logout() { + if [ -f "$OAUTH_FILE" ]; then + rm -f "$OAUTH_FILE" + echo "logged out (removed $OAUTH_FILE)" + else + echo "no token file to remove" + fi +} + +case "${1:-status}" in + login) cmd_login ;; + refresh) cmd_refresh ;; + ensure) cmd_ensure ;; + status) cmd_status ;; + logout) cmd_logout ;; + -h|--help|help) sed -n '2,25p' "$0" ;; + *) die "unknown subcommand: ${1:-} (try 'login|refresh|ensure|status|logout')" ;; +esac