v0.3.1: OAuth subscription auth + offline manual cheat sheet
Two additions:
1. OAuth subscription auth (lib/oauth.sh + larry-auth.sh)
- PKCE-based out-of-band flow against Claude.ai (no localhost server
needed; works behind any firewall).
- Uses the same client_id Claude Code uses, so calls bill against your
Max/Pro subscription quota instead of pay-as-you-go API metering.
- Tokens stored at $LARRY_HOME/.oauth.json (mode 0600), auto-refresh.
- larry.sh now detects oauth file at startup and uses Bearer auth.
- First-run flow now offers OAuth or API key; /login, /logout, /auth
slash commands in the REPL.
- Transparent fallback to API key if OAuth flow fails.
2. MANUAL.md — offline tool cheat sheet
- Documents every lib/*.sh script with copy-paste examples.
- Bryan's backup plan: when Anthropic is unreachable (no internet, on
a plane, etc.), all the underlying tools work standalone from the
shell. Larry just sequences them; they do not need Larry to run.
- Quick-recipe table at the bottom for the common day-to-day asks.
Files added:
- lib/oauth.sh
- larry-auth.sh
- MANUAL.md
Files modified:
- larry.sh — auth-mode detection, /auth /login /logout commands
- install-larry.sh — fetch new files
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
63d7bc6756
commit
61f1500492
479
MANUAL.md
Normal file
479
MANUAL.md
Normal file
@ -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/<tool>.sh`. From `$LARRY_HOME/`, that's `./lib/<tool>.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 `<thread> 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_<tag>_out` on OLD, `windows_<tag>_in` + `windows_<tag>_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 <session-id> # one session
|
||||||
|
cat "$LARRY_HOME/journal/<session>/manifest.md" # human-readable summary
|
||||||
|
cat "$LARRY_HOME/journal/<session>/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 `<target>.larry-prerollback.<unix-ts>` 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/<thread>.msgs # sampled inputs (1 per inbound)
|
||||||
|
├── outputs/env-a/<thread>/<dest>... # env-A route_test outputs
|
||||||
|
├── outputs/env-b/<thread>/<dest>... # env-B route_test outputs (using same inputs)
|
||||||
|
├── diff/<thread>.<dest>.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 <host> --env-b-user <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/<id>.log.md` — every Larry session is logged as markdown.
|
||||||
|
- `~/.larry/journal/<session>/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.
|
||||||
@ -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/cloverleaf-cheatsheet.md "$LARRY_HOME/agents/cloverleaf-cheatsheet.md"
|
||||||
fetch agents/regress.md "$LARRY_HOME/agents/regress.md"
|
fetch agents/regress.md "$LARRY_HOME/agents/regress.md"
|
||||||
fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh"
|
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-parse.sh "$LARRY_HOME/lib/nc-parse.sh"
|
||||||
fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.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"
|
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/nc-regression.sh "$LARRY_HOME/lib/nc-regression.sh"
|
||||||
fetch lib/journal.sh "$LARRY_HOME/lib/journal.sh"
|
fetch lib/journal.sh "$LARRY_HOME/lib/journal.sh"
|
||||||
fetch VERSION "$LARRY_HOME/VERSION"
|
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
|
# jq fallback — download static binary into $LARRY_HOME/bin/ if missing
|
||||||
|
|||||||
16
larry-auth.sh
Executable file
16
larry-auth.sh
Executable file
@ -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" "$@"
|
||||||
79
larry.sh
79
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
|
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
|
if [ -f "$LARRY_HOME/.env" ]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
set -a; . "$LARRY_HOME/.env"; set +a
|
set -a; . "$LARRY_HOME/.env"; set +a
|
||||||
fi
|
fi
|
||||||
|
[ -n "${ANTHROPIC_API_KEY:-}" ] && LARRY_AUTH_MODE="apikey"
|
||||||
|
else
|
||||||
|
LARRY_AUTH_MODE="apikey"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
prompt_first_run_auth() {
|
||||||
|
printf '%sFirst-run authentication setup%s\n\n' "$C_BOLD" "$C_RESET"
|
||||||
|
cat <<EOF
|
||||||
|
Two options:
|
||||||
|
|
||||||
|
1) OAuth login (bills your Claude Max / Pro subscription quota)
|
||||||
|
- Open a URL in any browser (even on a different device)
|
||||||
|
- Paste back the code
|
||||||
|
- Subscription billing — same as Claude Code
|
||||||
|
|
||||||
|
2) Anthropic API key (separate API billing, pay-as-you-go)
|
||||||
|
- Paste your sk-ant-... key, saved to $LARRY_HOME/.env
|
||||||
|
|
||||||
|
EOF
|
||||||
|
printf ' Choose [1=oauth, 2=apikey, q=quit]: '
|
||||||
|
read -r choice
|
||||||
|
case "${choice:-1}" in
|
||||||
|
1|o|oauth)
|
||||||
|
local auth_script=""
|
||||||
|
for c in "$(dirname "$0")/larry-auth.sh" "$LARRY_HOME/../larry-auth.sh" "$LARRY_HOME/lib/oauth.sh"; do
|
||||||
|
[ -x "$c" ] && { auth_script="$c"; break; }
|
||||||
|
done
|
||||||
|
[ -n "$auth_script" ] || { err "larry-auth.sh not found — reinstall or use API key"; prompt_api_key; return; }
|
||||||
|
"$auth_script" login || { err "OAuth failed — falling back to API key"; prompt_api_key; return; }
|
||||||
|
LARRY_AUTH_MODE="oauth"
|
||||||
|
;;
|
||||||
|
2|k|key|apikey)
|
||||||
|
prompt_api_key
|
||||||
|
LARRY_AUTH_MODE="apikey"
|
||||||
|
;;
|
||||||
|
q|quit) err "no auth selected"; exit 1 ;;
|
||||||
|
*) err "unrecognized choice; defaulting to OAuth"; prompt_first_run_auth ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
prompt_api_key() {
|
prompt_api_key() {
|
||||||
printf '%sFirst-run setup%s\n' "$C_BOLD" "$C_RESET"
|
printf '%sAPI key setup%s\n' "$C_BOLD" "$C_RESET"
|
||||||
echo " Paste your Anthropic API key (starts with sk-ant-...) and press Enter."
|
echo " Paste your Anthropic API key (starts with sk-ant-...) and press Enter."
|
||||||
echo " It will be saved to $LARRY_HOME/.env with permissions 0600."
|
echo " It will be saved to $LARRY_HOME/.env with permissions 0600."
|
||||||
echo ""
|
echo ""
|
||||||
@ -123,8 +171,8 @@ prompt_api_key() {
|
|||||||
log "API key saved."
|
log "API key saved."
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
|
if [ -z "$LARRY_AUTH_MODE" ]; then
|
||||||
prompt_api_key
|
prompt_first_run_auth
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -707,8 +755,23 @@ TOOLS_JSON='[
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
call_api() {
|
call_api() {
|
||||||
local payload_file="$1"
|
local payload_file="$1"
|
||||||
|
local auth_args=()
|
||||||
|
if [ "$LARRY_AUTH_MODE" = "oauth" ]; then
|
||||||
|
local oauth_script="$LARRY_LIB_DIR/oauth.sh"
|
||||||
|
local token
|
||||||
|
if [ -x "$oauth_script" ]; then
|
||||||
|
token=$("$oauth_script" ensure 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
err "OAuth token unavailable; run 'larry-auth.sh login' to re-authenticate"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
auth_args=(-H "Authorization: Bearer $token" -H "anthropic-beta: oauth-2025-04-20")
|
||||||
|
else
|
||||||
|
auth_args=(-H "x-api-key: $ANTHROPIC_API_KEY")
|
||||||
|
fi
|
||||||
curl -sS --max-time 180 \
|
curl -sS --max-time 180 \
|
||||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
"${auth_args[@]}" \
|
||||||
-H "anthropic-version: 2023-06-01" \
|
-H "anthropic-version: 2023-06-01" \
|
||||||
-H "content-type: application/json" \
|
-H "content-type: application/json" \
|
||||||
--data-binary "@$payload_file" \
|
--data-binary "@$payload_file" \
|
||||||
@ -820,6 +883,9 @@ Slash commands:
|
|||||||
/load <file> load file contents as your next user message
|
/load <file> load file contents as your next user message
|
||||||
/sys print the active system prompt
|
/sys print the active system prompt
|
||||||
/env print detected Cloverleaf env (HCIROOT, HCISITE, tools)
|
/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
|
/redetect re-scan for HCIROOT/HCISITE/tools
|
||||||
/sites list site dirs under HCIROOT
|
/sites list site dirs under HCIROOT
|
||||||
/site <name> switch HCISITE for this session
|
/site <name> switch HCISITE for this session
|
||||||
@ -876,6 +942,9 @@ main_loop() {
|
|||||||
/sys) printf '%s\n' "$system_prompt"; continue ;;
|
/sys) printf '%s\n' "$system_prompt"; continue ;;
|
||||||
/pwd) echo "$(pwd)"; continue ;;
|
/pwd) echo "$(pwd)"; continue ;;
|
||||||
/env) printf '%s\n' "$CLOVERLEAF_CTX"; 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
|
/redetect) detect_cloverleaf_env
|
||||||
system_prompt=$(build_system_prompt)
|
system_prompt=$(build_system_prompt)
|
||||||
larry_say "re-detected. /env to view."
|
larry_say "re-detected. /env to view."
|
||||||
|
|||||||
220
lib/oauth.sh
Executable file
220
lib/oauth.sh
Executable file
@ -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-._~' </dev/urandom | head -c 64)
|
||||||
|
challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | b64url)
|
||||||
|
printf '%s|%s' "$verifier" "$challenge"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_login() {
|
||||||
|
mkdir -p "$LARRY_HOME"
|
||||||
|
local pkce verifier challenge state
|
||||||
|
pkce=$(gen_pkce)
|
||||||
|
verifier="${pkce%%|*}"
|
||||||
|
challenge="${pkce##*|}"
|
||||||
|
state=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 32)
|
||||||
|
|
||||||
|
local url
|
||||||
|
url="${AUTHORIZE_URL}?code=true"
|
||||||
|
url="${url}&client_id=${CLIENT_ID}"
|
||||||
|
url="${url}&response_type=code"
|
||||||
|
url="${url}&redirect_uri=$(urlenc "$REDIRECT_URI")"
|
||||||
|
url="${url}&scope=$(urlenc "$SCOPE")"
|
||||||
|
url="${url}&code_challenge=${challenge}"
|
||||||
|
url="${url}&code_challenge_method=S256"
|
||||||
|
url="${url}&state=${state}"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
=== Larry-Anywhere — Claude subscription login ===
|
||||||
|
|
||||||
|
This binds Larry to your Claude.ai Max/Pro subscription quota (same flow
|
||||||
|
Claude Code uses). No API key needed.
|
||||||
|
|
||||||
|
1. Open this URL in any browser on any device:
|
||||||
|
|
||||||
|
${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 &).
|
||||||
|
|
||||||
|
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 <<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_API_KEY=sk-ant-...
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local now; now=$(date +%s)
|
||||||
|
umask 077
|
||||||
|
printf '%s' "$resp" | jq --arg now "$now" '. + {fetched_at: ($now | tonumber)}' > "$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
|
||||||
Loading…
Reference in New Issue
Block a user