npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@terminus-ai/cli

v0.0.4

Published

Terminus CLI (`terminus`): search and use skills, and develop, run, and publish apps, services, and agents on the Terminus platform.

Readme

@terminus-ai/cli

Lightweight Terminus tooling for developers working on their own machines (Node.js 22.16 or newer; zero runtime dependencies). (Inside the Terminus platform itself there is no CLI: apps are created and staged natively by asking Norbert, and published from the Store UI.)

Installs the terminus command (once the package's first npm publish lands — until then install from the repo: npm install -g github:Terminus-Intelligence/terminus-cli):

npm install -g @terminus-ai/cli

(One-off runs work too: npx @terminus-ai/cli status.)

Catalog search

Search the Terminus catalog as your account (run terminus login first):

terminus search "browser automation"
terminus skills use "browser automation"

The installed binary is also available as terminus-cli:

terminus-cli search "browser automation"
terminus-cli skills use "browser automation"

skills use searches the catalog, prompts for a numbered result, records the selected use on your account, and prints the selected skill content for the agent to follow. In non-interactive shells, pass an explicit selection:

terminus-cli skills use "browser automation" --select 1
terminus-cli skills use "browser automation" --use-first

When the argument is a skill address or id instead of a query, skills use skips search and loads that exact skill (installed pointer skills rely on this):

terminus-cli skills use "@zarazhangrui/frontend-slides"

Installing skills (pointers by default)

terminus skills install <skill> --agent claude|codex|both adds a skill to a coding agent so it auto-triggers in future sessions. By default it writes a small pointer stub — the skill's name and description plus instructions to load the real content from Terminus at use time — so content stays fresh on the platform and every use is counted. --local copies the full package to disk instead, like npx skills; local copies do not count uses, and terminus skills update refreshes them. Either way the content comes through terminus skills use, which serves open-source skills only: a skill whose content is closed is used on Terminus itself, and skills install, fetch, files, and file refuse it (exit 77) — though its owner can still fetch it and read its files.

terminus skills install "@zarazhangrui/frontend-slides" --agent claude            # → ./.claude/skills/
terminus skills install "@zarazhangrui/frontend-slides" --agent claude --global   # → ~/.claude/skills/
terminus skills install "@zarazhangrui/frontend-slides" --agent codex             # → ./.codex/skills/
terminus skills install "@zarazhangrui/frontend-slides" --local                   # full copy (open-source only)
terminus skills list
terminus skills update                        # or: terminus skills update frontend-slides
terminus skills uninstall frontend-slides

Without --agent, install picks the agent you are running in (Codex when its environment says so), else Claude Code. Installs are recorded in a .terminus-install.json manifest inside the skill directory; skills uninstall only removes directories that have one. Every skill command lives under terminus skills, and terminus skills on its own lists them. terminus skills update rewrites a pointer an older CLI wrote into today's wording.

skills update looks every install up by the uid its manifest recorded (signed in, never by search, so a skill that is gone is never swapped for a look-alike):

  • A pointer already loads the latest instructions at each use, so update only rewrites the stub, and only when it would read differently: a new name, description, or address, or wording from an older terminus. The folder keeps its name. No content is delivered and no use is counted.
  • A --local copy is downloaded again when the skill's change hash moved; that delivery counts as a use, like the install did.
  • A skill that left Terminus, or a local copy whose skill is no longer open source, is reported and left as it is; the command then exits 1.

Fetching supporting files

Skills that reference scripts, templates, or other package files can be materialized into a revision-keyed local cache (~/.terminus/cache/<uid>@<change_hash>/). The cache is ephemeral: safe to delete anytime, reused until the publisher ships a new revision.

terminus skills fetch "@zarazhangrui/frontend-slides"
terminus skills fetch "@zarazhangrui/frontend-slides" --refresh

Scaffolding a new skill

terminus init my-skill --name my-skill --description "One sentence on when agents should use this."
terminus validate my-skill
# make the skill on the web (Creations → New creation); it shows the address
terminus remote add @you/my-skill my-skill
terminus push my-skill

Then publish it from its page on the web; terminus push prints the link.

Login

login checks the local session first. If you are already logged in it prints your current profile name and @username. Otherwise it opens the browser login and stores a local device session at ~/.terminus/session.json.

terminus login

See who you are signed in as and what is waiting for you:

terminus account          # Hi, Winston Gao (@winstongu) / Notifications: 3 · Creations: 5
terminus notifications    # invitations, then unread notifications (--all adds read ones)
terminus creations        # what you have published (and anything Terminus has suspended)
terminus drafts           # what you have not published yet

account refreshes your profile, then counts the two things the web keeps for you: Notifications is what the notification box's badge counts (pending invitations to collaborate or to join a chat, plus unread notifications), and Creations is how many apps, agents, services, and skills you have published, the way Creations on the web lists them (not ones shared with you, not the platform's official rows, not external skill mirrors). A count that cannot be read drops out of the line; on the session's last day a third line says it is about to expire. Signed out, it says how to log in and exits with status 3, so scripts can check. Its JSON output never includes the session token. Reading notifications here does not mark them read. (terminus status is the working-copy command; outside a package it points here.)

Browser login opens https://www.terminus.build/login with a loopback callback. After the user signs in, the web app hands the callback a five-minute, one-time CLI login code bound to this process with PKCE S256. The CLI exchanges it via POST /v1/auth/cli-login-code/exchange for a seven-day database-backed session associated with this device. The browser tab says "You're signed in" only after that exchange succeeds; if Terminus refuses it, the tab shows the reason instead. The session is stored with owner-only permissions. terminus logout revokes it immediately before clearing the local file. System Settings → General → Devices on the web shows its computer, platform, client version, activity, and expiration, and can revoke it remotely.

For a fully local auth test, run both the local API and local frontend and point the CLI at both:

terminus login --api-base http://localhost:3001 --web-base http://localhost:3000

The hosted login page intentionally refuses a localhost API target so a crafted production login link cannot send credentials to an arbitrary service on the user's machine.

