@arbol-ai/cli
arbol, the Arbol API from your terminal. Its commands are generated by
Cloudflare Forge from the platform's public OpenAPI,
https://api.getarbol.com/openapi.json, and every request
goes through @arbol-ai/sdk. Its versions are semver,
from 2.1.0 on (see Releases); arbol --version also names the API release its commands
were generated from, as in arbol 2.1.0 (Arbol API 1.20261019.4056).
npm install --global @arbol-ai/cli
arbol --version
It needs Node.js 26 or newer, whose fetch and Temporal the SDK uses. The same command updates it.
An arbol installed before the CLI was on npm comes first or blocks the install: remove it with
npm uninstall --global @getarbol/cli, or delete ~/.local/bin/arbol and ~/.local/share/arbol.
Profiles
A profile is a named way to call Arbol: a login, or an API key. Commands use the profile --profile
names, else the one ARBOL_PROFILE names, else the default profile; arbol auth use <profile> changes
the default.
arbol auth login # a login, saved as the default profile
arbol auth login --profile clinic # another login, for another account or organization
arbol auth api-key --profile ci < /secure/path/arbol-key
arbol auth profiles # * marks the default
arbol auth use ci
arbol visoft connections list --profile clinic
ARBOL_PROFILE=ci arbol sura appointments lookup
An API key given to one command, through --api-key-stdin or ARBOL_API_KEY, wins over every
profile, as GH_TOKEN does for gh: scripts, servers and CI can run without saving anything.
When arbol auth login without --profile signs in as someone else, or to another organization,
than the default profile is, it asks where the login goes: a new profile (it suggests a name from the
organization), in place of the profile, or beside it in the same profile (arbol auth switch moves
between them). In a script it saves nothing and asks for --profile instead. --profile <name>
always saves the login in that profile.
arbol auth status prints which credential is in use and, for a login, its organization;
arbol auth logout signs a login out, or forgets a profile's API key (the key itself works until it
is revoked in Arbol Account).
Login
arbol auth login
arbol sura appointments lookup --help
arbol agenda appointments list --limit 25
arbol auth switch [<name|org_…>]
arbol auth status
arbol auth logout
The CLI knows only the API's origin (https://api.getarbol.com, or the one ARBOL_API_URL names),
and discovers how to sign in from it, as an MCP client does:
/.well-known/oauth-protected-resource(RFC 9728) names the resource tokens are issued for and the authorization server, whose own metadata names its endpoints and keys./.well-known/oauth-client/arbol-cli(RFC 7591 client metadata) names the CLI's public client (no secret), the loopback address a login returns to, and the scopes to ask for.
Login then opens the system browser (Authorization Code with S256 PKCE and the loopback callback,
http://127.0.0.1:43817/callback). When you belong to several organizations, the sign-in page asks
which one after you sign in. arbol auth switch lists the organizations you belong to: one you used
before switches at once, any other signs in again through single sign-on, which asks for no password.
arbol auth switch <name|org_…> does the same without asking, for scripts. The session is for the
organization the verified ID token names.
The scopes asked for are every permission of the API's release, because a token's authority is its
scopes intersected with the person's role permissions; ARBOL_SCOPES replaces them. Asking for a
scope does not grant it.
The CLI verifies state, nonce and S256 PKCE and the ID token's signature and organization.
--no-browser prints the URL for manual opening but still uses the loopback callback on this machine.
Where sessions and keys are kept
~/.arbol/config.json (mode 0600) holds the profiles and the default, and no secret. Each login's
session and each saved API key is a sealed file under ~/.arbol/ (mode 0700 directories, 0600
files): AES-256-GCM under one random key per person and machine, which the OS keeps. Profile, issuer,
client, audience, API, user and organization partition sessions, and each file is bound to its name.
There is no plaintext fallback.
| OS | The key |
|---|---|
| macOS | Keychain item "Arbol CLI" (com.getarbol.cli, session-key), kept by /usr/bin/security |
| Linux | Secret Service, through secret-tool (package libsecret-tools) |
| Windows | ~/.arbol/session-key.dpapi, protected by DPAPI for the Windows user |
The OS is asked for the key once per command and written to only once, when the key is created; a
refresh rewrites only the sealed file. On macOS the Keychain trusts the program that created an item.
Node from Homebrew, nvm or fnm is signed ad hoc, so CLIs that use the Keychain from Node ask for the
login password again after every Node update. /usr/bin/security is signed by Apple and stays trusted,
as it does for gh, so the CLI never asks. Like gh, any program running as you can read the key
through it; another user, a backup or a stolen disk cannot. The key never appears in a command line.
Each refresh is locked across processes. Before a rotating exchange the keyring records an
in-progress marker. A lost response or crash requires a fresh login, never a blind retry with the
previous refresh token. A stale .lock after a crashed process is removed only after confirming that
process stopped; then log in again. Logout removes local credentials even if remote revocation fails
and reports that uncertainty. It does not pretend already-issued tokens vanish immediately.
Machines and operation input
An API key is created in Arbol Account for one organization and shown once. A key is never accepted
as a command-line argument: arbol auth api-key reads it from stdin (or asks for it without showing
it), checks it against the API and seals it; --api-key-stdin and ARBOL_API_KEY use one for a
single command without saving it. A refused key never falls back to a login.
ARBOL_API_KEY="$(cat /secure/path/arbol-key)" arbol visoft connections list
arbol sura appointments lookup --api-key-stdin < /secure/path/arbol-key
Commands are arbol <service> <resource> <verb> [id], the operation's public name. A service is one
word: its published id without hyphens, so salud-total.affiliations.lookup is
arbol saludtotal affiliations lookup (and policia-colombia is policiacolombia). The published id
still works where you typed it before (arbol salud-total …). arbol <service>
lists a service's commands; --help after one prints its fields, the values each accepts, their
defaults and an example. --dry-run prints the request a command would send (method, URL and body)
without reading a credential or sending anything. A deletion asks for confirmation on a terminal; a
script passes --yes.
Each input field is a flag (--integration-id sura), or the whole input comes from
--input file.json (--input - reads stdin; flags override its fields; with --api-key-stdin, use a
file). The CLI validates the input against the operation's exact schema before sending it. The
response's data is printed as JSON; a failure prints its code and the request id and, for a write
that got no answer, that its outcome is unknown, and exits nonzero. Nothing is retried, even after 401
or 403; reconcile an uncertain write before repeating it.
ARBOL_API_URL selects a trusted nonproduction deployment for new profiles and for an API key given
to a command; it must be an HTTPS origin. Everything else about signing in is discovered from it, and a
profile keeps the API it was created for.
Development
bun install
bun run forge # build the pinned Cloudflare Forge with cargo into .forge/ (once; needs Rust)
bun run openapi # download https://api.getarbol.com/openapi.json
bun run generate # src/generated/ from openapi.json with the plugin in generation/plugins/cli.ts
bun run verify # typecheck and dist/; tests require an explicit human request
src/generated/ is the plugin's output, unedited; everything else in src/ is the CLI itself.
openapi.json is downloaded at generation time and not committed, and src/generated/generation.json
records which release, Forge commit and plugin wrote each file.
Releases
A release of the API flows through three repositories, each started by the one before it:
- project-arbol-gateway publishes the OpenAPI at
https://api.getarbol.com/openapi.jsonand sendsopenapi-releaseto project-arbol-sdk-js. - project-arbol-sdk-js regenerates
@arbol-ai/sdkfrom it, publishes it to npm and sendssdk-releasehere:{ "version": "<SDK version>", "openapi": { "release": "...", "sha256": "..." } }. - Here, Generate releases when the OpenAPI's content changed:
the SHA-256 of its canonical JSON without
info.version(keys sorted, no whitespace; seegeneration/openapi-hash.ts, the same hash sdk-js computes), compared withopenapiContentSha256in main'ssrc/generated/generation.json. It also releases when the@arbol-ai/sdkto pin is not the one main pins. Otherwise the run stops and nothing is committed or published. A release pins@arbol-ai/sdkexactly, regeneratessrc/generated/, and whenbun run verifypasses commitsRelease Arbol API <release> with @arbol-ai/sdk <version>to main and runs Publish on that commit withbump: minor.
The SDK Generate pins is the version sdk-release names. Every 6 hours and by hand, Generate runs the
same check and pins the highest @arbol-ai/sdk whose published package.json says it was generated
from the same OpenAPI content (arbol.openapi.sha256); while there is none, it stops with a notice,
since the SDK is released first. Its push uses an installation token of the organization's GitHub App
(Arbol Bot: the variable ARBOL_BOT_APP_ID and the secret ARBOL_BOT_PRIVATE_KEY), so the commit runs
Verify like any other.
Versions are semver, computed by Publish from the highest version on npm
(generation/release.ts):
- A release from Generate raises the minor: 2.1.0, then 2.2.0. The first 2.x release is 2.1.0; before it
versions were the build's date and time (
1.YYYYMMDD.HHMMSS). - A change to the CLI's own code, which the OpenAPI does not carry, is released by running Publish by
hand on main. Its
bumpinput raises thepatch(the default),minorormajor.
The published package.json records the OpenAPI the commands were generated from:
"arbol": { "openapi": { "release": "<info.version>", "sha256": "<content hash>" } }. npm receives the
package with the organization's NPM_TOKEN. Package access stays public while the source repository
is private. Provenance is disabled because npm does not support it for private source repositories.
Migrating to CLI 3
CLI 3 uses SDK 5 and the current public owner contracts. SURA appointment reads use
arbol sura appointments lookup; Agenda booking discovery has separate
booking-locations, booking-offerings, booking-requirements, booking-slots
and nearest-booking-slots resources. Account uses account organization get
and account organization update. Named resource identifiers and JSON request
fields follow each command’s help.
This is a deliberate major release for incompatible command and request changes. Automatic generation does not detect compatibility or choose a major version; review the owner contract changes before publishing.
License
Validation
bun run verify checks types and builds the CLI without executing tests. Tests are
manual: run bun run test or bun run verify:with-tests only when a human requests
that run. Automatic verification, generation and publication use the static path.
Staff connection administration
Authorized staff users can manage provider connections with arbol account connections create, update <connectionId>, delete <connectionId>, discover and refresh-tools <connectionId>, using --input for the owner's strict JSON body. These hidden REST commands use captured Account contracts, not public SDK or MCP credential intake. The API requires a real user and account:connections:operate; an API-key profile is refused locally too. Requests are sent once and an uncertain write must be checked before repeating. --dry-run is local and does not authenticate or save credentials.
The credential bundle is sealed by Account Vault. e-Oficlinic takes both login stages atomically plus the source company/site/IPS selection; Sanitas takes its portal login and document type. Never place real credentials in a repository or shell history. Refresh these contracts from a clean owner checkout with bun scripts/update-account-operator-contracts.ts <account-repository>. The captured file records the owner commit and source hash.