# @ariestools/cli

Aries Tools CLI — a suite by Arie Trouw for AI-client tooling, XL1 datalake provisioning, content hashing (strict + fuzzy + perceptual), XL1 wallet management, payload witnessing, and XYO protocol utilities.

## Installation

```sh
npm install -g @ariestools/cli
# or
pnpm add -g @ariestools/cli
```

## Quick start

```sh
aries --help
aries info
aries secrets status
ariesi hash --algorithm pdq ./photo.jpg
ariesi plex libraries
ariesi gate pair ticker BTC_USDT
aries wallet create
aries witness url --url https://example.com
aries datalake list
```

## Local datalake (standalone install)

The published CLI **embeds** the local datalake dev server. You do **not** need
private monorepo packages (`aries-datalake-plane`, etc.) installed.

```sh
# Isolated state (recommended for CI / Immortalizer)
export ARIES_HOME=/tmp/aries-test-$$

# Free ports by default (or pass --control-port / --plane-port)
aries datalake up
aries datalake list
aries datalake down
```

### Programmatic client (no shell-out per request)

```ts
import {
  createDatalakeClient,
  RestPayloadsClient,
} from '@ariestools/cli/datalake'

// After `aries datalake up` (or with baseUrl + authToken options)
const control = createDatalakeClient()
const lakes = await control.list()
// …
```

Private workspace packages stay private; runtime daemons ship under
`dist/bin/daemons/` inside this package.

## Top-level commands

The reference includes public `aries` commands and internal `ariesi` commands.
Internal commands require the private CLI build and an internal license tier;
the public installation cannot run them.

```text
aries ai           AI client tools
ariesi bank         Manage bank witness settings
aries chain        Run a local published-chain S3/REST server
aries datalake     Provision and manage on-demand XL1 datalakes
ariesi gate        Query the Gate exchange API (internal build)
ariesi hash [file]  Generate a hash (sha256, xyo, fuzzy, perceptual)
aries info         Display version and environment info
ariesi npmjs        Inspect and lint npmjs org packages
ariesi pentair      Manage Pentair ScreenLogic systems
ariesi plex        Inspect and manage Plex Media Server (internal build)
aries secrets      Secrets foundation, static AI instructions, and the human-only vault path
aries wallet       XL1 wallet — seed phrases, accounts, transactions, contacts
aries witness      Observe data and produce XYO-compliant payloads
aries xyo          XYO protocol utilities
```

## Global options

Available on every command:

| Option | Description |
|---|---|
| `-v, --verbose` | Enable verbose output |
| `--help-ai` | Print static AI-oriented help for a registered command path |
| `--version` | Show version number |
| `--help` | Show help |

---

## `aries secrets` — Secrets foundation and the human-only vault path

The current implementation provides versioned, static discovery and status,
the read-only inventory listing, local bastion profile and provider
configuration, and eight human-only commands, `init`, `create`, `reveal`,
`set`, `generate`, `delete`, `list`, and `describe`, each documented in the
next sections:

```sh
aries secrets status
aries secrets status --json
aries secrets --help-ai
aries secrets --help-ai --json
aries secrets init                                  # human-only; no --json form
aries secrets create --name "Deploy token" --input-fd 3 3<file   # human-only; value from fd 3, else the prompt
aries secrets reveal --secret-id sec_…              # human-only; the value, once, to the terminal
aries secrets set --secret-id sec_… --expected-version <v> --input-fd 3 3<file   # human-only; compare-and-swap replace
aries secrets generate --name "Signing key" --kind random-bytes --length 64      # human-only; drawn in-process, never printed
aries secrets delete --secret-id sec_… --expected-version <v>                    # human-only; compare-and-swap delete, not recoverable
aries secrets list                                  # human-only; metadata only
aries secrets describe --secret-id sec_…            # human-only; metadata only
```

`status` reports the implementation phase (`human-only-storage`) plus three
live observations: `bastionConfigured` is true when the bastion profile store
holds at least one profile and `data.bastionStore` names the store path and
its state (`configured`, `empty`, or `unreadable` — a refused or corrupt store
is reported, never thrown); `data.providers` carries the negotiated state of
every provider entry on the selected profile; and `data.humanOnlyPath` states
whether the human-only path is available on this platform. `data.state` is
`not-initialized` until a vault exists on the selected profile and
`human-only-storage` once one does. There is no `initialized` state and no
authenticated operator: `init` performs no operator or recovery bootstrap, so
the realm stays provisional and nothing accepts a grant.

`aries --help-ai [--json]` discovers registered AI-help subtrees. AI help is
embedded package content and runs before `.env`, settings, licensing, provider,
or broker initialization. Its manifest marks future commands as `planned`, the
human-only commands as `human-only` (risk `human-only`, no `--json` option),
and carries a `humanOnlyPath` block: `brokered: false`, `humanAttested:
true`, the eight conditions every human-only command enforces, the platforms
on which they can be satisfied (`darwin`, `linux`), `unlock:
'per-invocation'`, and `lease: 'not-available'`.

Agent-operational secret access is not implemented. This foundation cannot
connect to a bastion, create AI identities, authenticate with JWTs, manage
grants or approvals, or broker a secret to any program. The inventory listing
below observes presence metadata only: it initializes no provider and reads no
secret value. The bastion profile and provider commands record non-secret
configuration only: they contact no bastion and create no socket or vault.
The one path that touches a vault is human-only and unbrokered, described
under `aries secrets init`, `aries secrets create`, `aries secrets reveal`,
and the `set`, `generate`, `delete`, `list`, and `describe` sections below.

### The refusal contract

All eight human-only commands — `init`, `create`, `reveal`, `set`,
`generate`, `delete`, `list`, and `describe` — refuse unless standard input
and standard output are both interactive terminals, the platform provides
`/dev/tty`, `stty`, and a process-ancestry probe (an allow-list, so `win32`
and any unknown platform are refused outright), no AI agent host is detected
through a documented environment signal or an agent binary in the process
ancestry (an ambiguous probe fails closed), and argv carries no
structured-output option (`--json`, `--no-json`) and no delivery option
(`--delivery`, `--destination`, `--output`, `--out`, `--out-file`, `--fd`,
`--env`, `--clipboard`, `--temp-file`, `--to`) anywhere — including before
the `secrets` word. A secret value still has two ways in, both of them input
channels rather than delivery: the masked prompt on the attached terminal,
and `--input-fd <fd>` on `create` and `set`, which reads the value from a
descriptor the caller opened. Every refusal is `HUMAN_CHANNEL_REQUIRED`
(exit 2) with a fixed message on standard error that never reflects an argv
value and nothing at all on standard output, and the two kinds differ in
what they record: a refusal by the channel gate — a pipe, a non-terminal, an
unsupported platform, a detected or ambiguous agent host — is recorded as a
human-attested `refused` audit event, while an argv refusal is caught by the
preflight before any store is opened and writes no audit event at all. The
gate conditions are evaluated in the order above, so a piped or captured
caller is refused before any `ps` probe runs. Passing the gate refuses the
non-interactive case; it is not proof of a human, because a pseudo-terminal
remains agent-drivable.

No MCP tool and no GUI reaches this path. The `aries mcp` servers register no
tool that names a secret and import nothing from the secrets action tree; the
Electron shell and the dashboard import nothing from that tree either — not
the human-only modules, not the barrel that re-exports them, not the provider
seam, and not the encrypted-file vault package — and neither declares
`@ariestools/cli-lib` as a dependency (the dashboard reads only
`secrets inventory list --json` by running the packed CLI, which reads no
value). The vault package itself is not a GUI or IPC API: the only consumers
of its unlock, create, re-key, and record primitives are the human-only
command modules that handle plaintext and the provider seam they load it
through. There is no socket, no daemon, and no lease — a command unlocks the
vault for that invocation and locks it again before it returns.

### Storage layout under `~/.aries/secrets`

`ARIES_HOME` relocates `~/.aries` in full. Everything the CLI writes below it
is created `0600` inside `0700` directories. The three stores it owns —
`config/bastions.json`, `realms/<realmId>/secrets-index.json`, and
`audit/human-attested.jsonl` — are also checked when they are read: a group
or other permission bit, a foreign owner, a symlink, a non-regular file, or
an oversized one fails the command and names the `chmod` or `chown` fix
rather than repairing it. The vault files are written the same way and are
refused on a symlink, a non-regular file, or an oversized one, and their
directory is created `0700`. Two rows below are operator-authored and are
never written, moved, or permission-checked by the CLI: `inventory.json`,
read as declarations only, and `realms/<realmId>/denies.json`, which refuses
every secret-touching operation in this milestone by existing at all.

| Path | Contents |
| --- | --- |
| `config/bastions.json` | The bastion profile envelope: realms, bastions, endpoints, provider entries, and the active selection. Non-secret. `ARIES_SECRETS_BASTIONS` redirects it (absolute path). |
| `vaults/<providerId>/vault.json` | The encrypted-file vault header, format v1: plaintext parameters only — the vault ID, PBKDF2-SHA256 at 100,000 iterations, AES-256-GCM with a 12-byte IV, and the creation time. |
| `vaults/<providerId>/vault-password.json` | The password verifier: the PBKDF2 salt and a SHA-256 hash of the derived key. Never the password. |
| `vaults/<providerId>/records.json` | Every secret, as ciphertext with its authenticated metadata. The display name lives inside the encrypted body. |
| `realms/<realmId>/secrets-index.json` | The routing index: secret ID → `{ providerId, digest }`, and nothing else. |
| `realms/<realmId>/denies.json` | Deny records, consulted before every secret-touching operation. Nothing in this milestone writes it. |
| `audit/human-attested.jsonl` | The hash-chained, append-only human-attested audit log: one event per line, no secret value and no grant evidence. |
| `inventory.json` | The machine-wide managed-secret declarations, written by the operator and only read here. Presence metadata only. `ARIES_SECRETS_INVENTORY` redirects it. |

`broker.sock` is recorded on a local profile as the endpoint the future broker
would use; no socket is created, bound, or connected to in this build.

## `aries secrets inventory` — Managed secrets visibility

```sh
aries secrets inventory list
aries secrets inventory list --repo owner/myapp
aries secrets inventory list --source env --env-file ./deploy/.env --json
```

`secrets` is always registered at the free tier, so every example works
unchanged in the internal `ariesi` build.

Managed secrets are declared in one schema at two scopes, each a JSON file
with a root-level `inventory` list:

- `~/.aries/secrets/inventory.json` (override with `ARIES_SECRETS_INVENTORY`;
  `ARIES_HOME` relocates `~/.aries`) — machine-wide entries, each optionally
  naming its `repos`. The legacy `~/.config/aries/secrets.json` is still read
  when the new file is absent, with a notice on stderr and
  `data.legacyInventoryPath` under `--json`; move it to the new path.
- `<repo>/.config/aries/secrets.json` — entries needed by that repository's
  agents. These files are safe to commit: the closed entry shape
  (`service`, `account`, optional `label`, `env`, and — global only — `repos`)
  structurally rejects value-bearing fields, so a committed file can never
  become a secret store.