For automation (a coding agent in a container, a script), set TERMINUS_TOKEN to a session token; it takes precedence over the saved login and terminus account names its account. There are no API keys: every token is a device session, so it expires after seven days and is revoked with its device.

Catalog commands (search and every skill command) require a signed-in account; there is no anonymous lane. A 401 from Terminus usually means the login expired or was revoked on the web; the error says to run terminus login.

How creations move (the Git and GitHub model)

Every creation starts on the web — Creations → New creation (https://www.terminus.build/os?open=create) — which gives it its address, the way a new GitHub repository gets its URL. The CLI moves files between a folder and a creation that already exists; it never makes or publishes one (a fork is made by Terminus, the way GitHub's Fork button makes one):

terminus clone @you/my-app            # a fresh folder, linked (git clone)
terminus remote add @you/my-app             # connect a folder you already have (git remote add)
terminus push                         # upload your changes to its draft (git push)
terminus pull                         # bring the draft down (git pull)
terminus fork @someone/their-app      # fork it on Terminus, then clone your fork

fork makes free forks. When a creation charges for forking, fork says the price and prints the creation's page: the fork is bought there, and then terminus clone brings your fork down.

Publishing is done on the web, from the creation's page: that is where the version is chosen and what people get changes. terminus push prints the page.

Developing a skill

One of your own skills comes down the same way an app does: terminus clone @you/my-skill writes its SKILL.md and package files into a fresh directory and records the skill's id in the frontmatter (terminus remote add records it in a folder you already have), so a push updates that exact skill even if you rename it. terminus push uploads SKILL.md and the files it references.

A skill keeps a draft beside its live version, exactly as an app does: a push lands on the draft, and what people are using does not move until the skill is published again on the web. terminus status, diff, log, pull, and restore read a skill folder the same way they read an app's. Publishing, and a skill's source visibility, billing, and price, are set on the web.

Commands

Browser apps can use a zero-build starter or a full framework project:

terminus init app my-app --template react          # positional kind shorthand
terminus init my-app --kind app --template react   # vanilla | react | svelte
cd my-app && npm install
terminus remote add @you/my-app                          # the address the web gave it; writes id into terminus.json
npm run build                                      # required before starting the runtime
terminus dev                                       # terminal 1: the app runtime (/_terminus) + fixture data
npm run dev                                        # optional terminal 2: hot reload; proxies /_terminus
terminus push                                      # builds, validates, uploads the draft

Cloning an empty creation gives a folder holding only its address; terminus init <kind> . fills it in and keeps that link.

The same shorthand works for agent, service, and skill (including the plural aliases). Every creation versions the same way: strict MAJOR.MINOR.PATCH, starting at 0.0.1 — app, agent, and service scaffolds carry "version": "0.0.1" in terminus.json, and skills declare version: in their SKILL.md frontmatter. The version is chosen when you publish on the web, and every release must be strictly above the latest; you pick the bump (patch, minor, or major), the platform only requires that there is one.

Pushing hashes files locally and sends only the bundle index through the Terminus API. Missing content hashes stream directly from the CLI to short-lived, checksum-bound S3 upload URLs with bounded concurrency; repeated assets are deduplicated. Bundle bytes are therefore not base64-expanded into one JSON request, and the former 256-file / 4 MiB-file / 16 MiB-bundle CLI caps do not apply. The platform retains configurable operational guardrails for abuse and storage safety.

terminus dev first requires the configured bundle entry point (normally dist/index.html). If it is missing, the command exits before it creates fixture state or binds ports and tells you to run npm run build. It then serves the production runtime protocol over a local fixture data space, one port per simulated member. Plain terminus dev is Alan alone; --members 3 creates Alan, Bob, and Carol; counts from 1 through 26 use the stable Alan-to-Zoe directory, with a display name, bio, deterministic default avatar, and local public profile for every account. Comma-separated handles such as --members alice,bob remain supported for compatibility and receive generated profiles too. It also serves the built bundle from disk, so terminus build + refresh works without vite. The first member uses port 8868 by default and later members use consecutive ports; --port 5000 changes the starting port.

A coding agent can use the running local app from a second terminal in the same app directory (or a subdirectory), with no login or credential files:

terminus apps notes --dev
terminus apps notes create_page --title "Test plan" --dev
terminus apps notes get_page --page-name "Test plan" --dev
terminus apps notes --dev --member bob

--dev discovers this project's runtime; it never falls back to production or uses your saved login or TERMINUS_TOKEN. The default test user is the first member passed to terminus dev; --member selects another. The local target prints to stderr, keeping --json stdout machine-readable. Explicit --api-base and --web-base cannot be combined with --dev.

Access starts Off for each member. Open the App access URL printed by terminus dev or terminus apps notes --dev in a browser. The human selects Off, Read only, or Read and write, and personal/shared data scopes. CLI commands cannot grant themselves access. These are trusted local fixture permissions, not production account authentication. Changes to the app's server code require restarting dev and reviewing access again. The app's own operation schema drives the commands, so this works for any app with agent operations.

The runtime records its ports and instance ID in .terminus/dev-target.json. The CLI verifies the live instance, project, app and member before issuing any operation. A stopped or mismatched runtime fails locally. Only one local runtime can run per project directory; use a separate copy for independent fixtures. --dev targets the local SQLite harness, not terminus dev --remote.

Every dev server (this harness, --remote, agents, services) listens on 127.0.0.1 only: dev tokens, fixture data, and capabilities billed to you are never reachable from another machine. localhost reaches it, and a port that another program holds on any loopback address, 127.0.0.1, ::1, or all interfaces, counts as taken, so a browser resolving localhost never lands on that program instead. Each one also answers only requests addressed to a loopback name (localhost, *.localhost, 127.x, or [::1], at any port): a web page that re-points its own domain at 127.0.0.1 still sends its own Host, and gets a 403 rather than a dev token. Proxies that keep a loopback Host, like the scaffolded Vite proxy, pass. The app runtime (/_terminus/*) and the harness's own controls (/__terminus_dev/*) also refuse what a page on another site sends blind: an Origin that isn't a loopback page, or Sec-Fetch-Site: cross-site on a request with no Origin (an <img>, a no-cors GET). Loopback pages at any port, the Vite dev server's included, and clients that send neither header (curl, scripts, the SDK outside a browser) pass, and the app's pages stay open to a link from anywhere. Agent and service dev apply the same test to their token-gated /api doors. Before --fresh clears fixture data, the harness checks the entire requested port range. If a listener is already present, startup reports every blocked port and its simulated member, and says who holds it: another terminus dev by its app, folder, member, PID, and uptime (every harness answers GET /__terminus_dev/about), and any other program by its command line, folder, PID, and uptime where ps and lsof can tell; otherwise it prints a command to inspect the listener. It then says how to stop the holder and suggests a currently free consecutive range with a copyable version of the original command. When the holder is this same folder's harness, it reports the app as already running instead, since two harnesses on one folder would overwrite each other's spaces. An explicit --port is replaced in that suggestion, never changed silently; when using the scaffolded Vite proxy, point its target at the chosen port. Agent and service dev report a busy explicit --port the same way, before they start the engine or import the draft; without --port they take the next free port.

--guest adds one more port, after the members': a visitor who is not signed in, the way an app open to guests meets one on Terminus. There the bootstrap is the platform's guest bootstrap (guest: true, no installation, no user), the app's own files are served (without the notification corner: a guest has no account), and a capability's assets are forwarded for a capability the app declares, fetched with no credential, as the app host fetches a guest's. Every other /_terminus door answers 401 unauthorized ("Sign in to use this (guest mode)"), as production does, and so do the harness's own controls. The app's session.signIn() opens /_terminus/signin?return_to=<path>: on the guest's port that is a page listing the members, and picking one (a same-origin form POST, never a link) makes the port that member, on the same origin, so what the SDK kept for the guest in the browser moves into the account, and returns to the path. A return_to that is not a path on this origin returns to /. Signing out there (/_terminus/logout) makes the port a guest again. On a member's port, sign-in goes straight back to the path. User lookup and app-owned space/member operations are intrinsic, app-scoped SDK calls. Optional cross-boundary authority such as network_proxy still refuses locally unless the release declares it.

The fixture runtime also mirrors the production space contract. A direct space is limited to two people, rejects group member actions, and persists peer block/unblock state; a group persists owner/admin/editor/viewer roles (a viewer reads the space and writes nothing in it) and enforces rename, invite, role-change, removal, and leave permissions. Space rows carry the same my_role and capabilities fields that apps render in production, and a direct space names the other person as its peer — the person invited, until they answer.

When one simulated member starts a chat or sends a space invitation, the invitee's local app origin shows a Terminus-owned request popup with Accept, Decline, and a close-for-now button. Closing hides that request for the life of the page without accepting or declining it; reloading shows an unresolved request again.

Being asked to talk to one person and being added to a room are different questions, so the card is built from the space's kind and asks each on its own terms. A direct request leads with the person — their face in a circle, "@handle wants to chat", and Accept. A group invitation leads with the room — its mark in a rounded square, its name, who invited you, the people already in it, and Join. Pending requests are shown one at a time with the queue depth beside them, and GET /__terminus_dev/requests carries kind, a ready-made title, and the current members so any surface can draw the same distinction. The control is injected into served HTML by the local harness and lives outside /_terminus, so apps still cannot consent on a user's behalf through the SDK. The JSON controls remain available for automated tests at GET /__terminus_dev/requests and POST /__terminus_dev/requests/{id}/{accept|decline}. Like the desk's corner, it hears the member's account frames live — GET /__terminus_dev/notifications/stream, which never counts as the member being online — rather than polling. dev --remote does not inject this simulator because production consent belongs to the Terminus notification surfaces.

terminus dev --remote keeps the local ui/ bundle but sends /_terminus/* to the production runtime instead of the fixture space: the CLI uses your terminus login session to mint an app session exactly as the hosted app origin does (POST /v1/app-runtime/authorizations → POST /v1/app-runtime/session), then proxies every runtime call with that bearer and streams SSE feeds through. Signing out of the app (/_terminus/logout) revokes that session as the hosted origin does, and opening the app again mints a new one. It needs a published app whose address matches the package id and an installation for your account. Sign-in (/_terminus/signin) goes straight back to the path it names, minting a new session first when signed out. You are acting on real data under real quotas; the SQLite harness stays the default and --members, --profiles, --guest, and --fresh do not apply.

Use --profiles only when a test needs custom identities. If --members is omitted, the JSON keys become the member list; omitted avatars keep their generated defaults, and avatar paths are resolved relative to the JSON file:

{
  "alice": {
    "name": "Alice Chen",
    "bio": "Designing calm collaboration tools.",
    "avatar": "./fixtures/alice.png"
  },
  "bob": {
    "name": "Bob Li",
    "bio": "Local test account"
  }
}
terminus dev . --profiles ./dev-profiles.json --fresh

ready().user, users.lookup/search, and spaces.members then expose the same { id, handle, displayName, avatarUrl, publicProfileUrl } SDK shape as published apps. Avatar and public-profile URLs remain same-origin and open local fixture content. The profile file is development input only; exclude it with .terminusignore if it should not be included in source sync.

The online editor and CLI share one canonical source draft, identified by the committed id in terminus.json and an increasing server draft revision. Bring a creation made on the web down with terminus clone @you/app, or run terminus remote add @you/app inside an existing directory. Link writes that canonical address into terminus.json (a service is named by it too); there is no second local identity file. After that, from the project directory:

terminus status                  # what changed here, and what the draft has that you do not
terminus diff                    # those changes, line by line
terminus pull                    # merge the draft's commits into this folder
terminus push --message "..."    # send yours, as one entry in the draft's history
terminus log                     # the draft's commits, newest first
terminus restore <commit>        # put the draft back the way it was

Each push is one commit to the draft (POST /v1/apps/{id}/draft/commits): the files that differ from the draft's head, the ones the folder no longer has, and the fields terminus.json compiles to. The draft keeps every commit — pushes, web-editor saves, a version chosen on the web — and its page shows them under History, with the --message a push gave. A push that changes nothing records nothing ("Everything up to date"). A service's push goes through its import door, which compiles the OpenAPI contract and records the import the same way.

The folder remembers which commit it matches, in .terminus/sync.json (git's origin/main, with a .gitignore of its own so it stays out of your repository). That is what makes the loop behave like git:

  • push refuses a non-fast-forward. If the draft has commits this folder has not pulled, push says so and stops rather than replacing them; --force is the deliberate override.
  • pull merges. A file only the draft changed is taken, one only you changed is kept, one both changed is merged line by line, and terminus.json merges key by key. What cannot be merged is written between <<<<<<< yours / >>>>>>> draft markers and named; push waits until you resolve it. pull --force replaces the folder with the draft instead.
  • restore is a revert, not a reset. It puts the draft's files and compiled settings back to an earlier commit as a NEW commit, so the state it replaced stays in the history.

Git remains your local history: Terminus keeps the draft's, one commit per push or save.

A push never publishes: open the creation's page (push prints it) and publish there, which releases the draft as it stands.

clone answers for a skill you own as well. Someone else's open-source creation clones read-only — the files of its latest release, with terminus pull bringing newer ones in. It cannot be pushed; terminus fork makes a copy in your account that can.

An app package can be as small as:

{
  "kind": "app",
  "id": "@publisher/chat",
  "version": "0.0.1"
}

Name, description, icon, listing, source visibility, and commerce settings are set on the web and are never duplicated in this file. The app uses the stable same-origin /_terminus/icon URL; the hosted worker and terminus dev resolve the icon set on the web, so generated icons do not become repository assets and no icon pull command is needed.

Developer-authored terminus.json files — app, agent, and service alike — have no top-level schema_version. The CLI accepts older app and agent files that include one, removes it when reconstructing a working copy, and compiles the current authoring format into the backend's internally versioned release manifest; a service file that still carries the line is asked to delete it.

Source discovery respects two layers. .gitignore defines the repository's normal source tree. Optional .terminusignore removes additional files from Terminus source sync even when Git tracks them—for example local fixtures or internal design notes. Neither file controls the explicitly configured runtime payload: terminus push runs the build and attaches that output as a derived, content-addressed bundle bound to the resulting source revision. The platform serves that bundle, but it is not editable state and is never pulled back into the working tree.

Secrets, dependency trees (node_modules/), Git metadata, and local dev fixtures are denied in every plane even if an ignore file or Git index includes them. A source edit makes the previous bundle stale; publishing succeeds only after a fresh bundle is attached for that exact revision. A differing pull stops without changing files unless --force is supplied.

Published apps are normal multi-file websites on their own *.apps.terminus.build origin. The SDK uses same-origin, app-scoped APIs for managed collections, storage buckets, the person's own files (the private and output zones), spaces, collaborative events, notifications, logs, and the external services an app's code names. The user's Terminus login token is never exposed to app JavaScript. Managed collections are defined once where app code opens them (db.collection(...)) and are provisioned lazily for the installed release; they are not duplicated in terminus.json (only a collection that nothing but an automation writes is declared there, below). For conventional npm projects the CLI also infers npm run build and dist/, so ui is needed only as a nonstandard override. The manifest stays focused on canonical identity and technical release configuration.

Every generated starter imports the SDK by name (import { db, ready } from "@terminus-ai/app-sdk"), so its bundle carries only the namespaces it uses. It is collection-first: its sample UI opens a live items collection, renders the persistent IndexedDB-backed cached view, and uses optimistic put/delete mutations. The generated AGENTS.md records the same architecture for future coding agents: collections for structured state, storage buckets and files for blobs and exported files. App code must not add an app-owned backend database; user-owned capsules stay independent of the app release and therefore survive forks, upgrades, and migrations.

Apps declare background work as data, not as a deployment. workloads.automations is a bounded list of JSON steps using only collection.put, collection.delete, notification.create, connector.request, service.invoke, and server.run. workloads.schedules attaches a five-field cron plus an optional IANA timezone (UTC by default) to one automation, and lifecycle.migrations advances an integer app-data version through the same durable job path.

An app that needs logic the browser cannot run keeps it in server/: standard-library Node in server/main.js, exporting its ops (exports.ops = { top(input, { terminus, ctx }) { … } }), or Python in server/main.py. Nothing about it is written in terminus.json: what the server code may do is read from the code itself, as it is for everything else an app uses:

  • An op exists because something calls it: the app, with server.call("<op>", input) from @terminus-ai/app-sdk, or an automation, with a server.run step — always by literal name. An op the app calls runs within 10 s; an op only automations run is background-only and runs within 120 s.
  • Records are the scopes and collections the server code's literal terminus.records.<get|put|delete|query|update>("<scope>", "<collection>", …) calls name: global is one pool for the whole app, installation one per user.
  • Every op called must be one the entry exports; terminus validate loads the entry to check.

terminus.records.query answers one page, {documents, next_cursor}: by doc id, or by one top-level field with sort: "-best", at most limit (1–100) documents, and the next page with after: next_cursor. terminus.records.update(scope, collection, doc, fn) is the compare-and-set loop in one call: it reads the record, writes what fn(current) returns against the version it read, and reads again on a version_conflict. A refused syscall throws an Error whose code and status are the platform's (version_conflict 409, quota_exceeded 402, …) — branch on error.code, never on the message.

The CLI ships server/ as release materials with role: "server" (runtime code: it travels with a closed-source release and never enters the browser bundle) and holds the entrypoint to 128 KiB and server/ to 512 KiB. terminus dev runs it the way the platform does: every server.call and every server.run step runs the op in a fresh sandbox — the standard library and server/ siblings only, no network but terminus.webFetch, no files outside server/, the op's time budget — under the platform's records, capsule, fetch and collect rules, limits and error messages. Records live in .terminus/dev/ (one global pool for the app, one installation pool per member), and each op's console output prints in the terminal. Node server code runs locally on macOS and Linux (where one syscall argument, such as a records.put document, is capped at 128 KiB — tighter than on the platform); Python needs python3.

Agents do not use workloads at all: an agent is its own orchestrator, so a trigger on an agent just means "wake the agent". Agents declare four top-level prompt-form sections, each entry carrying at most a prompt (what the wake says — optional, since AGENT.md already says what the agent does) and enabled. Every standing run notifies — there is no toggle: the notification is a compact card (the agent's identity, a short preview the platform derives from the reply's first line, and the agent as the click-through), and a run whose reply is empty sends nothing:

  • schedules (≤64): a structured cadence — { "every": "day" | a weekday | "month" | "30m" | "2h", "at": "HH:MM", "on_day": 1-31, "timezone": IANA or "user" } — or a raw five-field cron escape hatch. "user" means each installer's own timezone: the platform resolves it per installation (captured at install approval, re-resolved hourly), so "every Monday morning" is every user's own Monday morning.
  • webhooks ({ name?, prompt?, description?, enabled?, coalesce_seconds? }, ≤16): installing provisions one capability-URL endpoint per webhook; the person who installed the agent makes its URL in the installation's Automations window on Terminus, which shows it once (making a new one revokes the old), and deliveries at POST /v1/hooks/{token} wake the agent with the payload.
  • watches ({ name?, url, pattern?, interval_minutes?, condition?, prompt?, description?, enabled? }, ≤8, https only, 15-minute floor, hourly by default): the platform polls the source, extracts the optional regex match, and only a real content change wakes the agent with the diff. condition gates firing on a numeric threshold the poller evaluates with no model spend — exactly one of { "changed_by_pct": 2 } (the watched number moved 2% from the value last fired about), { "below": 220 }, or { "above": 250 } (crossing the level) — so "tell me when AAPL moves 2%" costs nothing while the market sleeps.
  • events ({ name?, collection, prompt?, description?, enabled?, coalesce_seconds? }, ≤16): wakes the agent when records in one of the agent's own declared collections change.

name is optional while a section has a single entry and required to disambiguate several. coalesce_seconds (0–3600) batches webhook/event firings into fixed wall-clock windows delivered together when the window closes. Event payloads are data, never instructions — the platform appends them to the wake prompt inside an explicit untrusted-event-data fence. A trigger that fails ten firings in a row pauses itself and notifies the owner.

terminus validate compiles these sections into the platform's canonical workloads wire shape (shared scheduled-run automations parametrized by input.prompt), so the runtime — scheduling, fatigue, the durable runner — is identical to an app's. Publishing a workloads block on a kind: agent manifest is refused with a migration hint. An app example:

{
  "workloads": {
    "automations": [{
      "name": "remind",
      "steps": [{
        "action": "notification.create",
        "params": { "title": { "$from": "input.title" } }
      }]
    }],
    "schedules": [{
      "name": "morning",
      "cron": "0 9 * * *",
      "automation": "remind",
      "input": { "title": "Good morning" }
    }]
  },
  "shell": { "notifications": true },
  "lifecycle": { "data_version": 1, "migrations": [] }
}

terminus validate checks names, cron syntax, migration chains, action permissions, and explicit input.* / context.* substitutions. terminus dev simulates collection and notification actions plus job/schedule/lifecycle SDK routes. Connector and external-service actions stay broker boundaries and should be mocked in local app tests. Unlike collections opened only by browser code, a collection referenced by a background automation must also be declared in resources.collections; this gives the platform a release-approved schema to enforce when no browser session is present.

Agent packages keep their prompt in AGENT.md, any files the agent should know beside it (the whole package ships read-only in the agent's working directory, at its own paths), and a small semantic tool list in terminus.json. Terminus compiles those ids into the detailed release grant; OAuth tokens, connection ids, and per-user resource grants never enter the package. A scheduled inbox agent can look like this:

{
  "kind": "agent",
  "id": "@publisher/daily-inbox",
  "version": "0.0.1",
  "tools": [
    {
      "id": "gmail.read",
      "when": "Prepare the user's daily inbox digest."
    }
  ],
  "schedules": [{
    "every": "day",
    "at": "07:00",
    "timezone": "America/Los_Angeles",
    "prompt": "Summarize what happened in my inbox yesterday."
  }]
}

That one entry is the whole standing life: the agent runs every morning at 7:00 Pacific, and a compact notification lands — the agent, one preview line drawn from the reply's opening, and a tap-through to the agent (a morning with nothing to say sends nothing). The prompt is optional — without it the wake just tells the agent which schedule fired and AGENT.md carries the task — and shell.notifications is derived from declaring a trigger, never hand-written.

Run terminus connectors gmail to inspect the currently supported Gmail tool ids, operations, and scopes. Then terminus validate . and terminus dev . — local agent dev: the loop runs on your machine (bash/python execute natively inside a workspace-write sandbox over the lane workspace in .terminus/dev/agent/), while the system prompt and tool definitions compile server-side from your manifest, so local runs always match what publishing produces. It works immediately after init — no creation or link required — and terminus dev . --prompt "…" [--json] gives scripts and coding agents a one-shot lane.

An agent chooses its models in one models section. terminus init agent writes the house configuration, which is also what an agent that says nothing gets: every model on Terminus, starting on GPT-5.6 Luna.

"models": { "default": "openai:gpt-5.6-luna", "mode": "all" }

"mode": "selective" with an "available" list narrows the choice, and "mode": "default" keeps the agent on its default alone.

An agent's type says who can read its conversations. Left out, it is "default": each conversation stays private to the person having it. "observed" shares every conversation with the agent's maintainers, and people accept a notice saying so before their first message. The inspector's Type switch writes the key for you.

"type": "observed"

Declared triggers are exercisable locally before publishing: the inspector's Schedules section shows every trigger as a card — the cadence in words, the wake prompt, whether it notifies — with add, edit, and remove writing terminus.json in place (webhooks also accept real deliveries at the local POST /hooks/{token} URL the webhook's card shows; point curl or a tunnel at it. As on the platform, the token is the whole capability: it admits a delivery under any Host, and each webhook keeps its token in .terminus/dev/agent/webhook-tokens.json, so a tunnel survives restarts; delete the file to rotate them), and terminus dev . --trigger <name> [--payload file.json] [--json] is the headless lane. A simulated firing builds the trigger's input the way the platform builds it, runs the wake as a real local turn behind the same untrusted-event fence, and reports the notification (or its skip, when the run produced no text) on the transcript. Connect your own Gmail account in Terminus to exercise gmail.read live; before publishing, terminus dev . --remote runs the same conversation on the platform's real engine and real isolate. Publishing and installing the release binds the same gmail.read declaration to each visitor's own Gmail connection; the hosted schedule then runs on that visitor's durable VM-free session.

Service artifacts describe atomic callable APIs. runtime.kind says how the platform reaches the implementation: external names an HTTP endpoint (yours, platform-hosted, or a third party's — the authentication mode and explicit commerce fields carry that difference, while provider_relationship, upstream_cost_bearer, payout_recipient, and health_path default to the first-party posture owned/none/publisher//health/ready); builtin names an engine implemented inside the Terminus backend and carries nothing else.

terminus init my-converter --kind service                  # describe an existing HTTPS API
# set runtime.base_url and the auth mode in terminus.json; state
# provider_relationship/cost bearer only when they differ from the defaults
terminus validate my-converter                   # validates the manifest + OpenAPI
# make the service on the web (Creations → New creation), then connect the folder
terminus remote add @you/my-converter my-converter     # names the package after its address
# deploy the API on AWS/Azure/etc; test updates the service's private draft
terminus service test my-converter document.convert --file sample.pdf
terminus dev my-converter                        # serves the package's dev/ test page
terminus push my-converter                       # uploads the draft; publish it on the web

For a hosted service, create its container deployment in the web hosting panel, select it for the draft, then clone or pull the service. Its runtime contains only {"kind":"hosted","deployment_id":"<uuid>"}. push, service test and dev preserve that binding; calls go through the authenticated gateway. Container environment variables and secrets stay in the hosting panel. Draft tests have no marketplace charge; hosted compute and external provider charges still apply.

terminus dev <service-dir> starts a local test session on its own port: it refreshes the linked service's draft from the folder, serves the package's own dev/index.html (plus siblings in dev/), and relays invocations through the draft-test lane — a direct signed call for public terminus_signed endpoints, the gateway test door otherwise. The page contract is host-provided: the harness injects window.__SERVICE_DEV__ = { service, operations, invoke }, and the page calls invoke(operationId, { input, query, bytes, idempotencyKey }) — input for JSON operations, bytes (an ArrayBuffer or Blob) with query parameters for binary ones — and receives the test-lane result { status, content_type, body, body_base64, … }. Never hard-code a transport: dev/ files upload with the release (role: "dev", outside every runtime plane), and whatever serves the page later provides its own invoke.

For an external service, openapi.json is the machine contract and README.md is human/agent guidance. The contract lives at the repository root by default; terminus.json states an openapi path only when the file deliberately lives elsewhere, and a package with neither is refused. The manifest otherwise contains the unscoped package name, version, description, endpoint, provider/cost relationship, payout policy, and authentication mode. API-key or OAuth secrets are referenced by environment-variable name and uploaded separately; their values never enter the package or release. The backend is the authoritative parser: each test/publish uploads the raw package, compiles OpenAPI, and creates or updates the private service draft and binding. Terminus retains the publisher address, price, icon, listing, release/verification state, and encrypted secrets. Terminus-signed tests receive a short-lived scoped JWT and call the manifest endpoint directly; other auth modes use the broker. No credits move during tests, and there is no arbitrary URL override.

Terminus does not execute service-package code. Browser apps call only the external operations declared in their release; the broker supplies scoped identity, enforces consent and billing, and keeps provider credentials out of browser JavaScript.

Bare terminus (or terminus help) prints its version and the everyday commands, grouped, each with what it does on one line (<word> is a placeholder, [ ] is optional):

Terminus v0.0.4, created by Terminus Intelligence

Account:
  terminus login                         Open your browser and sign in
  terminus account                       Your account's station
  terminus notifications                 Check your notifications

Explore:
  terminus search <query>                Search the Terminus catalog

Developing:
  terminus init <kind> [<dir>]           Start a new creation
  terminus clone <address> [<dir>]       Download a creation to edit
  terminus remote add <address> [<dir>]  Connect a folder to a creation
  terminus dev [<dir>]                   Mock your creation locally
  terminus validate [<dir>]              Check a package for problems
  terminus status [<dir>]                Compare your copy with Terminus
  terminus pull [<dir>]                  Merge the draft's changes into yours
  terminus push [<dir>]                  Upload your changes as a draft
  terminus logs <app>                    Show a published app's logs

  Mock it locally so your coding agent can test end to end — an app:
    terminus clone @you/app && cd app
    npm run build                        # apps are built before they run
    terminus dev --members 3             # three at once; --fresh to start empty
    terminus push --message "Rooms can be renamed"

  An agent, which has no build step:
    terminus clone @you/agent && cd agent
    terminus dev                         # a chat page; --remote for real data
    terminus dev --prompt "Two lines on today's inbox"
    terminus push --message "Sharper summary"

  Every other key, --port included, and what each kind runs: terminus help dev

Install Skills:
  terminus skills                        Install skills in Claude Code or Codex

Management:
  terminus creations                     Manage your published creations
  terminus drafts                        Manage your drafts

Further help:
  terminus commands                      List every command
  terminus help [<command>]              Show how to use a command
  https://www.terminus.build/docs/

terminus commands lists every command, then the options, environment variables, and exit codes they share; terminus help <command> and terminus <command> --help show one command's usage, options, and examples, the way brew search --help does. terminus skills on its own lists every skills command, each with what it does. A command given the wrong arguments prints that same help, then Error: Invalid usage: … naming what was wrong (--json keeps just that line). Both blocks here are the CLI's exact output, kept in sync by a test.

Usage: terminus <command> [<arguments>] [<options>]

Account:
  login          Sign in to Terminus in your browser
  logout         Sign out and revoke this device's session
  account        Your account's station
  notifications  Check your notifications

Explore:
  search         Search the catalog

Developing:
  init           Create a new app, agent, service, or skill
  clone          Copy a creation into a new folder
  fork           Fork someone's open-source creation and clone it
  remote         Show or change the creation a folder is connected to
  dev            Run a package on this machine
  build          Build an app's browser bundle
  validate       Check a package for problems before pushing
  status         Compare your working copy with its draft and live release
  diff           Show your changes, or compare two releases
  pull           Merge the draft's changes into your working copy
  push           Upload your working copy to its online draft
  log            Show a draft's history, or a creation's releases
  restore        Put the draft back the way it was at an earlier commit
  logs           Show a published app's recent logs
  secrets        Manage sealed credentials for your package
  data           Inspect, export, or import local dev data
  service        Inspect services, test operations, and run jobs
  connectors     List the connectors your agents can use

Install Skills:
  skills         Install and use skills in Claude Code or Codex

Management:
  creations      Manage your published creations
  drafts         Manage your drafts

Your published apps:
  apps           List apps, check access and use their operations
  inspect        Show a published app's runtime state

Releases:
  outdated       Check a creation's dependencies for newer releases

Help:
  help           Show help for a command
  commands       List every command
  version        Print the CLI version

Options:
  -h, --help        Show help for terminus or a command
  -v, --version     Print the CLI version
  --json            Print machine-readable output (most commands)
  --api-base <url>  Use another Terminus API (default:
                    https://api.terminus.build)

Environment:
  TERMINUS_TOKEN     Use this access token instead of your saved login
  TERMINUS_API_BASE  Same as --api-base

Exit codes:
  0 success, 1 error, 2 bad usage, 3 not signed in, 69 Terminus unreachable,
  75 rate limited, 77 not allowed. With --json, errors are printed to stderr
  as JSON.

Run 'terminus help <command>' for details on a command.
Docs: https://www.terminus.build/docs/

Flags are parsed strictly: an option a command does not declare (or a typo such as --limt) fails before any network call.

The agentd binary

terminus dev <agent-dir> runs the agent on terminus-agentd. Installing this CLI installs it too: the four @terminus-ai/agentd-* packages are optionalDependencies carrying one prebuilt binary each, and their os and cpu fields mean npm unpacks only the one your machine can run. Once those packages are on npm there is no download on first use and nothing to authenticate against. Until their first publish, which waits for initial development to finish, agentd runs from a build of your own: lane 1, or lane 3 staged by hand.

The CLI takes the first of these it finds:

  1. --agentd PATH or $TERMINUS_AGENTD — point either at your own build;
  2. a terminus-agentd on $PATH;
  3. the installed @terminus-ai/agentd-* package.

On a platform with no build — Windows, or an unusual architecture — npm skips all four packages and installs the CLI anyway; everything except terminus dev on an agent works, and that command asks for $TERMINUS_AGENTD.

To run terminus dev against an agentd you built yourself, set $TERMINUS_AGENTD (lane 1) — or, to exercise the packaging itself, follow docs/LOCAL_DEVELOPMENT.md in the terminus-agent repo.

Sealed secrets

Outbound calls a package declares can use credentials the platform keeps sealed. Set them per linked package; values come from stdin (or $TERMINUS_SECRET_VALUE), never the command line, and can never be read back:

pbpaste | terminus secrets set OPENAI_API_KEY
terminus secrets list
terminus secrets delete OPENAI_API_KEY

Exit codes

| Code | Meaning | --json error code | | ---- | ------- | ------------------- | | 0 | success | — | | 1 | any other failure (another API 4xx/5xx, an invalid package, …) | error | | 2 | usage: unknown command or option, missing argument | usage | | 3 | not signed in: no login, an expired one, or the API answered 401 (unauthorized, session_ended) | auth | | 69 | Terminus unreachable: connection failure, timeout, 502/503/504 | unavailable | | 75 | rate limited (429; the message carries the retry hint) | rate_limited | | 77 | not allowed: the API answered 403 (forbidden, grant_required), or a closed skill's content was asked for | forbidden |

With --json, a failure writes exactly one JSON object to stderr — {"error":{"code":"…","message":"…"}} — so agents can branch on the code without parsing prose. A failure the Terminus API answered also carries its HTTP status, the API's own api_code (for example grant_required or price_confirmation_required), and its details when it gave any. Without --json the message is printed as plain text.

Reads (and any request carrying an Idempotency-Key) are retried twice, with backoff, when Terminus is briefly unavailable or the connection drops; other writes are sent once. A request has 30 seconds to be answered, plus time for what it uploads, and a download may take as long as its bytes keep arriving.

Version history

Artifacts whose listing enables source visibility (set on the web) expose their code and release history publicly — on the web (the artifact page's Code and History tabs) and locally:

terminus log @terminus/chats                 # release list with publish messages
terminus diff @terminus/chats                # latest release vs the previous one
terminus diff @terminus/chats --from 1.0.0 --to 1.2.0
terminus diff @terminus/chats --from 1 --to 3   # release numbers work too

The note you write when publishing on the web annotates the release, like a commit message. Skills use the same commands over their revision ledger. Versions follow the one grammar everywhere: "version": "1.2.0" in terminus.json (version: frontmatter for skills) names the release; a named version can never be reused for different content, and every publish must climb past the latest — bump patch, minor, or major per release, your call. terminus outdated <ref> reports every declared dependency's pin against its current version.

The CLI compiles the authored package into the backend's internal wire contract. Every runtime and source file carries its decoded byte length and SHA-256 digest, and a sorted aggregate digest identifies the complete package. The backend verifies both the file index and file bytes before accepting or materializing a release.

Presentation, visibility, and commerce settings change live on the web without a republish because they are artifact metadata, not package or release fields.

Environment

  • TERMINUS_TOKEN: a session token every command uses instead of the saved login.
  • TERMINUS_API_BASE: defaults to https://api.terminus.build; /v1 is added automatically if omitted.
  • TERMINUS_WEB_BASE: the web app browser login opens. Defaults to https://www.terminus.build.
  • TERMINUS_AGENTD: a terminus-agentd binary for terminus dev on agents (same as --agentd).
  • TERMINUS_SECRET_VALUE: the value for terminus secrets set when stdin is not used.

Platform services

terminus service inspect @terminus/brave-search --json reads the published operation contract without making a provider call. Every provider is an atomic service under the same address/operation permissions. In app source, services.search("@terminus/brave-search", input) and services.fetch("@terminus/obscura", input) infer search and fetch grants. Use literal addresses so the release can show its exact dependencies. Provider credentials stay with each service on Terminus and never enter a package; the agent tools web.search and web.fetch are routed across providers by the platform. terminus service test tests your own external service package.

Durable service jobs

Submit a managed image, document, code, or email operation with a stable key:

terminus service submit SERVICE_UUID execute --input '{"runtime":"python","code":"print(2 + 2)"}' --idempotency-key calculation-123
terminus service job JOB_UUID --wait
terminus service jobs --service-id SERVICE_UUID
terminus service cancel JOB_UUID
terminus service save JOB_UUID FILE_UUID home/result.txt

Use --file input.json instead of --input for larger inputs. File references must name a home/ path and its SHA-256 version. save creates by default; add --expected-sha256 PREVIOUS_SHA to replace exactly the version you inspected. Temporary results expire unless saved. The list command returns the latest 50 jobs; API consumers can page using the last (created_at, job_id) tuple.

Operations are generate/edit, convert, execute, and send. SDK helper calls compile to the corresponding service-operation grants during app build. Image helpers grant both generation and editing. Execution supports bounded shell/Python/JavaScript, not native packages or network access. Private jobs require the hosted runtime; the local app harness does not emulate provider side effects. --wait stops after five minutes or at a terminal/reconciling state. Reuse the same key to inspect an uncertain submission; do not generate a fresh key automatically. SMTP acceptance is distinct from inbox delivery.

Working on this CLI

npm test                            # the offline suite (no network)
npm link                            # use this checkout as `terminus`
npm uninstall -g @terminus-ai/cli   # remove the linked CLI

Pull-request CI runs npm test and npm run build and cancels superseded runs for the same PR; a change to docs alone skips them, except this README, whose help blocks the suite checks. The suite is not repeated after merge; run it locally before pushing code changes.

Releases are published from GitHub, never from a laptop: a maintainer dispatches the manual cli-publish workflow on main, which runs the suite on that commit and publishes it to npm as @terminus-ai/cli (with provenance once this repository is public). The unscoped name terminus belongs to another npm package; never publish under it.

Using installed apps from a local coding agent

The human enables access in System Settings → App access. Every app has separate Norbert and Local CLI choices: Off, Read only, or Read and write, plus personal data and selected shared spaces. The CLI cannot authorize itself. Its signed-in device identifies the account; the backend enforces app permission.

terminus apps
terminus apps notes
terminus apps notes list_pages
terminus apps notes get_page --page-id PAGE_ID
terminus apps notes append_text --page-id PAGE_ID --text "Hello"

Bare apps lists names and addresses. apps notes reports Local CLI access, data scope and operation arguments, even while Off. Denied operations exit 77 and direct the human to Settings. Use @owner/notes if names are ambiguous. No grant files are needed. Access persists until disabled or an app update requires review. Shared data uses --space <id> from the enabled spaces.

Arguments follow the operation schema: page_id becomes --page-id; strings stay strings, numbers/booleans are typed, arrays/objects accept JSON. Use --args '{"blocks":[...]}' or pipe JSON to --input - for complex input. Choose one form. Omitted arguments mean {} and never wait on stdin. Discovery prints readable text, or structured metadata with --json. Calls return JSON with the invocation ID and operation result.

Use --invocation <uuid> for safe retries and terminus apps notes --status <uuid> to inspect an invocation. After a permission change, reconcile an unknown earlier result before starting a new write. Disabling access stops later calls; committed actions remain committed.

These permissions govern app-agent operations, not the account owner's ordinary APIs. Agents sharing a CLI device use the same Local CLI setting; this is not process isolation or a replacement for an OS sandbox. Norbert uses the native app_actions tool and points the human to Settings when access is missing. The cloud agent never installs this CLI.

Earlier delegated credential files remain readable for compatibility, but changing Local CLI access in Settings revokes those earlier app grants. The CLI no longer grants authorization.

Four-app integration test

Build the SDK and each app's UI and agent bundle, then run:

PLAYWRIGHT_CORE_FROM=/path/to/package-with-playwright/package.json \
  node test/e2e/official-app-agents.mjs /path/to/official-apps

The scenario copies Chats, Notes, Teams and Strike into throwaway directories, starts real terminus dev servers, spawns real CLI processes, and opens their actual UIs in Chrome after the agent operations finish. It records assertions, checkout SHAs, dirty status and screenshots under .out/app-agent/. It uses no production data. Backend grant/broker authorization is separately tested against real Postgres in its app_spaces database lane.

Hosting a service from source

Create a service on the web and connect its address, then:

terminus init service my-service
cd my-service
npm install
terminus dev --members 2
terminus remote add @you/my-service
terminus push

Edit src/service.mjs. The SDK defines the handlers and OpenAPI; terminus dev runs the source locally with the same test people and data host used by apps. Runtime doors declared by the operation limit its local SDK access. On the platform the calling app must explicitly delegate those doors too, and its existing grants remain authoritative. Direct and agent calls have identity but do not automatically gain access to an app's data.

push uploads source, builds the Dockerfile on Terminus, and prepares a hosted draft. It does not publish a release. The manifest states a daily hosting budget and the compute rate it accepts; a rate change is refused. The beta includes three source builds per rolling day per owner, with 15-minute build deadlines. Never put credentials in source: service secrets belong to hosting settings. The platform handles image storage, HTTPS, invocation credentials and idle stop. Native dependencies belong in the Dockerfile. A public-web network profile uses the platform's guarded browser proxy; that proxy is available in hosted invocations, so validate native guarded browsing through the online draft.

To register an API you already operate, use terminus init service my-api --template external and configure its endpoint and authentication as before.

terminus dev also enforces current delivery updates (expected_version with a collection's delivery_updates policy), required relation creator bindings, and the SDK's seven-day generated retry IDs. Its SQLite cleanup follows the same expiry fence as production: an expired retry cannot become a new write after receipt cleanup. Explicit caller IDs keep their permanent retry contract.