larry tools list / <name> [args] makes all 24 lib/ Cloverleaf+HL7 tools discoverable and runnable by hand with no API/LLM; dispatches before bootstrap/self-update/network. _diagnose_api_block recognizes a blocked API (curl rc/stderr/body/headers, incl. Cisco Umbrella fingerprints) and guides the operator to manual-tools mode + IT allowlisting instead of a raw error dump. Graceful degradation + honest guidance only — NO traffic masking/proxy-hiding/circumvention on a PHI box. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
725 lines
29 KiB
Markdown
725 lines
29 KiB
Markdown
# 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.
|
|
|
|
---
|
|
|
|
## The front door: `larry tools` (v0.8.14)
|
|
|
|
You don't have to memorize the `lib/` paths. The `larry` command itself is the discoverable entry point for manual-tools mode — it works with **no REPL, no API, no LLM**, so it's the lifeline on a locked-down box where the model API is blocked:
|
|
|
|
```bash
|
|
larry tools list # every tool + a one-line description, grouped
|
|
larry tools <name> [args] # run a tool by hand (no args → its --help)
|
|
larry tools <name> --help # usage, flags, expected input/output + example
|
|
larry tools help # what manual-tools mode is
|
|
|
|
# Examples — the name is the script minus ".sh":
|
|
larry tools nc-parse list-protocols "$HCISITEDIR/NetConfig"
|
|
larry tools hl7-field PID.3 /tmp/sample.hl7
|
|
larry tools nc-status sites
|
|
```
|
|
|
|
`larry tools …` runs **before** any self-update or network call, so a fresh install with the API blocked still gives you the full toolkit. Equivalent to calling `lib/<name>.sh …` directly (documented below) — use whichever you prefer.
|
|
|
|
### When the API is blocked
|
|
|
|
If you launch the interactive REPL on a box where corporate security blocks `api.anthropic.com` (e.g. Cisco Umbrella returns a 403, TLS inspection trips `unable to get local issuer certificate`, or egress is refused), Larry **detects the block and guides you into manual-tools mode** instead of dumping a raw curl error:
|
|
|
|
```
|
|
Can't reach the model API — looks like a corporate network block.
|
|
What happened: TLS interception (an untrusted/MITM certificate ...).
|
|
The Cloverleaf tools still work — run them by hand (no API/LLM needed):
|
|
larry tools list
|
|
To use the AI brain: ask IT to allowlist api.anthropic.com, or run from a network that permits it.
|
|
```
|
|
|
|
This is graceful degradation and honest guidance only. Larry will **not** try to bypass, mask, proxy around, or otherwise circumvent a corporate security control on a PHI box — that is off the table by design. The fix is to get the endpoint allowlisted by IT, or run from a permitted network.
|
|
|
|
---
|
|
|
|
## 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"
|
|
```
|
|
|
|
---
|
|
|
|
## Auto-update & origin
|
|
|
|
`larry.sh` (and `install-larry.sh`) fetch updates from a single Gitea origin:
|
|
`$LARRY_BASE_URL` (default `https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main`).
|
|
|
|
Env vars that control fetching:
|
|
|
|
- `LARRY_BASE_URL` — override the origin (fork/mirror). No trailing slash.
|
|
- `LARRY_NO_UPDATE=1` (or `--no-update`) — skip the self-update entirely.
|
|
- `LARRY_GITEA_TOKEN` (alias `GITEA_TOKEN`) — a Gitea **personal access token**
|
|
(read scope) for **authenticated fetch against a PRIVATE repo**. When set,
|
|
every update/install fetch adds `Authorization: token <PAT>`. The token value
|
|
is never logged. Use this when the Gitea repo is private or the instance has
|
|
`REQUIRE_SIGNIN_VIEW=true`, so you don't have to flip the repo public.
|
|
|
|
```bash
|
|
LARRY_GITEA_TOKEN=<PAT> larry # authenticated auto-update
|
|
LARRY_GITEA_TOKEN=<PAT> bash install-larry.sh # authenticated install
|
|
```
|
|
|
|
**Hardening (v0.8.4):** every remote fetch is content-validated before the
|
|
bytes are trusted. If the origin returns the Gitea HTML *Sign-In* page (which
|
|
Gitea serves at HTTP 200 for an unauthenticated read of a private repo), the
|
|
installer/updater **fails loud** with an actionable error and does **not**
|
|
overwrite any real file — instead of silently parsing the HTML as
|
|
VERSION/MANIFEST/script content (the bug that stranded a client at v0.7.3).
|
|
The remedy is exactly what the error states: make the repo public +
|
|
`REQUIRE_SIGNIN_VIEW=false`, **or** set `LARRY_GITEA_TOKEN`.
|
|
|
|
---
|
|
|
|
## 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 = segments per line, with metadata header)
|
|
lib/nc-msgs.sh ADTto_3m --limit 5 --format text
|
|
|
|
# OUTPUT FORMATS:
|
|
# text (default) segments per line + metadata header per message
|
|
# oneline one message per line; segments separated by ⏎ marker
|
|
# fields each non-empty field on its own line: "SEG.N: value"
|
|
# mp alias for fields (v1 `mp`-style)
|
|
# labeled fields with alias names where known: "MSH.9 (msg_type): ADT^A08"
|
|
# raw raw bytes; messages separated by 0x1c (FS) — for piping
|
|
# json structured JSON
|
|
# count just the count
|
|
lib/nc-msgs.sh ADTto_3m --limit 3 --format oneline
|
|
lib/nc-msgs.sh ADTto_3m --limit 1 --format fields
|
|
lib/nc-msgs.sh ADTto_3m --limit 1 --format labeled # adds friendly aliases
|
|
|
|
# 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 operators (paths accept either . or - separators; same field name aliases everywhere):
|
|
# PATH=VALUE exact equality (any repetition)
|
|
# PATH!=VALUE not equal (no repetition matches)
|
|
# PATH~VALUE contains, case-insensitive
|
|
# PATH!~VALUE does not contain, case-insensitive
|
|
# PATH=NULL empty / absent / "" — any of those
|
|
# PATH= same as =NULL
|
|
# PATH=* wildcard — any non-empty value
|
|
# PATH!=NULL present (any non-empty repetition)
|
|
# Multiple --field flags AND together. For OR, run two queries.
|
|
|
|
# Examples using field-name ALIASES (case-insensitive; auto-translates to SEG.N)
|
|
lib/nc-msgs.sh ADTto_3m --field 'mrn=5720501458' --format count
|
|
lib/nc-msgs.sh ADTto_3m --field 'account_number=623000286' --format text
|
|
lib/nc-msgs.sh ADTto_3m --field 'event=A08' --format count
|
|
lib/nc-msgs.sh ADTto_3m --field 'visit=*' --format count # any non-empty
|
|
lib/nc-msgs.sh ADTto_3m --field 'ssn=NULL' --format count # missing SSN
|
|
lib/nc-msgs.sh ADTto_3m --field 'name~smith' --format text # contains
|
|
lib/nc-msgs.sh ADTto_3m --field 'name!~test' --format count # production-looking
|
|
lib/nc-msgs.sh ADTto_3m --field 'event=A08' --field 'visit=*' # AND of both
|
|
# Component access: name.2 (PID.5 component 2 = given name)
|
|
lib/nc-msgs.sh ADTto_3m --field 'name.2=SALLY' --format count
|
|
# Dash syntax (cheat-sheet style):
|
|
lib/nc-msgs.sh ADTto_3m --field 'PV1-3.4=100200' --format count
|
|
|
|
# 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.
|
|
|
|
---
|
|
|
|
## Cross-environment Larry — how the boxes communicate
|
|
|
|
**They don't, directly.** Each Larry on each box is independent. For regression
|
|
testing across two firewalled environments, the workflow is:
|
|
|
|
```
|
|
env-A (Windows) env-B (Linux)
|
|
nc-regression --phase env-a → nc-regression --phase env-b
|
|
--bundle-out /tmp/reg.tar.gz --bundle-in /tmp/reg.tar.gz
|
|
│ │
|
|
└────── you move the bundle ────────┘
|
|
(scp, gh release, USB,
|
|
shared mount, etc.)
|
|
```
|
|
|
|
The `--bundle-out` flag (after env-A phases 1-3) produces a tarball with:
|
|
- `inputs/*.msgs` — sampled messages from env-A smatdbs
|
|
- `outputs/env-a/*/*.out` — env-A route_test outputs
|
|
- `manifest.json` — env-A metadata + hints
|
|
- `inbounds.txt` — the threads tested
|
|
- `README.md` — instructions for the env-B side
|
|
|
|
Move the bundle however you can (see below). Then on env-B:
|
|
|
|
```bash
|
|
nc-regression.sh \
|
|
--bundle-in /path/to/reg.tar.gz \
|
|
--out /tmp/reg \
|
|
--env-b $HCIROOT --site-b $HCISITE \
|
|
--route-test-cmd '<env-b route_test command>' \
|
|
--phase env-b
|
|
```
|
|
|
|
That runs phases 4-6 (route_test on env-B with same inputs, diff each pair,
|
|
master summary).
|
|
|
|
### Bundle transport options (pick whichever your network allows)
|
|
|
|
| how | when |
|
|
|---|---|
|
|
| `scp` between the boxes | both boxes mutually SSH-reachable |
|
|
| `scp` to your laptop, `scp` to env-B | you can SSH to each separately |
|
|
| Private GitHub release / gist | both boxes can reach github.com but not each other |
|
|
| Shared NFS / SMB mount | the boxes share a filesystem |
|
|
| Box / SharePoint / S3 | enterprise common-storage |
|
|
| USB stick / portable drive | nothing else works |
|
|
| Tarball as email attachment | small bundles, both boxes have email |
|
|
|
|
The bundle is plain `tar.gz` — nothing Cloverleaf-specific. Inspect it with
|
|
`tar -tzf reg.tar.gz` to see what's inside before moving it.
|
|
|
|
### Lessons / refinements use the same pattern
|
|
|
|
Larry on a client captures lessons locally (`/lesson`, lesson_record tool).
|
|
You export the lesson bundle with `lib/lessons.sh export` and paste it back
|
|
to home-Larry (the dev-machine session you have with me). Same model:
|
|
local capture, manual transport, central merge. No direct peer protocol.
|
|
|
|
---
|
|
|
|
## 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
|
|
```
|
|
|
|
---
|
|
|
|
## PHI handling — sanitize / desanitize (`lib/hl7-sanitize.sh`, `lib/hl7-desanitize.sh`)
|
|
|
|
When working with prod data, tokenize PHI fields BEFORE they reach the API.
|
|
|
|
```bash
|
|
# Sanitize a file: replaces PHI fields with [[CATEGORY_NNNN]] tokens.
|
|
# Lookup table at ~/.larry/sanitize/lookup.tsv (mode 0600, never leaves the box).
|
|
lib/hl7-sanitize.sh /opt/cloverleaf/.../some.hl7 > /tmp/sanitized.hl7
|
|
|
|
# Pipe an entire smat-dump through sanitize:
|
|
lib/nc-msgs.sh ADTto_3m --limit 100 --format raw \
|
|
| lib/hl7-sanitize.sh > /tmp/sanitized-batch.hl7
|
|
|
|
# Strict mode also tokenizes unknown Z-segments wholesale:
|
|
lib/hl7-sanitize.sh --strict ./msg.hl7 > /tmp/sanitized.hl7
|
|
|
|
# See the current lookup table (PHI is here — DON'T share):
|
|
lib/hl7-sanitize.sh show-table
|
|
|
|
# Count entries:
|
|
lib/hl7-sanitize.sh count
|
|
|
|
# Clear the table (asks for confirmation):
|
|
lib/hl7-sanitize.sh clear-table
|
|
|
|
# Tokenize a single value (used by Larry's {{phi:...}} preprocessor):
|
|
lib/hl7-sanitize.sh tokenize-value --category MRN 12345
|
|
# → [[MRN_0001]]
|
|
|
|
# Detokenize a single token:
|
|
lib/hl7-sanitize.sh detokenize-value "[[MRN_0001]]"
|
|
# → 12345
|
|
|
|
# Desanitize a whole document (e.g. view Larry's tokenized output unmasked, locally):
|
|
cat larry-output.txt | lib/hl7-desanitize.sh | less
|
|
|
|
# Quick token lookup:
|
|
lib/hl7-desanitize.sh --token "[[NAME_0042]]"
|
|
|
|
# Override default PHI rules (rule file format: SEG|FIELD|CATEGORY per line)
|
|
lib/hl7-sanitize.sh --rules-file /tmp/my-rules.txt /tmp/msg.hl7
|
|
```
|
|
|
|
### Inside Larry (the REPL)
|
|
|
|
```
|
|
you> /phi 5720501458
|
|
phi> [[MRN_0001]] (use this in your next prompt)
|
|
|
|
you> find messages for {{phi:MRN:5720501458}} in last 3 days
|
|
phi> {{phi:MRN:5720501458}} → [[MRN_0001]]
|
|
(the actual prompt sent to Anthropic has [[MRN_0001]] — the original MRN never leaves the box)
|
|
|
|
you> /unmask [[NAME_0042]]
|
|
unmask> [[NAME_0042]] → MORRIS^SALLY^^^... (local only; never sent to API)
|
|
|
|
you> /tokens
|
|
(prints the full PHI ↔ token lookup table — local terminal only)
|
|
```
|
|
|
|
PHI inline syntax in any prompt:
|
|
- `{{phi:VALUE}}` — tokenize before send; auto-detects category (matches existing entries)
|
|
- `{{phi:MRN:12345}}` — explicit category=MRN (matches sanitized data)
|
|
- `{{phi:NAME:JOHN SMITH}}` — explicit category=NAME
|
|
|
|
### Default PHI rule set
|
|
|
|
Fields tokenized by default (override with `--rules-file`):
|
|
|
|
```
|
|
PID.2..7, .9, .11, .13, .14, .18, .19, .20, .21, .29, .30 (patient IDs, name, DOB, address, phone, account, SSN, license)
|
|
PV1.7, .8, .9, .17, .19, .50, .52 (providers, visit number)
|
|
NK1.2, .3, .4, .5, .6, .16 (next of kin)
|
|
GT1.3, .4, .5, .6, .7, .11, .12, .19 (guarantor)
|
|
IN1.16, .17, .18, .19, .20, .36, .49 (insurance)
|
|
OBR.10, .16, .32 / OBX.16 / DG1.3, .4 / ORC.10, .12 (orders/observations)
|
|
```
|
|
|
|
### Limitations (read these)
|
|
|
|
- **Your typed prompt can still leak PHI** if you don't use `{{phi:…}}` markers. Be deliberate.
|
|
- **Custom Z segments** aren't tokenized unless `--strict` is passed (which then redacts unknown Zs wholesale).
|
|
- **Free-text fields** (OBX.5 narratives, comments in NTE segments) can contain PHI in prose form. Default rules don't tokenize OBX.5; add it via `--rules-file` if your shop carries PHI in lab narratives.
|
|
- **Repetitions** (`~`-separated within a field) are tokenized as one value, not per-rep. Adequate for most analysis.
|
|
- **The lookup table at `~/.larry/sanitize/lookup.tsv` contains real PHI.** Mode 0600, never sent anywhere by these scripts, but it's still on disk. Wipe with `clear-table` before shipping the box anywhere.
|
|
|
|
---
|
|
|
|
## 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.
|