```json
{
  "version": 1,
  "inventory": [
    {
      "account": "ci-token",
      "env": "GITHUB_TOKEN",
      "label": "GitHub token",
      "service": "github"
    }
  ]
}
```

Presence is metadata only and never returns a value: on macOS the default
`auto` source probes the Keychain by `(service, account)` existence without
requesting the password; elsewhere (or with `--source env`) presence means the
mapped `env` variable is set and non-empty after `.env` loading. Repositories
are discovered below `ARIES_CODE_ROOT` (default `~/GitHub`). The `--json`
envelope follows the secrets result schema and contains identifiers and
presence booleans only — suitable as the dashboard panel's data source.

## `aries secrets bastion` — Bastion profiles

```sh
aries secrets bastion add --name "Laptop bastion"
aries secrets bastion list
aries secrets bastion show                      # active or sole profile
aries secrets bastion show "Laptop bastion"     # by display name
aries secrets bastion show --bastion bpf_…      # by profile ID
aries secrets bastion use "Laptop bastion"
aries secrets bastion remove "Laptop bastion"
aries secrets bastion list --json
```

A bastion profile is a closed, versioned, non-secret record naming a realm,
a bastion, its endpoint, and its providers; it holds no password, token, or
key material. Profiles live in one envelope at
`~/.aries/secrets/config/bastions.json` (`ARIES_HOME` relocates `~/.aries`;
`ARIES_SECRETS_BASTIONS` redirects the file for tests and must be absolute),
written `0600` inside a `0700` directory. A file or directory with group or
other permission bits, a foreign owner, a file over 256 KiB, or an unparseable
file fails every command with `BASTION_CONFIGURATION_INVALID` (exit 9), naming
only the path and the fix. A redirected store is disclosed on stderr in human
output and as `data.bastionStore` under `--json`.

- `add --name <displayName> [--kind local-ipc]` mints the profile, realm, and
  bastion identifiers, derives the audience, and records the inert socket path
  `<secretsRoot>/broker.sock` without touching the filesystem. The first
  profile written to an empty store becomes the active selection. Every new
  profile starts with one `encrypted-file` provider entry (`encrypted-local`,
  vault directory `<secretsRoot>/vaults/encrypted-local`); no vault is created.
  `--kind remote` is refused with `CAPABILITY_UNSUPPORTED` until Phase 9. A
  display name that collides with an existing profile ID or display name is an
  error, never an overwrite.
- `list` reads the store only: it never contacts a bastion or starts a broker,
  succeeds on a missing store with an empty table, and marks a `remote`
  profile `unusable in this build`.
- `show [<profile>] [--bastion <profile>]` resolves by profile ID first, then
  display name, else the active profile, else the sole profile; the result
  names which namespace matched. With nothing selectable it fails with
  `BASTION_NOT_SELECTED`. The endpoint is labeled `not used in this build`.
- `use <profile>` writes `activeProfileId` and nothing else. Selection is not
  authorization; a `remote` profile is refused.
- `remove <profile>` deletes the local record and clears an active selection
  that pointed at it. It revokes nothing: grants, delegations, and enrollments
  at the removed realm remain in force and must be revoked at the bastion
  separately, which the result states in both output forms.

`add`, `use`, and `remove` are mutating: they print the store path on stderr
for every write, and an agent must not run them without explicit operator
instruction (the AI-help manifest carries that rule). Every command takes
`--json`; the envelope follows the secrets result schema and typed failures
map to the secrets exit codes.

---

## `aries secrets provider` — Provider entries

```sh
aries secrets provider list
aries secrets provider show                       # active or sole provider
aries secrets provider show backup                # by provider ID
aries secrets provider add --id backup --kind encrypted-file
aries secrets provider add --id backup --kind encrypted-file --vault-path /srv/vaults/backup
aries secrets provider configure backup --vault-path /srv/vaults/backup
aries secrets provider use backup
aries secrets provider remove backup
aries secrets provider test                       # active or sole provider
aries secrets provider test backup --json
aries secrets provider list --bastion "Laptop bastion" --json
```

A provider entry is the non-secret placement record on a bastion profile:
`providerId`, `kind`, and `configurationRef` (for `encrypted-file`, the
absolute vault directory, default `<secretsRoot>/vaults/<providerId>`). It
holds no password and no key material; the vault's own files hold those.
Every command takes the inherited `--bastion <profile>` (profile ID first,
then display name; omitted means the active profile, else the sole profile,
else `BASTION_NOT_SELECTED`) and `--json`, discloses the store as
`data.bastionStore`, and refuses a `remote` profile with
`CAPABILITY_UNSUPPORTED`.

- `add --id <providerId> --kind encrypted-file [--vault-path <dir>]` appends
  one entry. The kind union is closed at the option table and again at the
  record parser, so `memory` and `macos-keychain` are refused by both. A
  duplicate ID is an error, never an overwrite. No vault and no directory is
  created: initialization needs a password and is human-only.
- `list` prints every entry with `initialized` — whether `vault.json` exists
  at `configurationRef`, a stat only, with no unlock and no header parse —
  and marks the active provider.
- `show [<providerId>]` resolves by ID, else the active provider (set by
  `use`), else the sole provider, and carries two fixed notes: unlock is
  per-invocation with no lease, and compare-and-swap is cooperative (it holds
  only among writers honoring the vault store lock).
- `configure <providerId> --vault-path <dir>` replaces `configurationRef`
  only; the kind is immutable and nothing is created, moved, or unlocked.
- `remove <providerId>` deletes the entry and clears an active provider
  selection that pointed at it. The vault directory is not deleted, which the
  result states in both output forms. The last provider of a profile cannot
  be removed; remove the profile instead.
- `use <providerId>` records `activeProviderId`, which satisfies `--provider`
  on management commands only. Placement on `create` and `generate` still
  requires `--provider` whenever the profile lists more than one provider;
  there is no default provider.
- `test [<providerId>]` resolves like `show` and reports the adapter's
  declared capabilities, `maximumSecretBytes`, a bounded health check, the
  per-invocation unlock mode, and the cooperative compare-and-swap caveat.
  It never prompts, unlocks, lists, or reads: the adapter is built around a
  locked placeholder session, and a locked vault — the designed steady state
  — succeeds with health `locked` and exit 0. Only an unavailable provider
  fails (`PROVIDER_UNAVAILABLE`, exit 7): not initialized (`next.command`
  names the human-only `init`), a corrupt header (reported with a redacted
  diagnostic, never a path), or a health check that misses its deadline
  (retryable). `status` runs the same negotiation for every provider entry
  on the selected profile and reports `providerCount` and per-provider rows
  under the same ok-when-locked rule; it still succeeds when every provider
  is unavailable.

`--vault-path` must be absolute and is refused when equal to or inside the
resolved XL1 wallet root, or inside the directory named by `XL1_WALLET_HOME`
or `ARIES_WALLET_HOME` when either is set, so a vault never shares a
directory with a wallet store; the refusal names the rule, never the path.
`add`, `configure`, `remove`, and `use` are mutating: they print the store
path on stderr for every write, and an agent must not run them without
explicit operator instruction.

## `aries secrets init` — Human-only vault creation

```sh
aries secrets init                                   # mints "Local bastion" when no profile exists
aries secrets init --name "Laptop bastion"           # display name for the minted profile
aries secrets init --bastion "Laptop bastion"        # an existing profile
aries secrets init --provider-id backup              # another entry; added to the profile when absent
```

`init` is the first human-only, unbrokered command. From an interactive
terminal it resolves the bastion profile (or mints a local one when none
exists), ensures an `encrypted-file` provider entry for `--provider-id`
(default `encrypted-local`, vault directory
`<secretsRoot>/vaults/<providerId>`), refuses an existing vault at that
entry's location (`VERSION_CONFLICT`, exit 8; the refusal names the rule,
never the path), prompts for the vault password twice on `/dev/tty` with
the vault's 12-character minimum, creates the vault, opens it once to confirm
it answers under that password, locks it again, and prints the profile,
provider, and vault summary. Unlock is per-invocation: no lease, session
file, or key survives the command. The password is read from the terminal
alone — never from an environment variable or a command-line argument.

`init` initializes one vault and one local profile only. It performs no
operator or recovery bootstrap: the realm stays provisional in the plan's
sense, nothing accepts a grant, and neither `status` nor `--help-ai`
describes the operator as authenticated.

Every human-only command enforces these rules, each covered by a test:

- standard input and standard output must both be interactive terminals; a
  piped, redirected, or captured channel fails with `HUMAN_CHANNEL_REQUIRED`
  (exit 2) on stderr and writes nothing to stdout;
- a detected AI agent host (Claude Code, Codex, Cursor, Aider, and the other
  documented signals, or an agent binary in the process ancestry) is refused,
  and an ambiguous probe fails closed;
- there is no `--json` and no delivery option; `--json`, `--no-json`,
  `--delivery`, `--destination`, `--output`, `--fd`, `--env`, `--clipboard`,
  `--temp-file`, or `--to` anywhere in argv — including before the `secrets`
  word — is refused with a fixed message before yargs runs, and the refused
  value is never reflected;
- the command is not mirrored as an MCP tool and is reachable over no IPC,
  socket, or remote transport;
- every run, refusal, and failure appends a human-attested, unbrokered event
  to `<secretsRoot>/audit/human-attested.jsonl`; an event that cannot be
  written refuses the operation (`AUDIT_UNAVAILABLE`);
- the realm's deny records are consulted before a secret-touching operation
  (`init` alone is exempt, because no realm exists before it mints one);
  `create`, `reveal`, `set`, `generate`, `delete`, `list`, and `describe`
  refuse with `POLICY_INVALID` (exit 5) when the deny set cannot be read,
  which in this milestone includes any existing
  `<secretsRoot>/realms/<realmId>/denies.json`, since nothing writes one yet;
- on `win32` every human-only command fails with `HUMAN_CHANNEL_REQUIRED` and
  reason `platform-unsupported`, which `status` and the manifest state.

Agents must not invoke a human-only command, wrap it, or treat it as agent
access: it identifies no principal, evaluates no grant, and confers nothing
on any agent principal.

## `aries secrets create` — Human-only secret storage

```sh
aries secrets create --name "Deploy token" --input-fd 3 3<./token.txt   # value from descriptor 3
aries secrets create --name "Deploy token"                               # masked prompt, typed twice
aries secrets create --name "API key" --provider backup                  # placement on a named entry
```

`create` stores one secret in the operator's own vault. From an interactive
terminal it resolves the bastion profile (`--bastion`, else the active
profile, else the sole profile — it never mints one), places the secret on
`--provider` (required when the profile lists more than one provider,
implied when it lists exactly one; the active provider never satisfies
placement), consults the realm's deny records, appends the `intent` audit
event, prompts for the vault password on `/dev/tty`, collects the value,
writes the record, indexes it, locks the vault, and prints the secret ID
and version — never the value.

