@omelhorsite/cli
v0.5.0
Published
oms - command line client for omelhorsite, built on @omelhorsite/sdk.
Readme
oms
oms is the command line client for omelhorsite, built
on @omelhorsite/sdk. It adds
exactly what an isolate cannot have: the filesystem, the environment, stdout and
a browser. Everything talks to https://backend.omelhorsite.pt.
- A shell, or an agent that has one. Use
oms. It has a stable exit code contract, prints JSON under a pipe without being asked, and describes itself withoms manifest --json. - Code that has to run somewhere else - a Worker, another app, a job. Use the SDK directly. It needs no filesystem and no environment.
- An agent with no shell. Use the MCP server, after reading its security note. It is not a sandbox.
Install
Node 20 or newer, nothing else.
Debian and Ubuntu, from the signed apt repository:
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL https://afonsopc.github.io/oms-packages/apt/oms.gpg -o /etc/apt/keyrings/oms.gpg
echo "deb [signed-by=/etc/apt/keyrings/oms.gpg] https://afonsopc.github.io/oms-packages/apt stable main" \
| sudo tee /etc/apt/sources.list.d/oms.list
sudo apt-get update && sudo apt-get install -y omsHomebrew, on macOS or Linux:
brew tap afonsopc/oms https://github.com/afonsopc/homebrew-oms
brew trust afonsopc/oms
brew install omsbrew trust is required: Homebrew will not load a formula from a third-party
tap without it.
From npm:
npm install -g @omelhorsite/cli
oms --helpZero-install: npx @omelhorsite/cli auth login (or bunx @omelhorsite/cli).
The packaged builds are one JavaScript file with the SDK bundled in, so node is the only thing they need. Debian 12 ships node 18, which is too old; Debian 13 and Ubuntu 24.04 are fine. The packaging lives in oms-packages.
A single-file binary with no runtime at all on the target machine comes out of
bun run build:binary on a checkout (dist/oms, about 60 MB).
The authority on what a given build can do is the build itself:
oms manifest --json # every command and flag, as data
oms <command> --help # one command, with its examples and limitsAuthenticate
oms auth login runs the OAuth device grant (RFC 8628). It prints a URL and a
code to stderr before it tries to open a browser, so it works over SSH, and
your password never enters this process.
oms auth login # all scopes, default profile
oms auth login --profile work
oms auth login --scope 'openid storage:read' # a token meant for one jobCredentials land in ~/.config/oms/config.json ($OMS_CONFIG_DIR and
$XDG_CONFIG_HOME win in that order) at mode 0600, per named profile. Access
tokens live two hours and refresh themselves; the refresh token in that file is
what actually holds the grant.
For CI, containers and the MCP server there is no browser, so pass the token in the environment instead. Nothing in this CLI ever prompts without a terminal.
| Variable | Meaning |
| --- | --- |
| OMS_TOKEN | The credential for this run. Never written to the config file. |
| OMS_BASE_URL | API root. OMS_API_URL is an accepted alias. |
| OMS_PROFILE | Which profile to use. |
| OMS_CONFIG_DIR | Directory holding config.json. |
Precedence is flag, then environment, then the config file, then the built-in default, resolved independently per axis.
An access token from oms auth login is the credential to use: whoami,
quota, tools, storage and tickets all accept it, bounded by the scopes
you were granted. The parts of the API that no scope reaches yet are listed
under Security.
| Command | |
| --- | --- |
| oms auth login | Sign in through the browser and store the credentials. |
| oms auth logout | Revoke the stored credentials and forget them. |
| oms auth whoami | Who the credential belongs to. One request. |
| oms auth status | What this machine holds, and until when. No network call. |
| oms auth token | Print the access token on stdout, refreshing first if needed. |
| oms auth profiles | List the profiles, or switch with --use. |
Commands
oms manifest --json is this table as data, generated from the same
declarations that produce --help. Prefer it over reading this file.
oms quota - every ceiling, in one request
| Command | |
| --- | --- |
| quota [resource] | Every quota on the account: the metered tools, the storage item count and the music bytes. |
One request, against GET /quotas. tools quota is still there and still
reads the per-tool endpoints; use it when the credential carries tools:read
but not profile.
oms tools - the metered media tools
| Command | |
| --- | --- |
| tools transcribe <file> | Transcribe audio to text, with SRT and VTT output. |
| tools separate <file> | Split a track into vocals and instrumental. |
| tools upscale <file> | Enlarge an image without turning it to mush. |
| tools bg-remove <file> | Cut the subject out of an image. |
| tools captions <file> | Burn karaoke captions into a video. |
| tools jumpstyle <file> | Cut clips to the beat of a track. |
| tools download <url> | Fetch media from a public URL, or find it by artist and title. |
| tools status <tool> <id> | Poll a run started with --no-wait and write its result. |
| tools quota [tool] | What is left of today's quotas. |
| tools passgen | Generate a password or passphrase. Never leaves the machine. |
| tools qr <text> | Render a QR code as SVG. Never leaves the machine. |
| tools iplookup <ip> | Country, ASN and network of an IP address. |
Every tool that uploads waits for its result by default; --no-wait prints the
id and exits, and tools status picks it up later. Quotas are daily, counted
per user when signed in and per IP when not.
oms storage - the virtual filesystem
| Command | |
| --- | --- |
| storage ls [path] | List a directory. |
| storage stat <path> | One node's metadata and where it sits. |
| storage get <path> | Download a file. -o - streams to stdout. |
| storage put <files...> | Upload into a directory. Multipart above 32 MiB, automatically. |
| storage mkdir <paths...> | Create directories, nested levels included. |
| storage mv <from> <to> | Move a node, rename it, or both. |
| storage cp <from...> <to> | Copy nodes into another directory. |
| storage trash <paths...> | Move to the trash. Reversible. |
| storage rm <paths...> | Delete permanently. --yes required, --recursive for a directory. |
| storage empty-trash | Destroy the trash and reclaim the quota. |
| storage zip <path> | Download a directory as a zip. |
| storage share <path> | Share a node, list its grants, or revoke one. |
Paths are a convenience of this CLI: the backend addresses nodes by id and has
no path column, so every path costs one request per segment. Pass id:<id> when
a script already knows the id.
oms tickets - support
| Command | |
| --- | --- |
| tickets list | Your tickets, most recently active first. |
| tickets show <id> | One ticket and its whole thread. |
| tickets new --subject ... | Open a ticket, screenshots included. |
| tickets reply <id> | Add a message. Reads stdin when --message is omitted. |
| tickets close / reopen <id> | The only fields an owner may change. |
oms links - everything that ends in a shareable URL
| Group | Leaves |
| --- | --- |
| links shortlink | create ls rm stats |
| links notepad | get set |
| links qr-dynamic | create ls set rm stats |
| links chest | open ls add get rm destroy |
| links form | ls show create publish submissions rm |
| links linktree | ls show create cv stats rm |
Creating any of these is throttled per IP and signing in does not raise the ceiling. None is retried automatically: a replayed create would mint a second one under a different endpoint.
oms cron - scheduled scripts
A job is a TypeScript script the server runs on a cron schedule with the SDK built in. The commands edit one field of a job at a time, so changing the code does not touch the config and changing a secret does not touch the code.
oms cron templates list # ready-made scripts
oms cron jobs create --name "Backup" --template hello --set greeting=Olá
oms cron jobs show <id> # schedule, config, variables, state
oms cron jobs show <id> --code > job.ts # edit it...
oms cron jobs update <id> --code job.ts # ...and send it back
oms cron jobs update <id> --set max_candidates=60 --set 'ignore=["recon.pt"]'
oms cron jobs update <id> --secret TELEGRAM_TOKEN=123:abc --network true
oms cron jobs update <id> --enabled false
oms cron jobs run <id> --wait # run now, print the logs
oms cron jobs test <id> --wait # same, without saving state or notifying
oms cron runs list --job <id>
oms cron runs show <run-id> --logs--set merges one key into the current config and parses the value as JSON
when it is JSON (--set on=true, --set 'terms=["a","b"]', --set
models.report=x for a nested key); --config file.json replaces the whole
thing. run --wait exits 1 when the run ends in error, so a script can chain
on it. The account needs the cron:read and cron:write scopes; a login from
before they existed has to sign in again.
The contract a script can rely on
- stdout is the result and nothing else. Progress, warnings and errors go to stderr, in every format.
- JSON is automatic when stdout is not a terminal.
--jsonforces it on,--no-jsonoff. A pipe needs no flag. (oms auth tokenis the deliberate exception: it prints the bare token, because$(oms auth token)is not a TTY either.) - Nothing prompts without a terminal. Destructive commands refuse instead of defaulting.
- Exit codes are stable.
| | | | |
| --- | --- | --- | --- |
| 0 ok | 3 auth | 6 timeout | 9 unreachable |
| 1 other failure | 4 quota or rate limit | 7 not found | 10 server broke |
| 2 usage | 5 the job ran and failed | 8 API refused | 11 local problem |
130 is Ctrl-C. Everything above 6 is a narrower reading of what would
otherwise be a 1; a script that only knows 0 to 6 still behaves correctly.
Security
Read this before handing a token to anything that is not you.
What a CLI token permits. oms auth login with no --scope asks for every
scope the server defines: openid profile email tools:read tools:write
storage:read storage:write tickets:write. A token holding those can read,
write, move and permanently delete your entire storage tree, empty your
trash, spend every daily tool quota, and open tickets in your name. Narrow it
with --scope when the token is for one job; the approval page shows what is
being asked before you consent.
What it does not permit. No scope grants administrative access. Scope
enforcement is default-deny: an OAuth token reaching a controller that has not
declared a scope for the action is refused, so opening a new area of the API to
the SDK is a deliberate edit rather than a side effect of shipping an endpoint.
Session and credential management - /sessions, /identities,
/webauthn_credentials, /auth/* - is closed to OAuth tokens entirely.
Status. Both halves are live. The server resolves an OAuth access token on every request that carries one (a browser session, when both are present, wins), and every endpoint an access token may reach declares the scope it needs; a token arriving anywhere else, or with the wrong scope, gets
403 insufficient_scopenaming the scope required and the scopes granted. That default-deny rule is enforced and tested, so scopes are a control, not a design.What no scope reaches yet, and therefore what
omscannot do with an access token: everything underoms links(short links, notepads, dynamic QR codes, chests, forms, link trees), plus music, movies, the library, blogs, the assistant, direct messages, anything administrative, and the account's own session and application management. Those commands answer403with an OAuth token; a session token still works there, with the warning below.
A legacy session token is not the same thing. OMS_TOKEN also accepts the
opaque UUID the web app holds. It carries full account authority, no scopes
and no expiry, and the two are indistinguishable on the wire. Never give one
to an agent, to CI, or to anything you would not hand your password.
Where the credential sits. ~/.config/oms/config.json, mode 0600,
plaintext. It holds a refresh token, which is the grant itself and not just a
copy of it. There is no OS keychain integration. Anything that can read that
file, or run oms auth token, has your account for as long as the grant lives.
Revocation, and its asymmetry. oms auth logout revokes server-side, so it
kills the whole grant and not only this machine's copy. Changing your password
revokes everything, sessions and grants alike. Signing out of the browser
revokes nothing here - that is the point of having two models.
Guards are against accidents, not intent. --yes on storage rm and
empty-trash stops a script that forgot to think. It does not stop an agent
that decided to pass it.
What the MCP server does not guarantee: a sandbox. execute runs the
model's TypeScript in a bun subprocess on your machine, as you, with your
credential in its environment. It can read your files, open sockets and
await import("node:fs"). The subprocess bounds accidents - a runaway loop, a
hang, output that would fill memory - and nothing else. The trust model is
exactly that of giving the agent shell access. The full accounting, including
where a real isolate would go, is in
the MCP server's README.
Found a hole? oms tickets new --subject ..., or the repo's usual channel.
Working on it
bun install
bun run src/main.ts --help # run it in place
bun link # then `oms` anywhere on this machine
bun run typecheck
bun test
bun run build # dist/oms.js, what npm ships; runs on nodesrc/host.ts is the only module that touches the filesystem, stdin, PATH or a
subprocess, and it uses node builtins only: the same code runs under node
(the published package) and under bun (the checkout and the tests). Adding a
command is one file under src/commands/ and one line in src/commands/index.ts;
the help and the manifest are generated from the same argument declarations,
so a flag is written down once.
