@capxul/cli
v4.20.0-beta.7
Published
Capxul CLI diagnostics and foundations.
Readme
Capxul CLI
The CLI provides email OTP login, persistent first-party sessions, terminal-owned signup, personal and Organization contacts, a bundled Openfort wallet page, backend profile and account reads, diagnostics, and one global collection preference. It requires Node 24 or later on macOS or Linux.
capxul --help
capxul --version
capxul --completions bash
capxul doctor --json
capxul telemetry status --json
capxul telemetry disable --json
capxul telemetry enable --json
capxul doctor --online --timeout-ms 30000 --jsonHelp and version do not require application credentials or a working backend.
With no arguments, capxul shows the same generated help as capxul --help.
They use the same observation policy as ordinary commands. Shell completion
machinery and collection controls send no observation records.
doctor checks local configuration unless --online is present. An online
check uses the public Capxul SDK and verifies the backend response nonce.
It does not authenticate a person or submit a transaction.
Contact commands
capxul contact list [--include-hidden]
capxul contact get --entry-id PARTY_ID
capxul contact add --input FILE|- [--confirm]
capxul contact label --entry-id PARTY_ID --label TEXT [--confirm]
capxul contact hide --entry-id PARTY_ID [--confirm]
capxul contact unhide --entry-id PARTY_ID [--confirm]
capxul org contact list --org ORGANIZATION_ID [--include-hidden]
capxul org contact get --org ORGANIZATION_ID --entry-id PARTY_ID
capxul org contact add --org ORGANIZATION_ID --input FILE|- [--confirm]
capxul org contact label --org ORGANIZATION_ID --entry-id PARTY_ID --label TEXT [--confirm]
capxul org contact hide --org ORGANIZATION_ID --entry-id PARTY_ID [--confirm]
capxul org contact unhide --org ORGANIZATION_ID --entry-id PARTY_ID [--confirm]Personal commands use the authenticated Account address book. Organization
commands require one explicit --org or --org-id; they never use the saved
Organization read default. list omits hidden entries unless
--include-hidden is present. get, label, hide, and unhide address one
stable Party with --entry-id.
add --input reads the existing AddressBookAddInput JSON shape. For example:
{
"ref": { "kind": "email", "email": "[email protected]" },
"label": "New Supplier"
}Use --input - to read the same object from stdin. An interactive add can
collect the reference kind, value, and optional label instead. Interactive
writes show the actor and exact contact change, then ask a default-no question.
JSON, CI, and other noninteractive writes require --confirm before the CLI
creates a client. Contact writes do not start a browser signer.
Success data is the SDK value without a CLI wrapper: list returns
AddressBookEntry[], get returns AddressBookEntry | null, and each mutation
returns AddressBookEntry. Human get prints No contact found for null and
exits successfully. Entries retain their stable PartyId, exact reference,
relationships, hidden state, and last activity time.
Organization commands
capxul org list --json
capxul org use --org ORGANIZATION_ID
capxul org status --org ORGANIZATION_ID --json
capxul org get --json
capxul org me --json
capxul org members --json
capxul org member list --json
capxul org member get --account-id ACCOUNT_ID --json
capxul org wait --timeout-seconds 120 --json
capxul org create --name NAME --handle HANDLE --country CC [--bio TEXT] [--size TEXT] [--confirm]
capxul org retry --org ORGANIZATION_ID [--confirm]These commands accept --email. Without it, they restore the protected current
session. Organization reads accept --org or its alias --org-id. Identical
repeated IDs are accepted. Different IDs are refused.
org use requires an explicit Organization ID. It checks current access and
saves a protected per-person read default. The default never grants authority.
Each read checks current access again. Without an explicit ID or saved default,
interactive mode asks you to select an Organization. JSON, CI, and noninteractive
mode refuse missing scope. No command starts a signer or changes backend state.
org get reports the canonical command org.status. Both member list forms
report org.members and retain pending members with accountId: null.
org member get matches the full AccountId, not a Safe address or ID prefix.
No match returns exit 2. Duplicate matches return exit 1.
org wait reads the same lifecycle every second until it reports ready or
failed. It never retries setup. The timeout accepts integers from 1 to 3600
seconds and defaults to 120. Timeout returns exit 5; interruption returns 130.
A successful read can report a failed or pending domain state.
Invitation commands
capxul org invite list [--mine] [--limit N] [--cursor C] [--phase PHASE ...] [--org O]
capxul org invite get --invitation-id I [--org O]
capxul org invite wait --invitation-id I [--org O] [--timeout-seconds N]
capxul org invite review --invitation-id I --org O [--timeout-seconds N]
capxul org invite accept --invitation-id I --offer-digest D --org O [--confirm] [--timeout-seconds N]
capxul org invite decline --invitation-id I --org O [--confirm]
capxul org invite cancel --invitation-id I --org O [--confirm]
capxul org invite resend --invitation-id I --org O [--confirm]
capxul org invite retry --invitation-id I --org O [--confirm]
capxul org member invite (--to-email E | --account-id ID) [--budget-id B ...] [--permission-id M ...] [--new-budget-name N --asset A (--limit Q | --unlimited) (--recipient-account ID ... | --any-recipient) --actions pay[,commitments]] --org O [--request-key K] [--preview] [--confirm]org invite list returns your own offers. It accepts --limit (1--100),
--cursor, and repeated --phase values, and its result carries invitations,
nextCursor, and observedAt: pass nextCursor back as --cursor, and change
no other filter between the two calls. --mine names the own-offer projection,
which is already the default. With --org O the command returns that
Organization's invitations for a current Admin instead; the Organization list is
not filtered to you, so --mine together with --org refuses with exit 2. The
other invitation commands require an exact --invitation-id.
org invite get and org invite wait use --org when you name it. Without
--org they walk your own-offer pages, 100 rows per request, until they find
the invitation or the pages end. They never consult the saved read default, and
an offer you cannot see is refused. --timeout-seconds bounds the resolution
and the read together on both commands.
org invite wait only reads. It finishes when the offer settles or when the
recovery asks a person to act. It never retries, resends, or accepts.
org invite review requires --org, returns the current InvitationView
directly in data, and performs no write or signing.
The four transitions require --org in both modes, as accept does. They never
author a new offer. A noninteractive or JSON run requires --confirm. An
interactive run shows the resolved current offer and asks a default-no question.
org member invite authorizes one exact offer. --preview writes nothing and
needs neither a request key nor confirmation. A noninteractive write requires
--request-key and --confirm. An interactive run prints the request key it
used before it submits, so a lost response is recoverable with the same
identity. At most one --new-budget-name is accepted per command.
org invite accept is the exact grantee's consent commit. It takes the exact
digest the offer shows as --offer-digest: a noninteractive or JSON run must
supply it, and without it the command refuses with exit 2 before it creates a
client. An interactive run may omit it, and then reads the offer and fills the
digest from the value it displayed. A digest you state is never replaced by the
displayed one. --expected-offer remains a compatibility spelling, and both
spellings must match when supplied together. The command returns the
InvitationView directly in data. The grant is executed by the deployment's
technical executor, so the command needs no browser bridge and works in a
headless or CI session. A repeat with the stored digest returns the same
accepted result; a different digest refuses and cannot overwrite consent. The
returned view is the authorization-time view of the consent commit
(pending_grant); read the settled active state with get or wait.
Organization writes
org create and org retry restore the protected current session and start the
existing command-scoped browser signer. They never read or write the saved read
default. Local input checks run before any client, browser, or backend work, and
they include the confirmation gate:
- A noninteractive or JSON write requires
--confirm. Without it the command refuses with exit 2 before any client, browser, or mutation work. - A terminal write verifies the session and performs its Organization reads
first, then prints the resolved preview and asks a default-no prompt, even when
--confirmis supplied. Rejection refuses with exit 2. An absent session is exit 3 before any preview. The browser signer is built only after confirmation succeeds, so a refused write never starts it.
--email selects the session explicitly. Otherwise the protected
current-session pointer decides. An invalid explicit email refuses with exit 2.
--timeout-seconds bounds the command's own wait, not backend execution. It
accepts integers from 1 to 3600 and defaults to 120, and it is one deadline for
the whole write: the completeOrganization or retrySetup call and the
settlement that follows share it. While a lane settles the command reads the
exact Organization lifecycle every second with no overlapping read. At the
deadline the active SDK signal is aborted, so the command stops waiting and
forwards no further signature, while the durable backend lane keeps running.
Committed lifecycle progress is flushed first, so the Organization ID stays on
stderr and the same Organization resumes with org retry --org. Exceeding the
deadline returns exit 5.
org create validates --name, the --handle grammar, and the ISO --country
code first, and its terminal preview also lists the Organizations the person
already has. It subscribes to the onboarding Organization-state projection, then
completes the Organization with an explicit create intent. The first committed
Organization ID is retained, and each committed lifecycle change is written to
stderr before the next Organization authorization digest reaches the signer.
JSON mode writes one newline-delimited progress object per change:
{ "type": "organization.lifecycle", "organizationId": "O", "status": "settingUp" }Human mode prints the full Organization ID and lifecycle status. A refused or
failed lane still reports the Organization it is about. The command returns
success only after an exact org(O).getLifecycle() read reports ready. It
never saves a read default and never returns a different ready Organization.
org retry requires an explicit --org (or --org-id). It validates the ID
with the public toOrgId constructor, reads that exact Organization's lifecycle
for its terminal preview, resumes that same durable setup lane, then settles the
same Organization's lifecycle. A resumed lane that asks the
signer to reset its session clears the stale readiness, closes that browser
bridge, and opens the next one for the same command. org retry never creates a
replacement Organization and never changes the read default. An already-ready
Organization returns that exact readiness without a signature. A conflicting
failed or in-flight lane is preserved and refused.
The result data for both commands is { organizationId, lifecycle }. A known
Organization ID on a WRONG_STATE refusal appears only in the SDK-supplied
error.details.organizationId field, and the refusal copy names the recovery
action instead of authentication guidance. No token, OTP, signature, or signer
capability is ever printed. Interruption returns 130 and closes both the
subscription and the browser bridge.
Configuration
| Variable | Use |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| CAPXUL_CLI_HOME | Directory for global CLI settings. Defaults to $XDG_CONFIG_HOME/capxul/cli, or $HOME/.config/capxul/cli. |
| CAPXUL_PUBLISHABLE_KEY | Optional developer override for the bundled first-party staging key. |
| CAPXUL_BOOTSTRAP_URL | Bootstrap origin. Defaults to https://site.preview.abuusama.dev. Convex Cloud origins are rejected. |
| CAPXUL_POSTHOG_HOST | Optional development override for the built-in public PostHog ingestion origin. |
| CAPXUL_POSTHOG_PROJECT_TOKEN | Optional development override for the built-in public project ingestion token. |
| CAPXUL_TELEMETRY_DISABLED | Set to true to disable remote observation regardless of the saved preference. |
The published CLI includes the verified first-party staging application key and
Capxul-owned public ingestion configuration. You do not need key or PostHog
environment variables. A different bootstrap origin requires an explicit key. Collection defaults to enabled for
ordinary local and online commands, help, version, and safely attributed argument
refusals. Parser refusals produce a completion without a start. Early native
global errors with no resolved command route send nothing, because the CLI
cannot determine whether they belong to a silent collection control.
telemetry disable saves one preference for the OS user and sends no final
remote event. Already running CLI processes check the current preference before
each export. Requests already sent cannot be recalled. telemetry status
reports the stored preference, effective policy, configuration, and reason.
--log-level controls normal diagnostic output, not collection. An eligible
invocation still produces its remote completion unless collection is disabled.
Eligible commands reuse a random anonymous identifier stored in the protected CLI directory. Invocation and trace identifiers remain separate. A first-use marker records the first eligible observed use, not a download or a verified person. If this state cannot be accessed safely, observation stops and the command retains its normal result. Delivery is bounded and best-effort; the CLI does not keep a persistent activity queue.
Settings use a versioned JSON file in a directory with mode 0700. The file has
mode 0600. Writes are atomic and serialize between processes. A later writer
can recover an abandoned lock after the owning process exits. Invalid schemas,
unsafe permissions, symlinks, and corrupt state produce a storage failure.
Output
--json writes one version 1 envelope to stdout. Success contains data, which
can be an object, array, or null according to the command's SDK result.
Failure contains error.code and CLI-owned error.message. Each envelope has
command, invocationId, and outcome. An invocation ID is null before a
command starts. Human errors go to stderr.
| Exit | Meaning |
| ----- | -------------------------------------------- |
| 0 | Command completed |
| 1 | Unexpected defect |
| 2 | Invalid input, configuration, or local state |
| 3 | Authentication required |
| 4 | Authority refused |
| 5 | Dependency failure |
| 124 | Online deadline exceeded |
| 130 | Interrupted |
Distribution
The npm package is @capxul/cli. The Homebrew formula consumes that exact npm
release, verifies its checksum, and installs Bash, Zsh, and Fish completions.
Public installation status is in the
Homebrew tap.
After publication:
npm install -g @capxul/cli
brew install xelmar-tech/tap/capxulHomebrew installs Zsh completions in its standard completion directory. npm users can generate the same script:
mkdir -p ~/.zsh/completions
capxul --completions zsh > ~/.zsh/completions/_capxulAdd these lines to .zshrc, before any existing compinit call. If the shell
framework already calls compinit, add only the fpath line before it.
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit
compinitUse npm update -g @capxul/cli or brew upgrade xelmar-tech/tap/capxul to update.
Use one installer for the capxul executable to avoid conflicting PATH entries.
After a successful human command, help, or version request, the CLI can show an
update notice on terminal stderr. It checks the public npm latest tag at most
once per 24 hours. It shows only the command for the verified running npm-global
or Homebrew installation. It does not execute that command. Unknown installations,
development versions, JSON output, redirected stderr, CI, shell completion, and
all telemetry commands receive no notice. The optional check has one 500 ms
budget. Safe failed attempts are cached. Storage or network failures remain
silent and do not change the command result. This check sends no telemetry.
Development commands
vp test run apps/cli
vp run --filter @capxul/cli check-types
vp run --filter @capxul/cli build
vp exec node --test --test-name-pattern='@capxul/cli' scripts/__tests__/pack-install-smoke.test.mjsThe last command installs the tarball outside the workspace and exercises the
installed executable with isolated settings and a local HTTP server. The CI
CLI / Linux / Node 24.0.0 job runs this proof at the declared minimum version.
Email login and session restoration
This CLI establishes a first-party BetterAuth session. It has broader authority
than the earlier native account:read grant. Existing native grants remain
stored and tagged as legacy-native-grant in status. A profile read asks for an
explicit new login; it never exchanges the old grant for a broader session.
Logout forgets and attempts to revoke both credential types for the selected email.
For a human, capxul auth login prompts for email and a hidden OTP. auth signup
also prompts for the current Profile fields: display name, two-letter country
code, and handle. The browser opens only when the shared Core onboarding journey
needs Openfort wallet readiness. It shows wallet status only. It has no email,
OTP, Profile, or approval form.
For an agent, send and verify in separate processes:
capxul auth send --email "$TEST_EMAIL" --json
# Supply the delivered six-digit OTP through stdin, not a command argument.
capxul auth verify --email "$TEST_EMAIL" --otp-stdin --json
capxul auth signup --email "$TEST_EMAIL" --display-name "Test Person" --country GH --handle test_person --json
capxul auth profile --email "$TEST_EMAIL" --json
# A later process uses the same protected CLI home; no new OTP is required.
capxul auth profile --email "$TEST_EMAIL" --json
capxul auth status --email "$TEST_EMAIL" --json
capxul auth logout --email "$TEST_EMAIL" --json--otp-stdin reads up to 64 bytes, ending at EOF. Pipe from a secret provider or
use input redirection from a protected file. OTPs are never accepted in argv,
persisted in the continuation, or included in output. Non-interactive commands
without the required input refuse with exit 2 instead of prompting.
A fresh non-interactive signup can do verification and setup in one command:
# Supply only the delivered OTP through stdin.
capxul auth signup --email "$TEST_EMAIL" --otp-stdin \
--display-name "Test Person" --country GH --handle test_person --jsonThe local page binds to an OS-selected loopback port. A random launch capability
is redeemed once and removed from the URL before Openfort starts. The page receives
only the authenticated wallet token and encryption session in memory. Closing it
ends the current wallet attempt. Re-run auth signup with the same CLI home to
resume the same Profile and Account without another OTP while the backend session
remains valid.
The version 2 first-party-session record stores the opaque provider credential
in the existing protected plaintext store. Its scope includes the application
key, issuer, environment, and email. Each process validates restoration with the
backend; cached session data is not authority. Conditional replacement prevents
a concurrent logout from being undone by a delayed credential save. The backend
owns expiry and revocation. A failed remote logout is reported as unconfirmed;
local sign-out remains in effect.
auth profile returns a backend-read Profile and account lifecycle without opening
the browser. Successful email authentication can return setupState: "setup-required".
auth signup returns setupState: "ready" only after Core reads a ready Account.
After logout, profile reads refuse with NOT_AUTHENTICATED and exit 3. An invalid
or expired OTP refuses with exit 2.