The value comes from exactly one of two sources. `--input-fd <n>` reads a
trusted producer's descriptor, which must be numbered 3 or higher, open,
and not a terminal: standard input is never accepted, because it may be
model-composed or captured in a transcript. The read is bounded at 65536
bytes and exactly one trailing newline is removed. Without `--input-fd`
the value is typed on the terminal, masked, and confirmed. In both cases
the record is stored as `text/plain`, and the descriptor is read before
the password prompt so a closed or reserved descriptor is refused early.
The display name (`--name`, at most 256 bytes, no control characters) is
stored inside the encrypted record body, where the vault's associated data
authenticates it.

The write spans two stores under two leases and is not atomic, so its order
is fixed: the provider record is created first, then the index entry. If
the index write fails, the record is deleted again under the same session
and the command reports the index failure with provider effect none; if
that deletion cannot be proven, the command reports
`PROVIDER_OUTCOME_UNKNOWN` (exit 7) naming the record's opaque ID, which
may then remain in the vault unindexed until it is reconciled.

### The secret index

Each realm has one routing index at
`<secretsRoot>/realms/<realmId>/secrets-index.json`, written `0600` inside
a `0700` realm directory through the same journaled, atomic store that
holds the bastion profiles. It maps a secret ID to `{ providerId, digest }`
and nothing else: no display name, content type, byte length, or timestamp
ever lands in it, so the file discloses routing alone. The `digest` is
SHA-256 over the canonical JSON of `{ secretId, providerId, opaqueId }`,
computed at create and re-verified on every resolve; `opaqueId` equals the
secret ID by construction, because the human path mints a canonical
22-character `sec_` token that the encrypted-file vault stores verbatim.
The digest is unkeyed: it detects an accidental edit or a truncated write,
and nothing more — any party with write access to the file can recompute
it. Phase 3 replaces it with a broker-held keyed MAC. Do not delete the
index: every secret it routes becomes unreachable by ID.

## `aries secrets reveal` — Human-only reveal to the terminal

```sh
aries secrets reveal --secret-id sec_…                     # the value, once, then a newline
aries secrets reveal --secret-id sec_… --bastion "Laptop"  # an explicit profile
```

`reveal` resolves `--secret-id` through the realm's secret index alone. A
missing entry is `INVALID_ARGUMENT` (exit 2); an entry naming a provider
the profile no longer lists, or one whose digest no longer verifies, is
`BASTION_CONFIGURATION_INVALID` (exit 9) — each fails closed, and no
provider is ever opened or searched to find a matching record. `--provider`
is refused with `INVALID_ARGUMENT` for the same reason: the index is the
only routing input, so a reference can never be redirected from the
command line. The command then consults the deny records, appends the
`intent` event, prompts for the vault password, reads the record, locks the
vault, appends the `completed` event, and writes the value exactly once to
standard output followed by one newline — nothing else is written there.
There is no other delivery: no destination, file, descriptor, environment,
clipboard, or temporary file, and no structured output. The value never
appears in an audit line or on standard error.

## `aries secrets set` — Human-only compare-and-swap replace

```sh
aries secrets set --secret-id sec_… --expected-version <v> --input-fd 3 3<./new.txt   # value from descriptor 3
aries secrets set --secret-id sec_… --expected-version <v>                             # masked prompt, typed twice
```

`set` replaces one secret's value in the operator's own vault. It resolves
`--secret-id` through the realm's secret index alone, exactly as `reveal`
does (`--provider` is refused with `INVALID_ARGUMENT`), consults the deny
records, appends the `intent` event, prompts for the vault password, collects
the new value from `--input-fd` (3 or higher; read before the password prompt)
or the masked terminal prompt with confirmation, and performs one
compare-and-swap replace: `--expected-version` must equal the record's
current version token exactly as the last `create`, `generate`, `set`, or
`describe` printed it, or the command fails with `VERSION_CONFLICT` (exit 8)
and nothing changes. The display name and content type are preserved from
the record. The reference does not change, so the index is untouched; the
command prints the new version and the superseded one, never the value. A
replace whose effect the provider cannot prove is `PROVIDER_OUTCOME_UNKNOWN`
(exit 7), naming the record and stating that the index still routes it.

## `aries secrets generate` — Human-only in-process generation

```sh
aries secrets generate --name "Signing key" --kind random-bytes --length 64
aries secrets generate --name "Webhook token" --kind base64url-token
aries secrets generate --name "Database password" --kind password --length 24 --provider backup
```

`generate` draws a value in the operator's own process from Web Crypto and
stores it exactly as `create` would: placement on `--provider` (or the sole
provider), the deny consultation, the `intent` event, the password prompt,
the provider record first with the display name inside the encrypted body,
then the routing entry in the realm's index, with the same rollback when the
index write fails. It prints the secret ID, kind, size, and version — the
value is never printed, logged, audited, or returned, and exists only in the
record's ciphertext; read it back with `reveal`. `--kind` is a closed list
and `--length` counts the kind's unit inside an inclusive range:

| Kind | Stored as | `--length` | Default | Range |
| --- | --- | --- | --- | --- |
| `random-bytes` | `application/octet-stream`, raw bytes | bytes | 32 | 8–1024 |
| `base64url-token` | `text/plain` | characters | 32 | 8–512 |
| `password` | `text/plain` | characters | 32 | 8–256 |

`password` uses a fixed 70-symbol printable alphabet with the ambiguous
`0 O 1 l I` removed and no space, quote, backslash, `$`, backtick, or
redirection character; `base64url-token` uses the 64 base64url symbols. Both
are uniform: a byte that would bias the draw is rejected, never reduced. An
unlisted kind or an out-of-range length is `INVALID_ARGUMENT` (exit 2) before
anything runs.

## `aries secrets delete` — Human-only compare-and-swap delete

```sh
aries secrets delete --secret-id sec_… --expected-version <v>
```

`delete` resolves `--secret-id` through the index alone, consults the deny
records, appends the `intent` event, prompts for the vault password, and
deletes the record under the `--expected-version` guard; a stale guard is
`VERSION_CONFLICT` (exit 8) and nothing changes. It then removes the routing
entry from the realm's index: the tombstone is the removal itself, so the ID
no longer resolves through any command and a later `describe` or `reveal`
stops at the index with `INVALID_ARGUMENT`. **The vault record is gone and is
not recoverable through aries**: the encrypted-file provider discards the
ciphertext and keeps no tombstone, which the receipt reports as
`unrecoverable`. The audit line records the secret ID and the fresh tombstone
version only. If the record is deleted but the index cannot be updated, the
command reports `PROVIDER_OUTCOME_UNKNOWN` (exit 7) naming both facts — the
record is gone, the index may still route the ID. Until you reconcile,
`describe` on that ID fails either at the vault with `RESOURCE_UNAVAILABLE`,
with `list` showing it as `missing`, or at the index with `INVALID_ARGUMENT`
if the entry did go after all.

## `aries secrets list` — Human-only metadata listing

```sh
aries secrets list                     # the active provider, or the sole provider
aries secrets list --provider backup   # one provider entry
```

`list` resolves the provider entry (`--provider`, else the active provider,
else the sole one), consults the deny records for the realm's catalog,
prompts for the vault password, and prints metadata alone: for every index
entry routed to that provider, the secret ID, version, incarnation, content
type, byte length, display name, and created and updated timestamps, read
from the vault's own description; an entry whose record the provider no
longer holds is shown as `missing`, one whose index digest does not verify
as `integrity-failed` (the vault is not consulted for it), and one whose
record the vault holds but will not describe as `undescribable` with the
failure code — a single unreadable record never hides the rest of the
catalog. Entries routed to other providers on the profile are counted, not
listed. The
command also enumerates the provider's own records and reports every record
the index does not route to that provider as `unindexed`, with its opaque
ID, version, byte length, and timestamps — never its value or display name.
An unindexed record is reachable by no aries command until it is reconciled,
because the index is the only routing input. No value is ever printed.

## `aries secrets describe` — Human-only metadata for one secret

```sh
aries secrets describe --secret-id sec_…
```

`describe` resolves `--secret-id` through the index alone (`--provider` is
refused), consults the deny records, prompts for the vault password, and
prints the record's current version, incarnation, created and updated
timestamps, content type, byte length, and display name — never the value,
which is decrypted only to measure it and wiped inside the library. A
record the vault does not hold is `RESOURCE_UNAVAILABLE` (exit 7); an ID the
index does not route is `INVALID_ARGUMENT` (exit 2) before any vault is
opened. The printed version is the `--expected-version` for the next `set`
or `delete`.

---

## `aries chain` — Local published-chain server

```sh
# Plain HTTP on loopback (default)
aries chain up

# Locally trusted HTTPS at https://chain.aries.test:8791 (macOS)
brew install mkcert
aries chain up --ssl auto
```

The server exposes the published-chain buckets at `/blocks`, `/state`, and
`/indexes`. `--ssl auto` keeps the listener on `127.0.0.1`, installs or reuses
mkcert's local development CA, generates a certificate under
`~/.aries/chain/tls`, and adds an idempotent `chain.aries.test` entry to
`/etc/hosts`. The first run may request administrator authorization. The CA and
hosts entry remain installed after `chain down`; `chain reset` removes the
generated Aries certificate files.

| Option | Default | Description |
|---|---|---|
| `--port <number>` | `8791` | Listener port |
| `--host <address>` | `127.0.0.1` | Bind address; auto TLS requires the default |
| `--ssl off\|auto` | `off` | Enable macOS automatic local HTTPS setup |
| `--backing memory` | `memory` | Ephemeral storage, wiped on shutdown |
| `--timeout <seconds>` | `10` | Startup health-check timeout |

The ready banner prints the `S3_ENDPOINT` used by `ariesi xyo s3 ...`. Aries
automatically selects path-style S3 addressing and trusts the generated CA for
the `chain.aries.test` endpoint. `S3_CA_BUNDLE` can provide a different private
CA file for another HTTPS S3-compatible endpoint.

---

## `ariesi bank` — Manage bank witness settings

```sh
ariesi bank capitalone config get
ariesi bank capitalone config set base-url https://api-sandbox.capitalone.com
ariesi bank capitalone config set token-url https://api-sandbox.capitalone.com/oauth2/token
ariesi bank capitalone config set client-id "$CAPITALONE_CLIENT_ID"
ariesi bank capitalone config set client-secret "$CAPITALONE_CLIENT_SECRET"
ariesi bank capitalone config set accounts-path /accounts
ariesi bank capitalone config set balances-path '/accounts/{accountId}/balances'
ariesi bank capitalone config set transactions-path '/accounts/{accountId}/transactions'
```

Capital One config is stored in `~/.aries/bank/capitalone.json` with restricted
file permissions. `ARIES_HOME` overrides the `~/.aries` root.

Capital One U.S. Customer Transactions is a private DevExchange product. Aries
therefore requires the endpoint paths supplied by your Capital One partner docs
for `aries witness bank --resource accounts`.

The public sandbox `Retrieve Consumer Bank Products` product is a deposit product
catalog, not personal account data. Use `aries witness bank --resource products`
for that API; Aries uses Capital One's documented sandbox defaults unless you
override `base-url` or `token-url`.

### Capital One config keys

| Key | Env var | Default | Description |
|---|---|---|---|
| `env` | `ARIES_CAPITALONE_ENV` | `sandbox` | Environment label: `sandbox` or `production` |
| `base-url` | `ARIES_CAPITALONE_BASE_URL` | | Capital One API base URL |
| `token-url` | `ARIES_CAPITALONE_TOKEN_URL` | | OAuth client-credentials token URL |
| `client-id` | `ARIES_CAPITALONE_CLIENT_ID` | | OAuth client id |
| `client-secret` | `ARIES_CAPITALONE_CLIENT_SECRET` | | OAuth client secret |
| `scope` | `ARIES_CAPITALONE_SCOPE` | | Optional OAuth scope |
| `accounts-path` | `ARIES_CAPITALONE_ACCOUNTS_PATH` | | Accounts endpoint path |
| `balances-path` | `ARIES_CAPITALONE_BALANCES_PATH` | | Balances endpoint path; may include `{accountId}` |
| `transactions-path` | `ARIES_CAPITALONE_TRANSACTIONS_PATH` | | Transactions endpoint path; may include `{accountId}` |

`config get` redacts `client-secret`; pass `--show-secrets` to print it. Access
tokens are not persisted; pass `--access-token` or set `ARIES_CAPITALONE_ACCESS_TOKEN`
for one run.

---

## `ariesi pentair` — Manage Pentair ScreenLogic systems

```sh
ariesi pentair config get
ariesi pentair config get address
ariesi pentair config set address 192.0.2.10
ariesi pentair config set port 80
ariesi pentair config set system-name "Pentair ScreenLogic"
ariesi pentair circuit runtime set --circuit-id 6 --hours 24 --on
```

Pentair config is stored in `~/.aries/pentair/config.json` and is used by the
Pentair witness and circuit commands. `ARIES_HOME` overrides the `~/.aries` root.

### Config keys

| Key | Default | Description |
|---|---|---|
| `address` | | ScreenLogic adapter IP for direct connection; omit to use UDP discovery |
| `port` | `80` | ScreenLogic adapter TCP port for direct connection |
| `password` | `SCREENLOGIC_PASSWORD` | ScreenLogic password for direct connection |
| `system-name` | `Pentair ScreenLogic` | ScreenLogic system name for direct connection |
| `response-timeout` | `15000` | Milliseconds to wait for each ScreenLogic command response |
| `search-timeout` | `5000` | Milliseconds to wait for each UDP discovery attempt |
| `search-attempts` | `3` | Number of UDP discovery attempts before failing |
| `series-file` | `~/.aries/pentair/intellichem.jsonl` | JSONL file used for saved IntelliChem samples and graphing |

`config get` redacts `password`; pass `--show-secrets` to print it.

### `ariesi pentair circuit runtime set`

Set the ScreenLogic circuit egg timer. Runtime values are sent to ScreenLogic as
minutes; `--hours 24` becomes `1440`.

| Option | Description |
|---|---|
| `--circuit-id <id>` | Pentair circuit id to update |
| `--hours <n>` | Runtime in hours; mutually exclusive with `--minutes` |
| `--minutes <n>` | Runtime in minutes; mutually exclusive with `--hours` |
| `--on` | Turn the circuit on after setting the runtime |
| `--json` | Output as JSON |

Connection override flags are also accepted: `--address`, `--port`,
`--password`, `--system-name`, `--response-timeout`, `--search-timeout`, and
`--search-attempts`.

---

## `ariesi plex` — Plex Media Server tools

The Plex command is available from the internal `ariesi` build with
`license.tier=internal`.

### `ariesi plex libraries`

List libraries configured in a Plex Media Server. By default, Aries tries the local server at `http://127.0.0.1:32400` and discovers the Plex token from `PLEX_TOKEN`, `X_PLEX_TOKEN`, `PLEX_AUTH_TOKEN`, local Plex `.LocalAdminToken`, or a local Plex `Preferences.xml`. If local API auth still blocks access, Aries falls back to the local Plex library database and then `Plex Media Scanner --list`.

| Option | Default | Description |
|---|---|---|
| `--url <url>` | `PLEX_URL` or `http://127.0.0.1:32400` | Plex server URL |
| `--token <token>` | env/local Preferences.xml | Plex auth token |
| `--preferences <path>` | auto-detect | Plex `Preferences.xml` path for token discovery |
| `--database <path>` | auto-detect | Plex library database path for local fallback |
| `--no-fallback` | | Disable fallback to `Plex Media Scanner --list` when the API is unavailable |
| `--json` | `false` | Output as JSON |

### `ariesi plex lint [--library <name-or-id>]`

Run read-only lint checks against the Plex setup. The first rule, `multiple-sources`, reports items that have more than one original source media copy. Plex optimized versions and files matching Plex's local extras naming conventions do not count as sources.

| Option | Default | Description |
|---|---|---|
| `-l, --library <name-or-id>` | all libraries | Plex library name or ID, e.g. `Movies` or `9` |
| `--database <path>` | auto-detect | Plex library database path |
| `--json` | `false` | Output as JSON |

### `ariesi plex items --library <name-or-id>`

List top-level items in a Plex library from the local Plex database, including media resolution and optimization details.

| Option | Default | Description |
|---|---|---|
| `-l, --library <name-or-id>` | required | Plex library name or ID, e.g. `Movies` or `9` |
| `--database <path>` | auto-detect | Plex library database path |
| `--fields <list>` | `media,optimizations` | Comma-separated details: `media`, `optimizations`, `files`, `all` |
| `--limit <n>` | | Maximum number of items to inspect |
| `--json` | `false` | Output as JSON |

### `ariesi plex items optimize --tv --library <name-or-id>`

Queue Plex "Optimized for TV" versions for items that do not already have one. Aries inspects the local Plex database first, skips items already marked `Optimized for TV`, and then queues missing items through the local Plex optimizer API.

| Option | Default | Description |
|---|---|---|
| `-l, --library <name-or-id>` | required | Plex library name or ID, e.g. `Movies` or `9` |
| `--tv` | required | Optimize missing items for Plex's TV profile |
| `--dry-run` | `false` | Show what would be queued without changing Plex |
| `--url <url>` | `PLEX_URL` or `http://127.0.0.1:32400` | Plex server URL |
| `--token <token>` | env/local token discovery | Plex auth token |
| `--preferences <path>` | auto-detect | Plex `Preferences.xml` path for token discovery |
| `--database <path>` | auto-detect | Plex library database path |
| `--limit <n>` | | Maximum number of missing optimizations to queue |
| `--inspect-limit <n>` | | Maximum number of library items to inspect |
| `--json` | `false` | Output as JSON |

### `ariesi plex items optimize queue [--library <name-or-id>]`

List items currently in Plex's optimizer queue. When `--library` is omitted, Aries lists queued optimizer items across all libraries.

| Option | Default | Description |
|---|---|---|
| `-l, --library <name-or-id>` | all libraries | Plex library name or ID, e.g. `Movies` or `9` |
| `--url <url>` | `PLEX_URL` or `http://127.0.0.1:32400` | Plex server URL |
| `--token <token>` | env/local token discovery | Plex auth token |
| `--preferences <path>` | auto-detect | Plex `Preferences.xml` path for token discovery |
| `--database <path>` | auto-detect | Plex library database path for library names |
| `--json` | `false` | Output as JSON |

---

## `ariesi gate` — Gate exchange API

The Gate command is available from the internal `ariesi` build with
`license.tier=internal`.

A read-only client for the [Gate](https://www.gate.com) exchange REST API (v4). It calls only
unauthenticated public spot endpoints — no API key is read, and nothing is ever traded.

These options are available on every `gate` subcommand:

| Option | Default | Description |
|---|---|---|
| `--base-url <url>` | `ARIES_GATE_API_URL`, `GATE_API_URL`, or `https://api.gateio.ws/api/v4` | Gate API v4 base URL |
| `--json` | `false` | Output as JSON |

### `ariesi gate pair list <symbol>`

List every Gate spot pair that trades `<symbol>`. The symbol matches on either side of the pair by default, so `BTC` returns both `BTC_USDT` (where it is the base) and `ETH_BTC` (where it is the quote). Pairs that are not currently tradable are hidden unless `--all` is passed.

| Option | Default | Description |
|---|---|---|
| `--side any\|base\|quote` | `any` | Match the symbol only as the base or only as the quote currency |
| `--all` | `false` | Include pairs whose trade status is not `tradable` |

```sh
ariesi gate pair list XYO
```

```text
XYO_ETH   XYO Network / Ethereum  fee 0.2%
XYO_USDT  XYO Network / Tether  fee 0.2%
2 pair(s) for XYO
```

### `ariesi gate pair ticker <pair>`

Show the 24-hour spot ticker for a single pair — last price, change, bid/ask, high/low, and base + quote volume.

The pair can be written as one argument (`BTC_USDT`, `BTC/USDT`, `BTC-USDT`, or a **quoted** `"BTC|USDT"` — an unquoted pipe is consumed by the shell before Aries sees it), or as its two halves in separate arguments (`BTC USDT`).

```sh
ariesi gate pair ticker BTC_USDT
```

```text
BTC_USDT
  last          78862
  change 24h    +0.04%
  bid / ask     78861.9 / 78862
  high / low    79235.9 / 77638.6
  base volume   4902.5960069814 BTC
  quote volume  384979909.02896446684 USDT
```

---

## `aries ai` — AI client tools

### `aries ai detect`

Scan for installed AI clients.

| Option | Default | Description |
|---|---|---|
| `--json` | `false` | Output as JSON |

### `aries ai lint [path]`

Check Claude Code and Codex AI configuration drift. The folder defaults to the current directory.

| Option | Default | Description |
|---|---|---|
| `--scope <all\|project\|home>` | `all` | Check project config, shared/custom home config, or both |
| `--json` | `false` | Output as JSON |

### `aries ai skills list [tool]`

List installed skills discovered for Claude Code, Codex, and Cursor. The optional
`tool` positional accepts `claude`, `codex`, or `cursor`; omit it to inspect all
three. Cursor currently reports no skills. Detection and listing do not modify client configuration.

| Option | Default | Description |
|---|---|---|
| `--json` | `false` | Output as JSON |

AI lint also inspects shared/custom home plugin manifests and skill files for
drift without changing them.

---

## `aries datalake` — Provision and manage on-demand XL1 datalakes

### Authentication

#### `aries datalake login`
| Option | Default | Description |
|---|---|---|
| `--base-url <url>` | | Control-plane base URL (P0: required) |
| `--token <jwt>` | | Pre-issued auth token (P0: required; OAuth lands in P1) |

`aries datalake logout` — clears stored credentials, no flags.

### Lifecycle

#### `aries datalake create <name>`
| Positional | | Description |
|---|---|---|
| `name` | required | Human-friendly name (unique per owner) |

| Option | Default | Description |
|---|---|---|
| `-t, --tier <small\|medium\|large\|archive>` | `small` | Service tier |
| `-s, --size <e.g. 10GB>` | | Capacity |
| `--iops <n>` | | Provisioned IOPS target |
| `-r, --region <us-west-2\|us-east-1\|eu-west-1>` | | Deployment region |
| `--retention-days <n>` | tier default | Retention in days |
| `--verify-hashes` | `false` | Reject inserts whose `$hash` does not match the content digest |
| `--rate-auth-per-minute <n>` | | Override: authenticated requests per minute |
| `--rate-anon-per-minute <n>` | | Override: anonymous requests per minute |
| `--rate-burst-factor <n>` | | Override: rate-limit burst factor (1 = no burst) |

`aries datalake list` — no flags.

#### `aries datalake describe [name]` / `aries datalake destroy [name]`
| Positional | | Description |
|---|---|---|
| `name` | active default | Datalake name or id |

`destroy` adds:
| Option | Default | Description |
|---|---|---|
| `-f, --force` | `false` | Required confirmation flag |

#### `aries datalake use [name]`

Sets the default datalake for subsequent commands. Omit the name to clear.

### Access control

#### `aries datalake grant <name> <principal>`
| Positional | | Description |
|---|---|---|
| `name` | required | Datalake name or id |
| `principal` | required | User id, or the literal `public` |

| Option | Default | Description |
|---|---|---|
| `-r, --role <viewer\|runner>` | `viewer` | Role to grant |
| `--confirm-public` | `false` | Required to grant `public runner` (anonymous writes) |

#### `aries datalake revoke <name> <principal>`

Same positionals; no command-specific options.

#### `aries datalake token [name]`
| Positional | | Description |
|---|---|---|
| `name` | active default | Datalake name or id |

| Option | Default | Description |
|---|---|---|
| `-r, --role <viewer\|runner>` | `viewer` | Role encoded in the token |
| `--ttl <seconds>` | server-capped | Lifetime in seconds |

### Data plane

#### `aries datalake push <file> [name]`
| Positional | | Description |
|---|---|---|
| `file` | required | Path to a `.json` (array) or `.jsonl` (one payload/line) file |
| `name` | active default | Target datalake |

| Option | Default | Description |
|---|---|---|
| `--batch-size <n>` | `500` | Max payloads per HTTP request |
| `--verify` | `false` | Verify each payload's `$hash` against content locally before pushing |
| `--xl1-sdk` | `false` | Use the real `@xyo-network/xl1-protocol-sdk RestDataLakeRunner` |

#### `aries datalake fetch <hash> [name]`
| Positional | | Description |
|---|---|---|
| `hash` | required | Content hash of the payload |
| `name` | active default | Target datalake |

| Option | Default | Description |
|---|---|---|
| `--raw` | `false` | Print only the raw JSON payload (no header) |
| `--xl1-sdk` | `false` | Use the real `@xyo-network/xl1-protocol-sdk RestDataLakeViewer` |

#### `aries datalake tail [name]`
| Positional | | Description |
|---|---|---|
| `name` | active default | Datalake name or id |

| Option | Default | Description |
|---|---|---|
| `-f, --follow` | `false` | Keep polling for new payloads after draining the backlog |
| `-n, --limit <n>` | `50` | Max payloads per page |
| `--cursor <hash>` | | Resume after this hash (exclusive) |
| `--schemas <csv>` | | Comma-separated list of schemas to include |
| `--poll-interval <ms>` | `1000` | Poll interval in ms when following |

### Audit (data-plane log inspection)

```sh
aries datalake audit          # default: view
aries datalake audit view     # Print audit log rows
aries datalake audit purge    # Delete rotated audit files older than --max-age-days
```

#### `aries datalake audit view`
| Option | Default | Description |
|---|---|---|
| `-f, --follow` | `false` | Stream new rows as they arrive (tails the most recent file) |
| `-n, --lines <n>` | `50` | Tail this many rows (0 = start fresh in `--follow` mode) |
| `--datalake-subject <subject>` | | Filter by the provider-scoped datalake pseudonym stored in audit rows |
| `--status <prefix>` | | Status code prefix filter (e.g. `4` matches 4xx) |
| `--method <verb>` | | HTTP method filter (`GET`, `POST`, …) |
| `--authenticated <true\|false>` | | Show only authenticated or only anonymous requests |
| `--grep <regex>` | | Regex pattern applied to the raw JSON line |
| `--file <path>` | | File or directory (auto-detects rotation layout) |

#### `aries datalake audit purge`
| Option | Default | Description |
|---|---|---|
| `--max-age-days <n>` | required | Retention window; files older than this are deleted |
| `--directory <path>` | `<ARIES_HOME>/dev/audit` | Rotation directory |
| `--dry-run` | `false` | Report what would be deleted without removing anything |

### Local development

```sh
aries datalake up        # Start the local control + data plane dev server
aries datalake down      # Stop it (clears credentials)
aries datalake status    # Report whether the local dev server is running and healthy
aries datalake logs      # Print recent dev server log output
aries datalake reset     # Stop the dev server and wipe all local state
```

The original `aries datalake dev up|down|status|logs|reset` spellings remain
available for compatibility.

#### `aries datalake up`
| Option | Default | Description |
|---|---|---|
| `--control-port <n>` | free loopback port | Control-plane port |
| `--plane-port <n>` | free loopback port | Data-plane port |
| `--control-audience <aud>` | `aries-datalake-control` | Exact JWT audience accepted by the control plane |
| `--cors-origin <origin>` | disabled | Exact browser origin to allow; repeat for multiple origins |
| `--timeout <s>` | `10` | Seconds to wait for health |
| `--persist` | `false` | Persist store state to disk so it survives restarts |
| `--audit` | `false` | Write a JSONL audit log of every data-plane request |
| `--audit-rotate` | `false` | Rotate the audit log daily (writes to `<home>/dev/audit/`) |
| `--audit-keep-days <n>` | | Prune rotated audit files older than this many days |

#### `aries datalake logs`
| Option | Default | Description |
|---|---|---|
| `-f, --follow` | `false` | Stream new lines as they arrive |
| `-n, --lines <n>` | `50` | Number of tail lines to show |

`down`, `status`, `reset` take no command-specific options.

---

## `ariesi signing-pool` — XL1 gas-sponsored transaction signing

Signing pools are wallet-backed servers that co-sign XL1 transactions so the
pool address pays gas instead of the caller. The transaction must use signature
slot 0 for the pool and signature slot 1 for the caller:

- `addresses[0]` is the signing-pool address.
- `addresses[1]` is the authenticated caller address.
- `$signatures[0]` must be `null` when submitted.
- `$signatures[1]` must already verify against `addresses[1]`.

`signing-pool sign` submits a hydrated transaction tuple
`[TransactionBoundWitness, payloads[]]`. For compatibility with the original
CLI helper, the input file may also be an unsigned template; in that case the
CLI fills the pool/caller slots, signs slot 1 with the active XL1 wallet, and
sends the hydrated tuple to the pool.

```sh
ariesi signing-pool create <name> --daily-limit <atto> --max-tx-gas <atto> --min-exp-blocks 10 --max-exp-blocks 500
ariesi signing-pool grant <id> <caller-address>
ariesi signing-pool sign <id> ./tx.json --output ./signed-tx.json
ariesi signing-pool dev up --xl1-node-rpc-url http://127.0.0.1:8545
```

The control plane rejects transactions it cannot inspect. In this phase, every
payload elevated on-chain via `script: ["elevate|<payload-hash>"]` must be
present in the hydrated payload array, and `network.xyo.transfer` is prohibited
as an elevated/on-chain payload.

### Local development

```sh
ariesi signing-pool dev up      # Start the local signing-pool control plane
ariesi signing-pool dev down    # Stop it
ariesi signing-pool dev status  # Report whether it is running and healthy
ariesi signing-pool dev logs    # Print recent server log output
ariesi signing-pool dev reset   # Stop the server and wipe local state
```

#### `ariesi signing-pool dev up`
| Option | Default | Description |
|---|---|---|
| `--port <n>` | `8790` | Control-plane port |
| `--timeout <s>` | `10` | Seconds to wait for health |
| `--persist` | `false` | Persist pool state under `<ARIES_HOME>/signing-pool-dev/store` |
| `--audience <aud>` | `aries-signing-pool` | JWT audience accepted by the dev control plane |
| `--master-mnemonic <phrase>` | generated | Override the dev mnemonic used to derive pool signing accounts |
| `--xl1-node-rpc-url <url>` | `$XL1_NODE_RPC_URL` | XL1 node RPC URL used to read current block height |

The dev client auto-discovers a live local signing-pool state when
`--base-url`, `$SIGNING_POOL_CONTROL_URL`, and `$TOKEN_POOL_CONTROL_URL` are not
set.

### Upstream builder follow-up

The signing pool currently uses a narrow local helper to fill a single
`$signatures[]` slot without mutating the rest of the bound witness. The proper
upstream fix belongs in `sdk-protocol-js`: add explicit partial-signing support
to `BoundWitnessBuilder`, then expose that through `TransactionBuilder` so a
caller can build/sign slot 1 and a signing pool can later sign slot 0 while
preserving the hydrated transaction tuple and all existing signatures.

---

## `ariesi hash [file]` — content hashing

```text
ariesi hash [file] [options]
ariesi hash compare <hashA> <hashB> --algorithm <name> [--json]
```

Reads input from `[file]` (positional), `--string`, or piped stdin (priority order). Dispatches to one of 10 algorithms.

### Options

| Option | Default | Description |
|---|---|---|
| `-a, --algorithm <name>` | `sha256` | Algorithm — see the matrix below |
| `-s, --string <text>` | | Hash this string instead of a file/stdin |
| `-x, --xyo` | `false` | Shortcut for `--algorithm xyo` (input must be JSON) |
| `--base64` | `false` | Re-encode hex output as base64 (only for hex-output algorithms) |
| `--fps <n>` | `1` | Frames-per-second sample rate (`video-frames` only) |
| `--frame-algorithm <phash\|pdq>` | `phash` | Per-frame hash for `video-frames` |

### Algorithm reference

| Algorithm | Family | Input | Output | External binary | Compare metric |
|---|---|---|---|---|---|
| `sha256` | strict | bytes | 64-char hex | — | `exact-match` |
| `xyo` | strict | JSON | 64-char hex | — | `exact-match` |
| `nilsimsa` | fuzzy-byte | bytes | 64-char hex | — | `nilsimsa-score` |
| `ssdeep` | fuzzy-byte | bytes | `bs:h1:h2` | `ssdeep` | `ssdeep-levenshtein` ⚠ |
| `phash` | perceptual-image | image | 16-char hex (64-bit) | — | `hamming-64` |
| `dhash` | perceptual-image | image | 16-char hex (64-bit) | — | `hamming-64` |
| `blockhash` | perceptual-image | image | 16-char hex (64-bit) | — | `hamming-64` |
| `pdq` | perceptual-image | image | 64-char hex (256-bit) — Meta PDQ via WASM | — | `hamming-256` |
| `chromaprint` | audio | audio file | JSON `{duration, fingerprint[]}` | `fpcalc` | `chromaprint-windowed-hamming` |
| `video-frames` | video | video file | JSON `{fps, frames:[{t,hash}]}` | `ffmpeg` | `frame-sequence-<algo>@<fps>fps` |

⚠ — Approximations, not the canonical reference scores:
- `ssdeep-levenshtein` — block-size-aware Levenshtein, without the canonical block-size attenuation curve.

### External binary install

```sh
# macOS
brew install ssdeep chromaprint ffmpeg

# Debian/Ubuntu
apt install ssdeep libchromaprint-tools ffmpeg

# Windows
choco install ssdeep chromaprint ffmpeg
```

Each algorithm probes its binary at hash time and prints clear install instructions if the binary is missing.

### Examples

```sh
# Strict
ariesi hash --string "hello"
ariesi hash ./payload.json --algorithm xyo

# Byte-fuzzy
ariesi hash --algorithm nilsimsa ./binary.exe
ariesi hash compare <hashA> <hashB> --algorithm nilsimsa

# Perceptual image
ariesi hash --algorithm phash ./photo.jpg
ariesi hash --algorithm pdq   ./photo.jpg
ariesi hash compare <hashA> <hashB> --algorithm pdq

# Audio
ariesi hash --algorithm chromaprint ./song.mp3 > song.fp.json
ariesi hash compare "$(cat a.fp.json)" "$(cat b.fp.json)" --algorithm chromaprint --json

# Video
ariesi hash --algorithm video-frames --fps 1 ./movie.mkv > movie.fp.json
ariesi hash --algorithm video-frames --fps 1 --frame-algorithm pdq ./movie.mkv > movie-pdq.fp.json
ariesi hash compare "$(cat a.fp.json)" "$(cat b.fp.json)" --algorithm video-frames
```

### `ariesi hash compare <hashA> <hashB>`

| Positional | | Description |
|---|---|---|
| `hashA` | required | First hash |
| `hashB` | required | Second hash |

| Option | Default | Description |
|---|---|---|
| `-a, --algorithm <name>` | required | Algorithm that produced the hashes |
| `--json` | `false` | Print full result as JSON |

Plain output: `<similarity>\t<metric>[\tdistance=<n>]` (similarity is a 0..1 float, 1 = identical). JSON output: `{"similarity":0.93,"distance":18,"metric":"hamming-256"}`.

The compare command needs only the two hash strings — works against hashes generated on different machines.

---

## `ariesi npmjs` — npmjs org package checks

```sh
ariesi npmjs list --org=xylabs
ariesi npmjs lint --org=xylabs
ariesi npmjs lint --org=xylabs --fix
ariesi npmjs lint              # all auth-visible org packages
```

By default, package discovery uses your current npm CLI authentication via `npm access list packages @<org> --json`, so private or restricted org packages are included when your npm account can see them. Omit `--org` to list or lint all scoped packages returned by `npm access list packages --json`, grouped by org with a per-org summary at the end. If npm auth is unavailable for a specific `--org`, `ariesi npmjs` falls back to public package discovery and reports `npm auth: unauthenticated` at the end of text output. Use `--public-only` with `--org` to force public discovery.

### `ariesi npmjs list`

| Option | Default | Description |
|---|---|---|
| `--org <scope>` | | npm org scope, with or without `@`; omit for all auth-visible org packages |
| `--public-only` | `false` | List only public packages without using `npm access` |
| `--json` | `false` | Output as JSON |

### `ariesi npmjs lint`

Find packages that are not marked deprecated but probably should be. V1 reports warning-only findings and exits `0` unless an operational error occurs.

| Option | Default | Description |
|---|---|---|
| `--org <scope>` | | npm org scope, with or without `@`; omit for all auth-visible org packages |
| `--public-only` | `false` | Lint only public packages without using `npm access` |
| `--abandoned-days <n>` | `365` | Warn when latest publish is at least this many days old |
| `--unused-downloads <n>` | `100` | Warn when weekly downloads are below this threshold |
| `--new-days <n>` | `30` | Do not warn for low downloads until a package is at least this many days old |
| `--fix` | `false` | Interactively choose lint findings to deprecate with `npm deprecate` |
| `--deprecation-message <text>` | | Message to pass to `npm deprecate` for selected packages |
| `--json` | `false` | Output as JSON |

Rules:
- `abandoned` — latest publish date is older than the configured threshold.
- `unused` — weekly downloads are below the configured threshold and the package is not new.

`--fix` is text-only and prompts with a default `Do Nothing` / optional `Deprecate` toggle for each package reported by lint. After selection, each selected package prompts for an optional replacement package name. Blank keeps the default message; a replacement appends `Use <replacement> instead.` Selected packages are deprecated with `npm deprecate <package>@* <message>` after Enter is pressed.

---

## `aries info`

Display ariestools version and environment info. No command-specific options.

---

## `aries wallet` — XL1 wallet

Wallet commands are provided by the standalone `@xyo-network/wallet-xl1-cli` package (`xl1-wallet`). The Aries CLI forwards `aries wallet ...` to that bin and uses the wallet CLI storage layout: `~/.xl1/wallet/cli` by default, overrideable with `XL1_WALLET_HOME` (`ARIES_WALLET_HOME` remains a legacy fallback).

### Wallet management

```sh
aries wallet create               # Generate a new XL1 wallet (random recovery phrase)
aries wallet import               # Import an existing XL1 recovery phrase
aries wallet export               # Print the recovery phrase for the active (or specified) wallet
aries wallet list                 # List stored wallets
aries wallet use <id>             # Set the active wallet
aries wallet rename <id> <label>  # Rename a stored wallet
aries wallet remove <id>          # Delete a stored wallet (and its address book)
```

#### `aries wallet create`
| Option | Default | Description |
|---|---|---|
| `-l, --label <text>` | first available `wallet#` | Friendly label for the wallet |
| `--algorithm <secp256k1\|ml-dsa-65>` | `secp256k1` | Wallet signing algorithm |

#### `aries wallet import`
| Option | Default | Description |
|---|---|---|
| `-l, --label <text>` | `imported` | Friendly label |
| `-p, --phrase <words>` | | Recovery phrase (skips interactive prompt) |
| `--algorithm <secp256k1\|ml-dsa-65>` | `secp256k1` | Wallet signing algorithm |

`ml-dsa-65` wallets use `QuantHDWallet` and derive `qm65...` bech32m addresses. Wallet-JWT commands and datalake `--wallet` mode require secp256k1 until ML-DSA-65 has a standardized JOSE algorithm.
Wallet labels must be unique. If `create` is run without `--label`, the CLI uses the first available `wallet#` label, starting with `wallet0`.

#### `aries wallet export`
| Option | Default | Description |
|---|---|---|
| `--id <wallet-id>` | active | Wallet id or label |

#### `aries wallet use <id>` / `aries wallet remove <id>`
| Positional | | Description |
|---|---|---|
| `id` | required | Wallet id or label |

#### `aries wallet rename <id> <label>`
| Positional | | Description |
|---|---|---|
| `id` | required | Wallet id or current label |
| `label` | required | New label |

### Session

```sh
aries wallet unlock                   # Cache the wallet password (encrypted at rest) for a TTL
aries wallet lock                     # Clear the cached unlocked session
aries wallet password change          # Change the wallet password (re-encrypts every stored phrase)
```

#### `aries wallet unlock`
| Option | Default | Description |
|---|---|---|
| `--ttl <seconds>` | `900` | Session lifetime in seconds (default 15 min) |

`lock` and `password change` take no command-specific options.

### Reset (lost password recovery)

```sh
aries wallet reset                    # Destroy all wallet data — only path forward when password is lost
```

Use this only when you have forgotten your wallet password. There is no other way to recover from a lost password — encrypted seed phrases cannot be decrypted without it.

`reset` deletes the entire wallet directory (`~/.xl1/wallet/cli` by default): every stored seed phrase, every derived account, every address-book entry, every saved contact, every saved network configuration, and the active session. **This is irreversible unless you have a backup** (see [`aries wallet backup`](#backup)).

To prevent accidental loss, the command requires two interactive confirmations:
1. Type the literal word `RESET` (uppercase, exactly).
2. Answer `y` to the final `[y/N]` prompt.

Anything else cancels with no changes. There is no `--force` flag.

If no wallet has been initialized (e.g. on first run), `reset` is a no-op and prints `No wallet found — no reset needed.`

### Accounts

```sh
aries wallet account list                    # List derived accounts in the active wallet
aries wallet account show <offset>           # Show details for a derived account
aries wallet account derive <offset>         # Derive a new account at the given HD offset
aries wallet account label <offset> <label>  # Label a derived account
aries wallet account remove <offset>         # Remove an account from the address book
```

All `account` commands take an HD path offset positional (e.g. `0`).

#### `aries wallet account derive <offset>`
| Option | Default | Description |
|---|---|---|
| `-l, --label <text>` | | Friendly label for the account |

### Balance & signing

#### `aries wallet balance [offset]`
| Positional | | Description |
|---|---|---|
| `offset` | `0` | HD path offset |

#### `aries wallet sign <file>`
| Positional | | Description |
|---|---|---|
| `file` | required | Path to a JSON payload file |

| Option | Default | Description |
|---|---|---|
| `-o, --offset <n>` | `0` | HD path offset of the signer |
| `-O, --output <path>` | | Write output to file instead of stdout |

Produces a BoundWitness.

### Self-signed JWT

A wallet account can mint a JWT-shaped, time-bounded permission token that a site verifies locally, without running its own login flow. Think of it as reverse OAuth: the holder signs an attestation declaring an audience (the domain they intend to use the token at) and an expiration; the site checks the signature and claims and grants the session.

The token is the standard `header.payload.signature` JWT format, base64url-encoded. The signing input is `SHA-256(utf8("${headerB64}.${payloadB64}"))` and the alg is `ES256K` (secp256k1 ECDSA). The header carries both `kid` (signer address) and `pub` (full public key) so verification is self-contained — no out-of-band pubkey lookup is needed.

**Header** fields: `alg` (`ES256K`), `typ` (`JWT`), `kid` (40-char lowercase hex address), `pub` (hex public key bytes).

**Payload** is an XYO Payload (must include `schema`). The default schema is `network.xyo.auth.signin` and the standard claims are `iss` (mirrors `kid`), `aud`, `iat`, `exp`, plus optional `nbf`, `nonce`, and any extra fields supplied via `--payload-file` or `--claim`.

> Note: this is the **single-signed** path. A future co-signed BoundWitness flow (where the site offers a permissions BW signed by its own address and the client co-signs) is planned and will reuse the same schema and account-derivation infrastructure.

#### `aries wallet jwt create`
| Option | Default | Description |
|---|---|---|
| `-a, --audience <domain>` | required | Audience — becomes the `aud` claim (e.g. `xyo.network`) |
| `--ttl <seconds>` | `3600` | Lifetime in seconds (mutually exclusive with `--exp`) |
| `--exp <unix-seconds>` | | Absolute expiration; alternative to `--ttl` |
| `--schema <schema>` | `network.xyo.auth.signin` | XYO schema for the payload |
| `--payload-file <path>` | | JSON file with extra payload fields (CLI flags win) |
| `--claim <key=value>` | | Extra claim, repeatable; numeric/boolean strings are coerced |
| `-o, --offset <n>` | `0` | HD path offset of the signer |
| `-O, --output <path>` | | Write the token to a file instead of stdout |

#### `aries wallet jwt verify [token]`
| Positional | | Description |
|---|---|---|
| `token` | | JWT to verify (omit to read from `--input` or stdin) |

| Option | Default | Description |
|---|---|---|
| `-a, --audience <domain>` | | If provided, fail unless `aud` matches |
| `-i, --input <path>` | | Read JWT from file |
| `--now <unix-seconds>` | | Override current time — useful for tests |

Prints `{ ok, header, payload, reasons? }` JSON. Exits non-zero on any verification failure (signature, expiry, audience mismatch, kid/iss mismatch, public-key/address mismatch).

#### `aries wallet jwt decode [token]`
Decode header and payload without verifying the signature. Useful for debugging.

### Transactions

```sh
aries wallet send <recipient> <amount>      # Send XL1 to a recipient address
aries wallet tx sign <file>                 # Sign an unsigned transaction JSON file
aries wallet tx broadcast <file>            # Broadcast a signed transaction JSON file
```

#### `aries wallet send <recipient> <amount>`
| Positional | | Description |
|---|---|---|
| `recipient` | required | Recipient address (`0x…`) |
| `amount` | required | Amount in the selected unit (XL1 by default) |

| Option | Default | Description |
|---|---|---|
| `-o, --offset <n>` | `0` | HD path offset of the sender |
| `--xl1` | | Interpret amount as XL1 (default) |
| `--milli` | | Interpret amount as milli XL1 |
| `--micro` | | Interpret amount as micro XL1 |
| `--nano` | | Interpret amount as nano XL1 |
| `--pico` | | Interpret amount as pico XL1 |
| `--femto` | | Interpret amount as femto XL1 |
| `--atto` | | Interpret amount as atto XL1; must be a whole number |
| `--dry-run` | `false` | Build and sign the transfer without submitting it |
| `--json` | `false` | Emit machine-readable JSON output |
| `--attempts <n>` | `30` | Confirmation polling attempts |
| `--delay <ms>` | `2000` | Delay between confirmation attempts |

#### `aries wallet tx sign <file>`
| Positional | | Description |
|---|---|---|
| `file` | required | Path to unsigned transaction JSON |

| Option | Default | Description |
|---|---|---|
| `-o, --offset <n>` | `0` | HD path offset of the signer |
| `-O, --output <path>` | | Write signed tx to file instead of stdout |

#### `aries wallet tx broadcast <file>`
| Positional | | Description |
|---|---|---|
| `file` | required | Path to signed transaction JSON |

| Option | Default | Description |
|---|---|---|
| `-o, --offset <n>` | `0` | HD path offset for client connection |

### Networks

```sh
aries wallet network list                # List configured networks
aries wallet network add <id> <rpcUrl>   # Add a custom network
aries wallet network use <id>            # Set the active network
aries wallet network remove <id>         # Remove a custom network
```

#### `aries wallet network add <id> <rpcUrl>`
| Positional | | Description |
|---|---|---|
| `id` | required | Network id (unique) |
| `rpcUrl` | required | Gateway RPC URL |

| Option | Default | Description |
|---|---|---|
| `-l, --label <text>` | | Friendly label |
| `--chain-id <hex>` | | Chain id (hex) |

`use` and `remove` take a network `id` positional only.

### Contacts

```sh
aries wallet contact list                       # List address-book contacts
aries wallet contact add <address> <label>      # Add or update a contact
aries wallet contact rename <address> <label>   # Rename an existing contact
aries wallet contact remove <address>           # Remove a contact
```

All take an `address` positional (`0x…`); `add`/`rename` also take a `label` positional. No additional flags.

### Backup

```sh
aries wallet backup export <file>   # Export address books and contacts to a JSON file
aries wallet backup import <file>   # Import a wallet backup JSON file (merges into current state)
```

`file` is the only positional; no additional flags.

---

## `aries witness` — Observe data and produce XYO-compliant payloads

```sh
aries witness timestamp     # Capture the current timestamp
aries witness system-info   # Capture system information (OS, CPU, memory, etc.)
aries witness url           # Capture metadata and content from a URL
aries witness bank          # Capture bank account snapshots or Capital One product catalog data
aries witness pentair       # Capture Pentair ScreenLogic pool equipment state
aries witness pentair-intellichem # Capture and graph Pentair IntelliChem chemistry data
aries witness pentair-schedules   # Capture Pentair ScreenLogic schedules with circuit and pump context
aries witness app           # Inspect installed applications
aries witness app list      # List installed macOS applications
```

### Common witness flags

These flags are available on every `witness` leaf:

| Option | Default | Description |
|---|---|---|
| `--json` | `false` | Output raw JSON |
| `-o, --output <path>` | | Write output to file |
| `--pretty` | `true` | Pretty-print JSON output |
| `--bound-witness` | `false` | Wrap payloads in a BoundWitness |
| `--binary-dir <path>` | `~/.aries/binaries/` | Directory for binary artifacts |

### Per-command additions

#### `aries witness url`
| Option | Default | Description |
|---|---|---|
| `--url <url>` | required | The URL to fetch |

#### `aries witness bank`

```sh
aries witness bank --provider capitalone --account-id acct-123 --from 2026-06-01 --to 2026-06-25 --json
aries witness bank --provider capitalone --access-token "$CAPITALONE_TOKEN" --include-raw --json
aries witness bank --provider capitalone --resource products --client-id "$CAPITALONE_CLIENT_ID" --client-secret "$CAPITALONE_CLIENT_SECRET" --json
```

| Option | Default | Description |
|---|---|---|
| `--provider <capitalone>` | `capitalone` | Bank provider |
| `--resource <accounts\|products>` | `accounts` | `accounts` for private account APIs; `products` for Capital One's sandbox deposit product catalog |
| `--from <value>` | | Transaction window start passed as query param `from` |
| `--to <value>` | | Transaction window end passed as query param `to` |
| `--account-id <id>` | | Provider account id for balances and transactions |
| `--include-raw` | `false` | Store raw provider JSON as witness binary artifacts |
| `--redaction <default\|none>` | `default` | Hash provider account/transaction ids unless explicitly set to `none` |
| `--env <sandbox\|production>` | `bank config` | Environment label |
| `--base-url <url>` | `bank config` | Capital One API base URL |
| `--token-url <url>` | `bank config` | OAuth client-credentials token URL |
| `--access-token <token>` | `ARIES_CAPITALONE_ACCESS_TOKEN` | Pre-issued bearer token; not persisted |
| `--client-id <id>` | `bank config` | OAuth client id |
| `--client-secret <secret>` | `bank config` | OAuth client secret |
| `--scope <scope>` | `bank config` | Optional OAuth client-credentials scope |
| `--api-version <version>` | `5` | Capital One products media-type version |
| `--operation <retrieve-products\|retail-products\|search>` | `search` | Capital One products operation |
| `--product-id <id>` | `~` for product-id operations | Capital One path product id |
| `--body <json>` | `{"isCollapseRate":true}` for `search`; `{}` otherwise | JSON body for Capital One products POST operations |
| `--body-file <path>` | | JSON body file for Capital One products POST operations |
| `--accounts-path <path>` | `bank config` | Accounts endpoint path |
| `--balances-path <path>` | `bank config` | Balances endpoint path; may include `{accountId}` |
| `--transactions-path <path>` | `bank config` | Transactions endpoint path; may include `{accountId}` |

With `--resource accounts`, the bank witness returns one
`network.xyo.bank.account-snapshot` payload containing normalized accounts,
balances, and transactions. By default, provider account and transaction
identifiers are hashed.

With `--resource products`, the bank witness calls Capital One's documented
`Retrieve Consumer Bank Products` sandbox API and returns one
`network.xyo.bank.product-catalog` payload. This is product catalog data, not a
consumer account snapshot. The default product request follows Capital One's
published "Scenario 1a: All Products" example: `POST /deposits/products/~/search`
with `{"isCollapseRate": true}`.

If Capital One returns `{"id":"200008","text":"no endpoint matched for request"}`
from the product request after OAuth succeeds, the client credentials are valid
but Capital One's sandbox gateway did not match the product endpoint. Verify the
app is connected to DevExchange product `1359-5` in the sandbox and confirm the
active sandbox base path with Capital One.

#### `aries witness pentair-intellichem`
| Option | Default | Description |
|---|---|---|
| `--history` | `false` | Fetch IntelliChem controller history |
| `--from <date\|age>` | `24h` | History start as ISO date or relative age like `30m`, `6h`, `7d` |
| `--to <date\|age>` | now | History end as ISO date or relative age |
| `--save` | `false` | Save normalized samples to the JSONL series file |
| `--series-file <path>` | `pentair config` or `~/.aries/pentair/intellichem.jsonl` | JSONL file used for saved samples and graphing |
| `--graph <path>` | | Write a standalone HTML graph |
| `--graph-only` | `false` | Render a graph from the series file without connecting to ScreenLogic |
| `--address <ip>` | `pentair config` | Connect directly to a ScreenLogic adapter IP instead of UDP discovery |
| `--port <number>` | `pentair config` or `80` | ScreenLogic adapter TCP port for direct connection |
| `--password <password>` | `pentair config` or `SCREENLOGIC_PASSWORD` | ScreenLogic password for direct connection, if configured |
| `--system-name <name>` | `pentair config` or `Pentair ScreenLogic` | ScreenLogic system name for direct connection |
| `--response-timeout <ms>` | `pentair config` or `15000` | Milliseconds to wait for each ScreenLogic command response |
| `--search-timeout <ms>` | `pentair config` or `5000` | Milliseconds to wait for each UDP discovery attempt |
| `--search-attempts <count>` | `pentair config` or `3` | Number of UDP discovery attempts before failing |

Examples:

```sh
aries witness pentair-intellichem --save
aries witness pentair-intellichem --history --from 7d --save --graph intellichem.html
aries witness pentair-intellichem --graph-only --graph intellichem.html
```

#### `aries witness pentair-schedules`
| Option | Default | Description |
|---|---|---|
| `--address <ip>` | `pentair config` | Connect directly to a ScreenLogic adapter IP instead of UDP discovery |
| `--port <number>` | `pentair config` or `80` | ScreenLogic adapter TCP port for direct connection |
| `--password <password>` | `pentair config` or `SCREENLOGIC_PASSWORD` | ScreenLogic password for direct connection, if configured |
| `--system-name <name>` | `pentair config` or `Pentair ScreenLogic` | ScreenLogic system name for direct connection |
| `--response-timeout <ms>` | `pentair config` or `15000` | Milliseconds to wait for each ScreenLogic command response |
| `--search-timeout <ms>` | `pentair config` or `5000` | Milliseconds to wait for each UDP discovery attempt |
| `--search-attempts <count>` | `pentair config` or `3` | Number of UDP discovery attempts before failing |

Example:

```sh
aries witness pentair-schedules --json
```

#### `aries witness app list`
| Option | Default | Description |
|---|---|---|
| `--scope <all\|global\|system\|user>` | `all` | Filter by install scope |

`witness timestamp`, `witness system-info`, and `witness pentair` take only the common flags.

---

## `aries xyo` — XYO protocol utilities

### `aries xyo bridge <mainnet|sequence> status`

Summarize the native XL1 ⇄ ERC-20 XL1 bridge: bridgings in each direction,
unique bridgers, volume both ways, and the liquidity of the bridge wallets.

```sh
aries xyo bridge mainnet status
aries xyo bridge sequence status --json
aries xyo bridge mainnet status --evm-rpc-url https://my-provider.example/<key>
```

Activity comes from the bridge contract on Ethereum (Sepolia for Sequence),
which records every bridging in both directions. Every contract read is pinned
to one block at the bridge service's confirmation depth, so counts, amounts,
and balances agree. Volume is the ERC-20 amount the contract recorded: payouts
after fees for native → ERC-20, deposits before fees for ERC-20 → native.

Liquidity covers the ERC-20 liquidity source (balance and allowance to the
bridge contract), the native XL1 escrow and fee wallets published by the
bridge service API, and the ETH balance of the contract owner that pays for
payouts. The command also flags a paused or retired contract, liquidity below
the maximum single bridging, and an allowance below the liquidity balance.

| Option | Environment | Default (Mainnet) |
| --- | --- | --- |
| `--evm-rpc-url` | `ARIES_BRIDGE_MAINNET_EVM_RPC_URL` | `https://ethereum-rpc.publicnode.com` |
| `--xl1-rpc-url` | `ARIES_BRIDGE_MAINNET_XL1_RPC_URL` | `https://api.chain.xyo.network/rpc` |
| `--bridge-api-url` | `ARIES_BRIDGE_MAINNET_API_URL` | `https://api.bridge.chain.xyo.network` |

Sequence uses the `ARIES_BRIDGE_SEQUENCE_*` variables. Output reports only each
endpoint's origin, so a provider key in a URL path is never printed. If the
bridge API or XL1 RPC fails, the command still reports activity and marks the
result `partial`; if the EVM contract cannot be read, it exits non-zero.
Finalized records are cached in `~/.aries/xyo-bridge/<network>/records-v1.json`
so later runs read only new bridgings; `--no-cache` re-reads every record.

### `aries xyo rewards <sequence|mainnet> <view>`

Query finalized XL1 **reward lifecycle** activity (pool funding → step holders →
escrows → redemptions) through the SDK REST gateway and its multi-family block
rollup indexes (`blocks/*`).

```sh
aries xyo rewards sequence summary
aries xyo rewards mainnet steps --from-block 500000
aries xyo rewards sequence recipients --stage redeemed --limit 25
aries xyo rewards mainnet recipients --stage supplemental-funding --limit 25
aries xyo rewards sequence address <address> --json
aries xyo rewards sequence summary --from-block 510000 --explain
aries xyo rewards sequence index status
aries xyo rewards sequence index verify --concurrency 8
```

| View | Description |
|---|---|
| `summary` | Direct and supplemental funding, allocation, payout, actual step-holder balance, unique-recipient, and reward-step totals |
| `steps` | Per-step direct funding, supplemental funding, and allocation; optionally filter with `--step-level` |
| `recipients` | Rank recipients at the `funded`, `supplemental-funding`, `allocated`, or `redeemed` stage |
| `address <address>` | Reward lifecycle events involving an escrow, staker, or destination |
| `index status` | Show the durable **network-stake** rewards-index head and its XL1/EVM source-watermark lag |
| `index verify` | Recompute cumulative **network-stake** totals from every immutable reward-step record and verify the snapshot and source hashes |

**Two different “rewards indexes”.** Do not confuse them:

| Index | Path / command | What it is for | Used by `summary` / `steps` / `recipients` / `address`? |
|---|---|---|---|
| Multi-family **blocks** index | `indexes.*.xyo.space` `blocks/{level}/{i}.json` | Bulk block frames for history walks | **Yes** (via `blocksByStep`) |
| Local **lifecycle cache** | `~/.aries/xyo-rewards/<net>/lifecycle/v1/` | Client-side cumulative summary resume | **Yes** (`summary` only, from-block 0) |
| Durable **network-stake** rewards index | `rewards/network-stake/v1/{policy}/…` + `xyo rewards … index` | Stake-reward **allocation** math (XL1 ∩ EVM watermarks) | **No** — backfilling or verifying it does **not** speed lifecycle summary |

| Common option | Default | Description |
|---|---|---|
| `--from-block <n>` | `0` | First finalized block to include (non-zero → range-delta lifecycle nets) |
| `--to-block <n>` | finalized head | Last finalized block to include |
| `--recent-blocks <n>` | | Scan only the last N blocks (derived from `--to-block`/head; not with `--from-block`) |
| `--skip-balance` | `false` | Skip the step-holder balance batch (faster incremental scans; omits outstanding balance) |
| `--lifecycle-cache` | `true` | Summary only: reuse/extend local cumulative cache under `~/.aries/xyo-rewards/<network>/lifecycle/v1/` |
| `--refresh-lifecycle-cache` | `false` | Summary only: force a full cumulative rescan and rewrite the local cache |
| `--concurrency <n>` | `8` | Indexed ranges read concurrently, from 1 to 16 |
| `--json` | `false` | Stable machine-readable output; XL1 amounts include atto and decimal forms |
| `--explain` | `false` | Show indexed frame use, direct reads, gateway setup/scan/balance requests, bytes, timing, and SDK budget warnings |
| `--no-progress` | | Suppress the default gateway and scan progress written to stderr |

**Incremental / recent windows.** Full-history mainnet scans are expensive because every
finalized block’s transfers are decoded. Prefer a bounded window when you only need recent
activity:

```sh
aries xyo rewards mainnet summary --recent-blocks 50000 --skip-balance
aries xyo rewards mainnet summary --from-block 900000 --to-block 950000 --skip-balance
```

With a non-zero start, funding/allocation/escrow nets are **range deltas**. Step-holder
balance (unless `--skip-balance`) remains the actual balance at `--to-block`.

**Local lifecycle cache (summary).** Cumulative `summary` runs (`--from-block 0`, no
`--recent-blocks`) persist accumulator state under
`~/.aries/xyo-rewards/<network>/lifecycle/v1/head.json`. The next run verifies the
cached block hash, hydrates unique sets and totals, and **only live-scans the tip**
above the cache watermark. Use `--no-lifecycle-cache` to disable, or
`--refresh-lifecycle-cache` to rebuild. Cache status appears in `--explain` and
stderr progress. This is a client-side acceleration cache — not the durable
network-stake rewards index under `rewards/network-stake/...`.

The initial **network-stake** rewards-index backfill is an internal S3 mutation and
is intentionally excluded from the public `aries` binary. It builds stake-allocation
snapshots under `rewards/network-stake/...` and does **not** feed or accelerate
`summary` / `steps` / `recipients` / `address`. Operators run it through `ariesi`
only while the automatic indexer writer is stopped:

```sh
ariesi xyo rewards sequence index backfill --dry-run
ariesi xyo rewards sequence index backfill \
  --bucket sequence-index \
  --account-id "$CF_ACCOUNT" \
  --confirm-writer-stopped
aries xyo rewards sequence index verify
```

`backfill` resumes from the published rewards head, advances only through the
intersection of the durable XL1 index watermark and finalized EVM event-index
watermark, and verifies the resulting immutable steps and snapshot directly from
S3 before reporting success. `--confirm-writer-stopped` confirms that the
automatic writer is stopped and authorizes writes; `--dry-run` requires no S3
credentials.

The reward lifecycle distinguishes direct funding (reward pool to step holder)
from supplemental funding (any other inbound transfer to a deterministic step
holder), followed by allocation (step holder to reward escrow) and redemption
(reward escrow to a final wallet). “Total Rewards Funded” is direct plus
supplemental funding. “Total Rewards Paid Out” means redeemed rewards. Exact
staker identity is visible when an escrow redeems, so
`uniqueObservedStakers` counts stakers seen in redemption context; it is not an
allocation-time staker count.

“Outstanding in Step Holders” is the actual sum of deterministic holder account
balances at `--to-block`, read in one SDK batch against the finalized range.
The separately reported step-holder lifecycle net is direct plus supplemental
funding, minus allocation and rounding returns. With a later `--from-block`,
that lifecycle value and the reward-escrow value are labeled range deltas,
while the step-holder balance remains the actual balance at `--to-block`.

`--explain` is intentionally part of the product rather than only a debugging
flag. It separates gateway startup work from the reward scan and makes direct
block fallbacks, payload volume, and repeated request patterns visible. It also
reports **blocks-index lag** (multi-family `blocks/*` watermark vs chain head):
when the blocks index trails the tip, the unfinished range falls back to
per-block GETs and full-history scans slow down sharply. Check overall index
health with `ariesi xyo s3 index monitor mainnet`. Those measurements also
identify reward-specific lifecycle indexes worth publishing through the XL1
indexing pipeline as full-history use grows.

Human-readable commands print gateway initialization and bounded scan progress
to stderr by default. JSON commands remain quiet so stdout stays
machine-readable; SDK diagnostics and the completion footer also stay on
stderr. Add `--verbose` to a JSON command to opt into progress.

Every `aries` and `ariesi` invocation writes a status-aware timing footer to
stderr after the command finishes: `✅ Succeeded in <milliseconds>ms.` when it
exits 0, and `❌ Failed in <milliseconds>ms.` for any other exit status. A
failed exit includes a usage error, a failed command, and a human-only
`secrets` refusal, where the footer follows the fixed message. This includes
JSON commands without changing their machine-readable stdout. `--help` and
`--version` print no footer.

### `aries xyo tx validate [data]`

Validate one or more XL1 transactions. Reads from a file path, inline JSON, or stdin.

| Positional | | Description |
|---|---|---|
| `data` | | Path to a `.json`/`.jsonl` file, or an inline transaction JSON tuple `[bw, payloads[]]`. Omit to read from stdin. |

| Option | Default | Description |
|---|---|---|
| `-d, --detail` | `false` | Print per-transaction error details (always shown for invalid transactions) |

**Input modes** (resolved in priority order):

| Mode | Example |
|---|---|
| File path | `aries xyo tx validate ./tx.json` |
| File path (JSONL batch) | `aries xyo tx validate ./txs.jsonl` |
| Inline JSON | `aries xyo tx validate '[[...], [...]]'` |
| Piped stdin | `cat txs.jsonl \| aries xyo tx validate` |

**Supported file formats:**
- `.json` — a single transaction tuple `[boundWitness, payloads[]]`, or a JSON array of tuples
- `.jsonl` / `.ndjson` — one transaction tuple per line

**Output:**

```text
Total:   3
Valid:   2
Invalid: 1

[2] ./txs.jsonl:3
  - TransactionGasValidator: insufficient gas
```

**Exit codes:** `0` = all valid, `1` = any invalid or parse error.

---

## Configuration

Aries configuration lives under `~/.aries/` (or `$ARIES_HOME` if set). Wallet data lives under `~/.xl1/wallet/cli` (or `$XL1_WALLET_HOME` if set) and is encrypted at rest using your wallet password.

---

## License

LGPL-3.0-only © XY Labs

## Credits

[Made with 🔥 and ❄️ by AriesTools](https://arietrouw.com)
