@terminus-ai/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -0
  3. package/bin/agent-discovery.mjs +71 -0
  4. package/bin/agent-icon.mjs +77 -0
  5. package/bin/agent-models.mjs +77 -0
  6. package/bin/agent-type.mjs +51 -0
  7. package/bin/agentdev.mjs +657 -0
  8. package/bin/app-route-script.mjs +59 -0
  9. package/bin/app-runtime-contract.mjs +2 -0
  10. package/bin/appdev-remote.mjs +346 -0
  11. package/bin/appdev.mjs +4446 -0
  12. package/bin/apps.mjs +5512 -0
  13. package/bin/capability-calls.mjs +437 -0
  14. package/bin/capsule-data.mjs +260 -0
  15. package/bin/client.mjs +189 -0
  16. package/bin/commands.mjs +1194 -0
  17. package/bin/dev-capsules.mjs +1599 -0
  18. package/bin/dev-contract.mjs +262 -0
  19. package/bin/dev-data.mjs +287 -0
  20. package/bin/dev-members.mjs +18 -0
  21. package/bin/dev-net.mjs +316 -0
  22. package/bin/dev-notification-popup.mjs +628 -0
  23. package/bin/dev-ports.mjs +567 -0
  24. package/bin/dev-server-binding.mjs +35 -0
  25. package/bin/dev-server-ops.mjs +1086 -0
  26. package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
  27. package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
  28. package/bin/dev-ui/OFL.txt +92 -0
  29. package/bin/dev-ui/agent-robot.webp +0 -0
  30. package/bin/dev-ui/app.js +5217 -0
  31. package/bin/dev-ui/highlight.js +195 -0
  32. package/bin/dev-ui/index.html +34 -0
  33. package/bin/dev-ui/style.css +3640 -0
  34. package/bin/devlint.mjs +112 -0
  35. package/bin/devserver.mjs +2127 -0
  36. package/bin/devtriggers.mjs +367 -0
  37. package/bin/endpoints.mjs +156 -0
  38. package/bin/errors.mjs +61 -0
  39. package/bin/files.mjs +169 -0
  40. package/bin/horizontal-capabilities/v1/contract.json +280 -0
  41. package/bin/http.mjs +500 -0
  42. package/bin/lint-manifests/justbash-commands.json +88 -0
  43. package/bin/lint-manifests/python-stdlib.json +295 -0
  44. package/bin/login-page.mjs +488 -0
  45. package/bin/schedules.mjs +664 -0
  46. package/bin/server-sandbox.mjs +204 -0
  47. package/bin/servicedev.mjs +425 -0
  48. package/bin/sync.mjs +357 -0
  49. package/bin/terminus.js +3666 -0
  50. package/bin/toolchain.mjs +125 -0
  51. package/bin/vendor/app-runtime-v1/app-host.json +124 -0
  52. package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
  53. package/bin/vendor/app-runtime-v1/doors.json +2867 -0
  54. package/bin/vendor/appd/node-harness.mjs +209 -0
  55. package/bin/vendor/appd/python-harness.py +12 -0
  56. package/bin/vendor/appd/server-protocol.json +84 -0
  57. package/bin/vendor/where.mjs +541 -0
  58. package/bin/versioning.mjs +72 -0
  59. package/bin/write-rules.mjs +398 -0
  60. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,1055 @@
1
+ # @terminus-ai/cli
2
+
3
+ Lightweight Terminus tooling for developers working on their own machines
4
+ (Node.js 22.16 or newer; zero runtime dependencies).
5
+ (Inside the Terminus platform itself there is no CLI: apps are created and
6
+ staged natively by asking Norbert, and published from the Store UI.)
7
+
8
+ Installs the `terminus` command (once the package's first npm publish lands —
9
+ until then install from the repo: `npm install -g github:Terminus-Intelligence/terminus-cli`):
10
+
11
+ ```sh
12
+ npm install -g @terminus-ai/cli
13
+ ```
14
+
15
+ (One-off runs work too: `npx @terminus-ai/cli status`.)
16
+
17
+ ## Catalog search
18
+
19
+ Search the Terminus catalog as your account (run `terminus login` first):
20
+
21
+ ```sh
22
+ terminus search "browser automation"
23
+ terminus skills use "browser automation"
24
+ ```
25
+
26
+ The installed binary is also available as `terminus-cli`:
27
+
28
+ ```sh
29
+ terminus-cli search "browser automation"
30
+ terminus-cli skills use "browser automation"
31
+ ```
32
+
33
+ `skills use` searches the catalog, prompts for a numbered result, records the selected use on your
34
+ account, and prints the selected skill content for the agent to follow. In non-interactive
35
+ shells, pass an explicit selection:
36
+
37
+ ```sh
38
+ terminus-cli skills use "browser automation" --select 1
39
+ terminus-cli skills use "browser automation" --use-first
40
+ ```
41
+
42
+ When the argument is a skill address or id instead of a query, `skills use` skips search and loads
43
+ that exact skill (installed pointer skills rely on this):
44
+
45
+ ```sh
46
+ terminus-cli skills use "@zarazhangrui/frontend-slides"
47
+ ```
48
+
49
+ ## Installing skills (pointers by default)
50
+
51
+ `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
52
+ writes a small pointer stub — the skill's name and description plus instructions to load the real
53
+ content from Terminus at use time — so content stays fresh on the platform and every use is
54
+ counted. `--local` copies the full package to disk instead, like `npx skills`; local copies do
55
+ not count uses, and `terminus skills update` refreshes them. Either way the content comes through
56
+ `terminus skills use`, which serves open-source skills only: a skill whose content is closed is
57
+ used on Terminus itself, and `skills install`, `fetch`, `files`, and `file` refuse it (exit 77) —
58
+ though its owner can still `fetch` it and read its files.
59
+
60
+ ```sh
61
+ terminus skills install "@zarazhangrui/frontend-slides" --agent claude # → ./.claude/skills/
62
+ terminus skills install "@zarazhangrui/frontend-slides" --agent claude --global # → ~/.claude/skills/
63
+ terminus skills install "@zarazhangrui/frontend-slides" --agent codex # → ./.codex/skills/
64
+ terminus skills install "@zarazhangrui/frontend-slides" --local # full copy (open-source only)
65
+ terminus skills list
66
+ terminus skills update # or: terminus skills update frontend-slides
67
+ terminus skills uninstall frontend-slides
68
+ ```
69
+
70
+ Without `--agent`, install picks the agent you are running in (Codex when its environment says
71
+ so), else Claude Code. Installs are recorded in a `.terminus-install.json` manifest inside the skill
72
+ directory; `skills uninstall` only removes directories that have one. Every skill command lives
73
+ under `terminus skills`, and `terminus skills` on its own lists them. `terminus skills update`
74
+ rewrites a pointer an older CLI wrote into today's wording.
75
+
76
+ `skills update` looks every install up by the uid its manifest recorded (signed in, never by
77
+ search, so a skill that is gone is never swapped for a look-alike):
78
+
79
+ - A pointer already loads the latest instructions at each use, so update only rewrites the stub,
80
+ and only when it would read differently: a new name, description, or address, or wording from
81
+ an older terminus. The folder keeps its name. No content is delivered and no use is counted.
82
+ - A `--local` copy is downloaded again when the skill's change hash moved; that delivery counts as
83
+ a use, like the install did.
84
+ - A skill that left Terminus, or a local copy whose skill is no longer open source, is reported and
85
+ left as it is; the command then exits 1.
86
+
87
+ ## Fetching supporting files
88
+
89
+ Skills that reference scripts, templates, or other package files can be materialized into a
90
+ revision-keyed local cache (`~/.terminus/cache/<uid>@<change_hash>/`). The cache is ephemeral: safe to
91
+ delete anytime, reused until the publisher ships a new revision.
92
+
93
+ ```sh
94
+ terminus skills fetch "@zarazhangrui/frontend-slides"
95
+ terminus skills fetch "@zarazhangrui/frontend-slides" --refresh
96
+ ```
97
+
98
+ ## Scaffolding a new skill
99
+
100
+ ```sh
101
+ terminus init my-skill --name my-skill --description "One sentence on when agents should use this."
102
+ terminus validate my-skill
103
+ # make the skill on the web (Creations → New creation); it shows the address
104
+ terminus remote add @you/my-skill my-skill
105
+ terminus push my-skill
106
+ ```
107
+
108
+ Then publish it from its page on the web; `terminus push` prints the link.
109
+
110
+ ## Login
111
+
112
+ `login` checks the local session first. If you are already logged in it prints your current profile
113
+ name and `@username`.
114
+ Otherwise it opens the browser login and stores a local device session at
115
+ `~/.terminus/session.json`.
116
+
117
+ ```sh
118
+ terminus login
119
+ ```
120
+
121
+ See who you are signed in as and what is waiting for you:
122
+
123
+ ```sh
124
+ terminus account # Hi, Winston Gao (@winstongu) / Notifications: 3 · Creations: 5
125
+ terminus notifications # invitations, then unread notifications (--all adds read ones)
126
+ terminus creations # what you have published (and anything Terminus has suspended)
127
+ terminus drafts # what you have not published yet
128
+ ```
129
+
130
+ `account` refreshes your profile, then counts the two things the web keeps for you:
131
+ **Notifications** is what the notification box's badge counts (pending invitations to collaborate
132
+ or to join a chat, plus unread notifications), and **Creations** is how many apps, agents,
133
+ services, and skills you have published, the way Creations on the web lists them (not ones shared
134
+ with you, not the platform's official rows, not external skill mirrors). A count that cannot be read
135
+ drops out of the line; on the session's last day a third line says it is about to expire. Signed
136
+ out, it says how to log in and exits with status 3, so scripts can check. Its JSON output never
137
+ includes the session token. Reading notifications here does not mark them read. (`terminus status`
138
+ is the working-copy command; outside a package it points here.)
139
+
140
+ Browser login opens `https://www.terminus.build/login` with a loopback callback. After the user signs
141
+ in, the web app hands the callback a five-minute, one-time CLI login code bound to this process with
142
+ PKCE S256. The CLI exchanges it via
143
+ `POST /v1/auth/cli-login-code/exchange` for a seven-day database-backed session associated with this
144
+ device. The browser tab says "You're signed in" only after that exchange succeeds; if Terminus
145
+ refuses it, the tab shows the reason instead. The session is stored with owner-only permissions.
146
+ `terminus logout` revokes it immediately before clearing the local file. System Settings → General
147
+ → Devices on the web shows its computer, platform, client version, activity, and expiration, and
148
+ can revoke it remotely.
149
+
150
+ For a fully local auth test, run both the local API and local frontend and point the CLI at both:
151
+
152
+ ```sh
153
+ terminus login --api-base http://localhost:3001 --web-base http://localhost:3000
154
+ ```
155
+
156
+ The hosted login page intentionally refuses a localhost API target so a crafted production login
157
+ link cannot send credentials to an arbitrary service on the user's machine.
158
+
159
+ For automation (a coding agent in a container, a script), set `TERMINUS_TOKEN` to a session
160
+ token; it takes precedence over the saved login and `terminus account` names its account. There
161
+ are no API keys: every token is a device session, so it expires after seven days and is revoked
162
+ with its device.
163
+
164
+ Catalog commands (`search` and every `skill` command) require a signed-in account; there is no
165
+ anonymous lane. A 401 from Terminus usually means the login expired or was revoked on the web; the
166
+ error says to run `terminus login`.
167
+
168
+ ## How creations move (the Git and GitHub model)
169
+
170
+ Every creation starts on the web — Creations → New creation
171
+ (`https://www.terminus.build/os?open=create`) — which gives it its address, the
172
+ way a new GitHub repository gets its URL. The CLI moves files between a folder
173
+ and a creation that already exists; it never makes or publishes one (a fork is
174
+ made by Terminus, the way GitHub's Fork button makes one):
175
+
176
+ ```sh
177
+ terminus clone @you/my-app # a fresh folder, linked (git clone)
178
+ terminus remote add @you/my-app # connect a folder you already have (git remote add)
179
+ terminus push # upload your changes to its draft (git push)
180
+ terminus pull # bring the draft down (git pull)
181
+ terminus fork @someone/their-app # fork it on Terminus, then clone your fork
182
+ ```
183
+
184
+ `fork` makes free forks. When a creation charges for forking, `fork` says the
185
+ price and prints the creation's page: the fork is bought there, and then
186
+ `terminus clone` brings your fork down.
187
+
188
+ Publishing is done on the web, from the creation's page: that is where the
189
+ version is chosen and what people get changes. `terminus push` prints the page.
190
+
191
+ ## Developing a skill
192
+
193
+ One of your own skills comes down the same way an app does: `terminus clone
194
+ @you/my-skill` writes its SKILL.md and package files into a fresh directory and
195
+ records the skill's `id` in the frontmatter (`terminus remote add` records it in a
196
+ folder you already have), so a push updates that exact skill even if you rename
197
+ it. `terminus push` uploads SKILL.md and the files it references.
198
+
199
+ A skill keeps a draft beside its live version, exactly as an app does: a push
200
+ lands on the draft, and what people are using does not move until the skill is
201
+ published again on the web. `terminus status`, `diff`, `log`, `pull`, and
202
+ `restore` read a skill folder the same way they read an app's. Publishing, and
203
+ a skill's source visibility, billing, and price, are set on the web.
204
+
205
+ ## Commands
206
+
207
+ Browser apps can use a zero-build starter or a full framework project:
208
+
209
+ ```sh
210
+ terminus init app my-app --template react # positional kind shorthand
211
+ terminus init my-app --kind app --template react # vanilla | react | svelte
212
+ cd my-app && npm install
213
+ terminus remote add @you/my-app # the address the web gave it; writes id into terminus.json
214
+ npm run build # required before starting the runtime
215
+ terminus dev # terminal 1: the app runtime (/_terminus) + fixture data
216
+ npm run dev # optional terminal 2: hot reload; proxies /_terminus
217
+ terminus push # builds, validates, uploads the draft
218
+ ```
219
+
220
+ Cloning an empty creation gives a folder holding only its address;
221
+ `terminus init <kind> .` fills it in and keeps that link.
222
+
223
+ The same shorthand works for `agent`, `service`, and `skill` (including the
224
+ plural aliases). Every creation versions the same way: strict
225
+ `MAJOR.MINOR.PATCH`, starting at `0.0.1` — app, agent, and service scaffolds
226
+ carry `"version": "0.0.1"` in `terminus.json`, and skills declare `version:`
227
+ in their `SKILL.md` frontmatter. The version is chosen when you publish on the
228
+ web, and every release must be strictly above the latest; you pick the bump
229
+ (patch, minor, or major), the platform only requires that there is one.
230
+
231
+ Pushing hashes files locally and sends only the bundle index through the
232
+ Terminus API. Missing content hashes stream directly from the CLI to short-lived,
233
+ checksum-bound S3 upload URLs with bounded concurrency; repeated assets are
234
+ deduplicated. Bundle bytes are therefore not base64-expanded into one JSON
235
+ request, and the former 256-file / 4 MiB-file / 16 MiB-bundle CLI caps do not
236
+ apply. The platform retains configurable operational guardrails for abuse and
237
+ storage safety.
238
+
239
+ `terminus dev` first requires the configured bundle entry point (normally
240
+ `dist/index.html`). If it is missing, the command exits before it creates
241
+ fixture state or binds ports and tells you to run `npm run build`.
242
+ It then serves the production runtime protocol over a local fixture data space,
243
+ one port per simulated member. Plain `terminus dev` is Alan alone;
244
+ `--members 3` creates Alan, Bob, and
245
+ Carol; counts from 1 through 26 use the stable Alan-to-Zoe directory, with a
246
+ display name, bio, deterministic default avatar, and local public profile for
247
+ every account. Comma-separated handles such as `--members alice,bob` remain
248
+ supported for compatibility and receive generated profiles too. It also serves
249
+ the built bundle from disk, so `terminus build` + refresh works without vite.
250
+ The first member uses port `8868` by default and later members use consecutive
251
+ ports; `--port 5000` changes the starting port.
252
+ Every dev server (this harness, `--remote`, agents, services) listens on
253
+ 127.0.0.1 only: dev tokens, fixture data, and capabilities billed to you are
254
+ never reachable from another machine. `localhost` reaches it, and a port that
255
+ another program holds on any loopback address, 127.0.0.1, ::1, or all
256
+ interfaces, counts as taken, so a browser resolving `localhost` never lands on
257
+ that program instead. Each one also answers only requests addressed to a
258
+ loopback name (`localhost`, `*.localhost`, `127.x`, or `[::1]`, at any port):
259
+ a web page that re-points its own domain at 127.0.0.1 still sends its own
260
+ `Host`, and gets a 403 rather than a dev token. Proxies that keep a loopback
261
+ `Host`, like the scaffolded Vite proxy, pass. The app runtime (`/_terminus/*`)
262
+ and the harness's own controls (`/__terminus_dev/*`) also refuse what a page
263
+ on another site sends blind: an `Origin` that isn't a loopback page, or
264
+ `Sec-Fetch-Site: cross-site` on a request with no `Origin` (an `<img>`, a
265
+ no-cors GET). Loopback pages at any port, the Vite dev server's included,
266
+ and clients that send neither header (curl, scripts, the SDK outside a
267
+ browser) pass, and the app's pages stay open to a link from anywhere. Agent
268
+ and service dev apply the same test to their token-gated `/api` doors.
269
+ Before `--fresh` clears fixture data, the harness checks the entire requested
270
+ port range. If a listener is already present, startup reports every blocked
271
+ port and its simulated member, and says who holds it: another `terminus dev` by
272
+ its app, folder, member, PID, and uptime (every harness answers
273
+ `GET /__terminus_dev/about`), and any other program by its
274
+ command line, folder, PID, and uptime where `ps` and `lsof` can tell; otherwise
275
+ it prints a command to inspect the listener. It then says how to stop the
276
+ holder and suggests a currently free consecutive range with a copyable version
277
+ of the original command. When the holder is this same folder's harness, it
278
+ reports the app as already running instead, since two harnesses on one folder
279
+ would overwrite each other's spaces. An explicit `--port` is replaced in that
280
+ suggestion, never changed silently; when using the scaffolded Vite proxy, point
281
+ its target at the chosen port. Agent and service dev report a busy explicit
282
+ `--port` the same way, before they start the engine or import the draft;
283
+ without `--port` they take the next free port.
284
+
285
+ `--guest` adds one more port, after the members': a visitor who is not signed
286
+ in, the way an app open to guests meets one on Terminus. There the bootstrap
287
+ is the platform's guest bootstrap (`guest: true`, no installation, no user),
288
+ the app's own files are served (without the notification corner: a guest has
289
+ no account), and a capability's assets are forwarded for a capability the app
290
+ declares, fetched with no credential, as the app host fetches a guest's. Every
291
+ other `/_terminus` door answers `401 unauthorized` ("Sign in to use this (guest
292
+ mode)"), as production does, and so do the harness's own controls. The app's
293
+ `session.signIn()` opens `/_terminus/signin?return_to=<path>`: on the guest's
294
+ port that is a page listing the members, and picking one (a same-origin form
295
+ POST, never a link) makes the port that member, on the same origin, so what the
296
+ SDK kept for the guest in the browser moves into the account, and returns to the
297
+ path. A `return_to` that is not a path on this origin returns to `/`. Signing
298
+ out there (`/_terminus/logout`) makes the port a guest again. On a member's
299
+ port, sign-in goes straight back to the path.
300
+ User lookup and app-owned space/member operations are intrinsic, app-scoped SDK
301
+ calls. Optional cross-boundary authority such as `network_proxy` still refuses
302
+ locally unless the release declares it.
303
+
304
+ The fixture runtime also mirrors the production space contract. A `direct`
305
+ space is limited to two people, rejects group member actions, and persists
306
+ peer block/unblock state; a `group` persists owner/admin/editor/viewer roles (a
307
+ viewer reads the space and writes nothing in it) and enforces rename, invite,
308
+ role-change, removal, and leave permissions. Space rows carry the same
309
+ `my_role` and `capabilities` fields that apps render in production, and a
310
+ direct space names the other person as its `peer` — the person invited,
311
+ until they answer.
312
+
313
+ When one simulated member starts a chat or sends a space invitation, the
314
+ invitee's local app origin shows a Terminus-owned request popup with
315
+ **Accept**, **Decline**, and a close-for-now button. Closing hides that request
316
+ for the life of the page without accepting or declining it; reloading shows an
317
+ unresolved request again.
318
+
319
+ Being asked to talk to one person and being added to a room are different
320
+ questions, so the card is built from the space's `kind` and asks each on its
321
+ own terms. A `direct` request leads with the person — their face in
322
+ a circle, "@handle wants to chat", and **Accept**. A `group` invitation leads
323
+ with the room — its mark in a rounded square, its name, who invited you, the
324
+ people already in it, and **Join**. Pending requests are shown one at a time
325
+ with the queue depth beside them, and `GET /__terminus_dev/requests` carries
326
+ `kind`, a ready-made `title`, and the current `members` so any surface can
327
+ draw the same distinction. The control is injected into served HTML
328
+ by the local harness and lives outside `/_terminus`, so apps still cannot
329
+ consent on a user's behalf through the SDK. The JSON controls remain available
330
+ for automated tests at `GET /__terminus_dev/requests` and
331
+ `POST /__terminus_dev/requests/{id}/{accept|decline}`. Like the desk's corner,
332
+ it hears the member's account frames live — `GET
333
+ /__terminus_dev/notifications/stream`, which never counts as the member
334
+ being online — rather than polling. `dev --remote` does not
335
+ inject this simulator because production consent belongs to the Terminus
336
+ notification surfaces.
337
+
338
+ `terminus dev --remote` keeps the local `ui/` bundle but sends `/_terminus/*`
339
+ to the production runtime instead of the fixture space: the CLI uses your
340
+ `terminus login` session to mint an app session exactly as the hosted app
341
+ origin does (`POST /v1/app-runtime/authorizations` → `POST
342
+ /v1/app-runtime/session`), then proxies every runtime call with that bearer and
343
+ streams SSE feeds through. Signing out of the app (`/_terminus/logout`) revokes
344
+ that session as the hosted origin does, and opening the app again mints a new
345
+ one. It needs a published app whose address matches the package `id` and an
346
+ installation for your account. Sign-in (`/_terminus/signin`) goes straight
347
+ back to the path it names, minting a new session first when signed out. You are
348
+ acting on real data under real quotas; the SQLite harness stays the default and
349
+ `--members`, `--profiles`, `--guest`, and `--fresh` do not apply.
350
+
351
+ Use `--profiles` only when a test needs custom identities. If `--members` is
352
+ omitted, the JSON keys become the member list; omitted avatars keep their
353
+ generated defaults, and avatar paths are resolved relative to the JSON file:
354
+
355
+ ```json
356
+ {
357
+ "alice": {
358
+ "name": "Alice Chen",
359
+ "bio": "Designing calm collaboration tools.",
360
+ "avatar": "./fixtures/alice.png"
361
+ },
362
+ "bob": {
363
+ "name": "Bob Li",
364
+ "bio": "Local test account"
365
+ }
366
+ }
367
+ ```
368
+
369
+ ```sh
370
+ terminus dev . --profiles ./dev-profiles.json --fresh
371
+ ```
372
+
373
+ `ready().user`, `users.lookup/search`, and `spaces.members` then expose the
374
+ same `{ id, handle, displayName, avatarUrl, publicProfileUrl }` SDK shape as
375
+ published apps. Avatar and public-profile URLs remain same-origin and open
376
+ local fixture content. The profile file is development input only; exclude it
377
+ with `.terminusignore` if it should not be included in source sync.
378
+
379
+ The online editor and CLI share one canonical **source draft**, identified by
380
+ the committed `id` in `terminus.json` and an increasing server draft revision.
381
+ Bring a creation made on the web down with `terminus clone @you/app`, or run
382
+ `terminus remote add @you/app` inside an existing directory. Link writes that
383
+ canonical address into `terminus.json` (a service is named by it too); there is
384
+ no second local identity file. After that, from the project directory:
385
+
386
+ ```sh
387
+ terminus status # what changed here, and what the draft has that you do not
388
+ terminus diff # those changes, line by line
389
+ terminus pull # merge the draft's commits into this folder
390
+ terminus push --message "..." # send yours, as one entry in the draft's history
391
+ terminus log # the draft's commits, newest first
392
+ terminus restore <commit> # put the draft back the way it was
393
+ ```
394
+
395
+ Each push is one commit to the draft (`POST /v1/apps/{id}/draft/commits`): the
396
+ files that differ from the draft's head, the ones the folder no longer has,
397
+ and the fields `terminus.json` compiles to. The draft keeps every commit —
398
+ pushes, web-editor saves, a version chosen on the web — and its page shows
399
+ them under History, with the `--message` a push gave. A push that changes
400
+ nothing records nothing ("Everything up to date"). A service's push goes
401
+ through its import door, which compiles the OpenAPI contract and records the
402
+ import the same way.
403
+
404
+ The folder remembers which commit it matches, in `.terminus/sync.json` (git's
405
+ `origin/main`, with a `.gitignore` of its own so it stays out of your
406
+ repository). That is what makes the loop behave like git:
407
+
408
+ - **push refuses a non-fast-forward.** If the draft has commits this folder
409
+ has not pulled, push says so and stops rather than replacing them;
410
+ `--force` is the deliberate override.
411
+ - **pull merges.** A file only the draft changed is taken, one only you
412
+ changed is kept, one both changed is merged line by line, and
413
+ `terminus.json` merges key by key. What cannot be merged is written between
414
+ `<<<<<<< yours` / `>>>>>>> draft` markers and named; push waits until you
415
+ resolve it. `pull --force` replaces the folder with the draft instead.
416
+ - **restore is a revert, not a reset.** It puts the draft's files and
417
+ compiled settings back to an earlier commit as a NEW commit, so the state
418
+ it replaced stays in the history.
419
+
420
+ Git remains your local history: Terminus keeps the draft's, one commit per
421
+ push or save.
422
+
423
+ A push never publishes: open the creation's page (push prints it) and publish
424
+ there, which releases the draft as it stands.
425
+
426
+ `clone` answers for a skill you own as well. Someone else's open-source
427
+ creation clones **read-only** — the files of its latest release, with
428
+ `terminus pull` bringing newer ones in. It cannot be pushed; `terminus fork`
429
+ makes a copy in your account that can.
430
+
431
+ An app package can be as small as:
432
+
433
+ ```json
434
+ {
435
+ "kind": "app",
436
+ "id": "@publisher/chat",
437
+ "version": "0.0.1"
438
+ }
439
+ ```
440
+
441
+ Name, description, icon, listing, source visibility, and commerce settings are
442
+ set on the web and are never duplicated in this file. The app uses the stable
443
+ same-origin `/_terminus/icon` URL; the hosted worker and `terminus dev` resolve
444
+ the icon set on the web, so generated icons do not become repository assets and
445
+ no icon pull command is needed.
446
+
447
+ Developer-authored `terminus.json` files — app, agent, and service alike —
448
+ have no top-level `schema_version`. The CLI accepts older app and agent files
449
+ that include one, removes it when reconstructing a working copy, and compiles
450
+ the current authoring format into the backend's internally versioned release
451
+ manifest; a service file that still carries the line is asked to delete it.
452
+
453
+ Source discovery respects two layers. `.gitignore` defines the repository's
454
+ normal source tree. Optional `.terminusignore` removes additional files from
455
+ Terminus source sync even when Git tracks them—for example local fixtures or
456
+ internal design notes. Neither file controls the explicitly configured runtime
457
+ payload: `terminus push` runs the build and attaches that output as a derived,
458
+ content-addressed bundle bound to the resulting source revision. The platform
459
+ serves that bundle, but it is not editable state and is never pulled back into
460
+ the working tree.
461
+
462
+ Secrets, dependency trees (`node_modules/`), Git metadata, and local dev fixtures
463
+ are denied in every plane even if an ignore file or Git index includes them. A
464
+ source edit makes the previous bundle stale; publishing succeeds only after a
465
+ fresh bundle is attached for that exact revision. A differing `pull` stops
466
+ without changing files unless `--force` is supplied.
467
+
468
+ Published apps are normal multi-file websites on their own
469
+ `*.apps.terminus.build` origin. The SDK uses same-origin, app-scoped APIs for
470
+ managed collections, storage buckets, the person's own files (the private and
471
+ output zones), spaces, collaborative events, notifications, logs, and the
472
+ external services an app's code names. The user's Terminus login token is never exposed to
473
+ app JavaScript. Managed collections are defined once where app code opens them
474
+ (`db.collection(...)`) and are provisioned lazily for the installed
475
+ release; they are not duplicated in `terminus.json` (only a collection that
476
+ nothing but an automation writes is declared there, below). For conventional npm
477
+ projects the CLI also infers `npm run build` and `dist/`, so `ui` is needed only
478
+ as a nonstandard override. The manifest stays focused on canonical identity and
479
+ technical release configuration.
480
+
481
+ Every generated starter imports the SDK by name (`import { db, ready } from
482
+ "@terminus-ai/app-sdk"`), so its bundle carries only the namespaces it uses. It
483
+ is collection-first: its sample UI opens a live `items` collection, renders the
484
+ persistent IndexedDB-backed cached view, and uses optimistic `put`/`delete`
485
+ mutations. The generated `AGENTS.md` records the same architecture for future
486
+ coding agents: collections for structured state, storage buckets and files for
487
+ blobs and exported files. App code must not add an app-owned backend database;
488
+ user-owned capsules stay independent of the app release and therefore survive
489
+ forks, upgrades, and migrations.
490
+
491
+ Apps declare background work as data, not as a deployment.
492
+ `workloads.automations` is a bounded list of JSON steps using only
493
+ `collection.put`, `collection.delete`, `notification.create`,
494
+ `connector.request`, `service.invoke`, and `server.run`.
495
+ `workloads.schedules` attaches a five-field cron plus an optional IANA
496
+ `timezone` (UTC by default) to one automation, and `lifecycle.migrations` advances an
497
+ integer app-data version through the same durable job path.
498
+
499
+ An app that needs logic the browser cannot run keeps it in `server/`:
500
+ standard-library Node in `server/main.js`, exporting its ops
501
+ (`exports.ops = { top(input, { terminus, ctx }) { … } }`), or Python in
502
+ `server/main.py`. Nothing about it is written in `terminus.json`: what the
503
+ server code may do is read from the code itself, as it is for everything else
504
+ an app uses:
505
+
506
+ - An op exists because something calls it: the app, with
507
+ `server.call("<op>", input)` from `@terminus-ai/app-sdk`, or an
508
+ automation, with a `server.run` step — always by literal name. An op the
509
+ app calls runs within 10 s; an op only automations run is background-only
510
+ and runs within 120 s.
511
+ - Records are the scopes and collections the server code's literal
512
+ `terminus.records.<get|put|delete|query|update>("<scope>", "<collection>", …)`
513
+ calls name: `global` is one pool for the whole app, `installation` one
514
+ per user.
515
+ - Every op called must be one the entry exports; `terminus validate` loads
516
+ the entry to check.
517
+
518
+ `terminus.records.query` answers one page, `{documents, next_cursor}`: by
519
+ doc id, or by one top-level field with `sort: "-best"`, at most `limit`
520
+ (1–100) documents, and the next page with `after: next_cursor`.
521
+ `terminus.records.update(scope, collection, doc, fn)` is the compare-and-set
522
+ loop in one call: it reads the record, writes what `fn(current)` returns
523
+ against the version it read, and reads again on a `version_conflict`. A
524
+ refused syscall throws an `Error` whose `code` and `status` are the
525
+ platform's (`version_conflict` 409, `quota_exceeded` 402, …) — branch on
526
+ `error.code`, never on the message.
527
+
528
+ The CLI ships `server/` as release materials with `role: "server"` (runtime
529
+ code: it travels with a closed-source release and never enters the browser
530
+ bundle) and holds the entrypoint to 128 KiB and `server/` to 512 KiB.
531
+ `terminus dev` runs it the way the platform does: every `server.call` and
532
+ every `server.run` step runs the op in a fresh sandbox — the standard
533
+ library and `server/` siblings only, no network but `terminus.webFetch`, no
534
+ files outside `server/`, the op's time budget — under the platform's
535
+ records, capsule, fetch and collect rules, limits and error messages.
536
+ Records live in `.terminus/dev/` (one global pool for the app, one
537
+ installation pool per member), and each op's console output prints in the
538
+ terminal. Node server code runs locally on macOS and Linux (where one
539
+ syscall argument, such as a `records.put` document, is capped at 128 KiB —
540
+ tighter than on the platform); Python needs `python3`.
541
+
542
+ Agents do not use `workloads` at all: an agent is its own orchestrator, so a
543
+ trigger on an agent just means "wake the agent". Agents declare four
544
+ top-level prompt-form sections, each entry carrying at most a `prompt` (what
545
+ the wake says — optional, since `AGENT.md` already says what the agent does)
546
+ and `enabled`. Every standing run notifies — there is no toggle: the
547
+ notification is a compact card (the agent's identity, a short preview the
548
+ platform derives from the reply's first line, and the agent as the
549
+ click-through), and a run whose reply is empty sends nothing:
550
+
551
+ - `schedules` (≤64): a structured cadence — `{ "every": "day" | a weekday |
552
+ "month" | "30m" | "2h", "at": "HH:MM", "on_day": 1-31, "timezone": IANA
553
+ or `"user"` }` — or a raw five-field `cron` escape hatch. `"user"` means
554
+ each installer's own timezone: the platform resolves it per installation
555
+ (captured at install approval, re-resolved hourly), so "every Monday
556
+ morning" is every user's own Monday morning.
557
+ - `webhooks` (`{ name?, prompt?, description?, enabled?,
558
+ coalesce_seconds? }`, ≤16): installing provisions one capability-URL
559
+ endpoint per webhook; the person who installed the agent makes its URL in
560
+ the installation's Automations window on Terminus, which shows it once
561
+ (making a new one revokes the old), and deliveries at
562
+ `POST /v1/hooks/{token}` wake the agent with the payload.
563
+ - `watches` (`{ name?, url, pattern?, interval_minutes?, condition?,
564
+ prompt?, description?, enabled? }`, ≤8, https only, 15-minute floor,
565
+ hourly by default): the platform polls the source, extracts the optional
566
+ regex match, and only a real content change wakes the agent with the
567
+ diff. `condition` gates firing on a numeric threshold the poller
568
+ evaluates with **no model spend** — exactly one of
569
+ `{ "changed_by_pct": 2 }` (the watched number moved 2% from the value
570
+ last fired about), `{ "below": 220 }`, or `{ "above": 250 }` (crossing
571
+ the level) — so "tell me when AAPL moves 2%" costs nothing while the
572
+ market sleeps.
573
+ - `events` (`{ name?, collection, prompt?, description?, enabled?,
574
+ coalesce_seconds? }`, ≤16): wakes the agent when records in one of the
575
+ agent's own declared collections change.
576
+
577
+ `name` is optional while a section has a single entry and required to
578
+ disambiguate several. `coalesce_seconds` (0–3600) batches webhook/event
579
+ firings into fixed wall-clock windows delivered together when the window
580
+ closes. Event payloads are data, never instructions — the platform appends
581
+ them to the wake prompt inside an explicit untrusted-event-data fence. A
582
+ trigger that fails ten firings in a row pauses itself and notifies the
583
+ owner.
584
+
585
+ `terminus validate` compiles these sections into the platform's canonical
586
+ `workloads` wire shape (shared `scheduled-run` automations parametrized by
587
+ `input.prompt`), so the runtime — scheduling, fatigue, the durable
588
+ runner — is identical to an app's. Publishing a `workloads` block on a
589
+ `kind: agent` manifest is refused with a migration hint. An app example:
590
+
591
+ ```json
592
+ {
593
+ "workloads": {
594
+ "automations": [{
595
+ "name": "remind",
596
+ "steps": [{
597
+ "action": "notification.create",
598
+ "params": { "title": { "$from": "input.title" } }
599
+ }]
600
+ }],
601
+ "schedules": [{
602
+ "name": "morning",
603
+ "cron": "0 9 * * *",
604
+ "automation": "remind",
605
+ "input": { "title": "Good morning" }
606
+ }]
607
+ },
608
+ "shell": { "notifications": true },
609
+ "lifecycle": { "data_version": 1, "migrations": [] }
610
+ }
611
+ ```
612
+
613
+ `terminus validate` checks names, cron syntax, migration chains, action
614
+ permissions, and explicit `input.*` / `context.*` substitutions. `terminus
615
+ dev` simulates collection and notification actions plus job/schedule/lifecycle
616
+ SDK routes. Connector and external-service actions stay broker boundaries and
617
+ should be mocked in local app tests. Unlike collections opened only by browser
618
+ code, a collection referenced by a background automation must also be declared
619
+ in `resources.collections`; this gives the platform a release-approved schema
620
+ to enforce when no browser session is present.
621
+
622
+ Agent packages keep their prompt in `AGENT.md`, any files the agent should
623
+ know beside it (the whole package ships read-only in the agent's working
624
+ directory, at its own paths), and a small semantic tool list in
625
+ `terminus.json`.
626
+ Terminus compiles
627
+ those ids into the detailed release grant; OAuth tokens, connection ids, and
628
+ per-user resource grants never enter the package. A scheduled inbox agent can
629
+ look like this:
630
+
631
+ ```json
632
+ {
633
+ "kind": "agent",
634
+ "id": "@publisher/daily-inbox",
635
+ "version": "0.0.1",
636
+ "tools": [
637
+ {
638
+ "id": "gmail.read",
639
+ "when": "Prepare the user's daily inbox digest."
640
+ }
641
+ ],
642
+ "schedules": [{
643
+ "every": "day",
644
+ "at": "07:00",
645
+ "timezone": "America/Los_Angeles",
646
+ "prompt": "Summarize what happened in my inbox yesterday."
647
+ }]
648
+ }
649
+ ```
650
+
651
+ That one entry is the whole standing life: the agent runs every morning at
652
+ 7:00 Pacific, and a compact notification lands — the agent, one preview
653
+ line drawn from the reply's opening, and a tap-through to the agent (a
654
+ morning with nothing to say sends nothing). The prompt is optional —
655
+ without it the wake just tells the agent which schedule fired and
656
+ `AGENT.md` carries the task — and `shell.notifications` is derived from
657
+ declaring a trigger, never hand-written.
658
+
659
+ Run `terminus connectors gmail` to inspect the currently supported Gmail tool
660
+ ids, operations, and scopes. Then `terminus validate .` and `terminus dev .`
661
+ — local agent dev: the loop runs on your machine (bash/python execute
662
+ natively inside a workspace-write sandbox over the lane workspace in
663
+ `.terminus/dev/agent/`), while the system prompt and tool definitions
664
+ compile server-side from your manifest, so local runs always match what
665
+ publishing produces. It works immediately after init — no creation or link
666
+ required — and `terminus dev . --prompt "…" [--json]` gives scripts and coding
667
+ agents a one-shot lane.
668
+
669
+ An agent chooses its models in one `models` section. `terminus init agent`
670
+ writes the house configuration, which is also what an agent that says nothing
671
+ gets: every model on Terminus, starting on GPT-5.6 Luna.
672
+
673
+ ```json
674
+ "models": { "default": "openai:gpt-5.6-luna", "mode": "all" }
675
+ ```
676
+
677
+ `"mode": "selective"` with an `"available"` list narrows the choice, and
678
+ `"mode": "default"` keeps the agent on its default alone.
679
+
680
+ An agent's `type` says who can read its conversations. Left out, it is
681
+ `"default"`: each conversation stays private to the person having it.
682
+ `"observed"` shares every conversation with the agent's maintainers, and
683
+ people accept a notice saying so before their first message. The
684
+ inspector's Type switch writes the key for you.
685
+
686
+ ```json
687
+ "type": "observed"
688
+ ```
689
+
690
+ Declared triggers are exercisable locally before publishing: the
691
+ inspector's Schedules section shows every trigger as a card — the cadence in
692
+ words, the wake prompt, whether it notifies — with add, edit, and remove
693
+ writing `terminus.json` in place (webhooks also accept real deliveries at
694
+ the local `POST /hooks/{token}` URL the webhook's card shows; point curl or a
695
+ tunnel at it. As on the platform, the token is the whole capability: it
696
+ admits a delivery under any `Host`, and each webhook keeps its token in
697
+ `.terminus/dev/agent/webhook-tokens.json`, so a tunnel survives restarts;
698
+ delete the file to rotate them), and
699
+ `terminus dev . --trigger <name> [--payload file.json] [--json]` is the
700
+ headless lane. A simulated firing builds the trigger's input the way the
701
+ platform builds it, runs the wake as a real local turn behind the same
702
+ untrusted-event fence, and reports the notification (or its skip, when the
703
+ run produced no text) on the transcript. Connect your own Gmail account in Terminus to
704
+ exercise `gmail.read` live; before publishing, `terminus dev . --remote`
705
+ runs the same conversation on the platform's real engine and real isolate.
706
+ Publishing and installing the release binds the same `gmail.read`
707
+ declaration to each visitor's own Gmail connection; the hosted schedule then
708
+ runs on that visitor's durable VM-free session.
709
+
710
+ Service artifacts describe atomic callable APIs. `runtime.kind` says how the
711
+ platform reaches the implementation: `external` names an HTTP endpoint (yours,
712
+ platform-hosted, or a third party's — the `authentication` mode and explicit
713
+ commerce fields carry that difference, while `provider_relationship`,
714
+ `upstream_cost_bearer`, `payout_recipient`, and `health_path` default to the
715
+ first-party posture `owned`/`none`/`publisher`/`/health/ready`); `builtin`
716
+ names an engine implemented inside the Terminus backend and carries nothing
717
+ else.
718
+
719
+ ```sh
720
+ terminus init my-converter --kind service # describe an existing HTTPS API
721
+ # set runtime.base_url and the auth mode in terminus.json; state
722
+ # provider_relationship/cost bearer only when they differ from the defaults
723
+ terminus validate my-converter # validates the manifest + OpenAPI
724
+ # make the service on the web (Creations → New creation), then connect the folder
725
+ terminus remote add @you/my-converter my-converter # names the package after its address
726
+ # deploy the API on AWS/Azure/etc; test updates the service's private draft
727
+ terminus service test my-converter document.convert --file sample.pdf
728
+ terminus dev my-converter # serves the package's dev/ test page
729
+ terminus push my-converter # uploads the draft; publish it on the web
730
+
731
+ ```
732
+
733
+ `terminus dev <service-dir>` starts a local test session on its own port: it
734
+ refreshes the linked service's draft from the folder, serves the package's own `dev/index.html` (plus siblings in
735
+ `dev/`), and relays invocations through the free draft-test lane — a direct
736
+ signed call for public `terminus_signed` endpoints, the gateway test door
737
+ otherwise. The page contract is host-provided: the harness injects
738
+ `window.__SERVICE_DEV__ = { service, operations, invoke }`, and the page calls
739
+ `invoke(operationId, { input, query, bytes, idempotencyKey })` — `input` for
740
+ JSON operations, `bytes` (an ArrayBuffer or Blob) with `query` parameters for
741
+ binary ones — and receives the test-lane result `{ status, content_type,
742
+ body, body_base64, … }`. Never hard-code a transport: `dev/` files upload
743
+ with the release (`role: "dev"`, outside every runtime plane), and whatever
744
+ serves the page later provides its own `invoke`.
745
+
746
+ For an external service, `openapi.json` is the machine contract and `README.md`
747
+ is human/agent guidance. The contract lives at the repository root by default;
748
+ `terminus.json` states an `openapi` path only when the file deliberately lives
749
+ elsewhere, and a package with neither is refused. The manifest otherwise
750
+ contains the unscoped package name, version, description, endpoint,
751
+ provider/cost relationship, payout policy, and authentication mode. API-key or
752
+ OAuth secrets are referenced by environment-variable name and uploaded
753
+ separately; their values never enter the package or release. The backend is the
754
+ authoritative parser: each test/publish uploads the raw package, compiles
755
+ OpenAPI, and creates or updates the private service draft and binding. Terminus
756
+ retains the publisher address, price, icon, listing, release/verification state,
757
+ and encrypted secrets. Terminus-signed tests receive a short-lived scoped JWT
758
+ and call the manifest endpoint directly; other auth modes use the broker. No
759
+ credits move during tests, and there is no arbitrary URL override.
760
+
761
+ Terminus does not execute service-package code. Browser apps call only the
762
+ external operations declared in their release; the broker supplies scoped
763
+ identity, enforces consent and billing, and keeps provider credentials out of
764
+ browser JavaScript.
765
+
766
+ Bare `terminus` (or `terminus help`) prints its version and the everyday commands, grouped, each
767
+ with what it does on one line (`<word>` is a placeholder, `[ ]` is optional):
768
+
769
+ ```text
770
+ Terminus v0.0.1, created by Terminus Intelligence
771
+
772
+ Account:
773
+ terminus login Open your browser and sign in
774
+ terminus account Your account's station
775
+ terminus notifications Check your notifications
776
+
777
+ Explore:
778
+ terminus search <query> Search the Terminus catalog
779
+
780
+ Developing:
781
+ terminus init <kind> [<dir>] Start a new creation
782
+ terminus clone <address> [<dir>] Download a creation to edit
783
+ terminus remote add <address> [<dir>] Connect a folder to a creation
784
+ terminus dev [<dir>] Mock your creation locally
785
+ terminus validate [<dir>] Check a package for problems
786
+ terminus status [<dir>] Compare your copy with Terminus
787
+ terminus pull [<dir>] Merge the draft's changes into yours
788
+ terminus push [<dir>] Upload your changes as a draft
789
+ terminus logs <app> Show a published app's logs
790
+
791
+ Mock it locally so your coding agent can test end to end — an app:
792
+ terminus clone @you/app && cd app
793
+ npm run build # apps are built before they run
794
+ terminus dev --members 3 # three at once; --fresh to start empty
795
+ terminus push --message "Rooms can be renamed"
796
+
797
+ An agent, which has no build step:
798
+ terminus clone @you/agent && cd agent
799
+ terminus dev # a chat page; --remote for real data
800
+ terminus dev --prompt "Two lines on today's inbox"
801
+ terminus push --message "Sharper summary"
802
+
803
+ Every other key, --port included, and what each kind runs: terminus help dev
804
+
805
+ Install Skills:
806
+ terminus skills Install skills in Claude Code or Codex
807
+
808
+ Management:
809
+ terminus creations Manage your published creations
810
+ terminus drafts Manage your drafts
811
+
812
+ Further help:
813
+ terminus commands List every command
814
+ terminus help [<command>] Show how to use a command
815
+ https://www.terminus.build/docs/
816
+ ```
817
+
818
+ `terminus commands` lists every command, then the options, environment
819
+ variables, and exit codes they share; `terminus help <command>` and
820
+ `terminus <command> --help` show one command's usage, options, and examples, the
821
+ way `brew search --help` does. `terminus skills` on its own lists every skills
822
+ command, each with what it does. A command given the wrong arguments prints that
823
+ same help, then `Error: Invalid usage: …` naming what was wrong (`--json` keeps
824
+ just that line). Both blocks here are the CLI's exact output, kept in sync by a
825
+ test.
826
+
827
+ ```text
828
+ Usage: terminus <command> [<arguments>] [<options>]
829
+
830
+ Account:
831
+ login Sign in to Terminus in your browser
832
+ logout Sign out and revoke this device's session
833
+ account Your account's station
834
+ notifications Check your notifications
835
+
836
+ Explore:
837
+ search Search the catalog
838
+
839
+ Developing:
840
+ init Create a new app, agent, service, or skill
841
+ clone Copy a creation into a new folder
842
+ fork Fork someone's open-source creation and clone it
843
+ remote Show or change the creation a folder is connected to
844
+ dev Run a package on this machine
845
+ build Build an app's browser bundle
846
+ validate Check a package for problems before pushing
847
+ status Compare your working copy with its draft and live release
848
+ diff Show your changes, or compare two releases
849
+ pull Merge the draft's changes into your working copy
850
+ push Upload your working copy to its online draft
851
+ log Show a draft's history, or a creation's releases
852
+ restore Put the draft back the way it was at an earlier commit
853
+ logs Show a published app's recent logs
854
+ secrets Manage sealed credentials for your package
855
+ data Inspect, export, or import local dev data
856
+ service Inspect services, test operations, and run jobs
857
+ connectors List the connectors your agents can use
858
+
859
+ Install Skills:
860
+ skills Install and use skills in Claude Code or Codex
861
+
862
+ Management:
863
+ creations Manage your published creations
864
+ drafts Manage your drafts
865
+
866
+ Your published apps:
867
+ inspect Show a published app's runtime state
868
+
869
+ Releases:
870
+ outdated Check a creation's dependencies for newer releases
871
+
872
+ Help:
873
+ help Show help for a command
874
+ commands List every command
875
+ version Print the CLI version
876
+
877
+ Options:
878
+ -h, --help Show help for terminus or a command
879
+ -v, --version Print the CLI version
880
+ --json Print machine-readable output (most commands)
881
+ --api-base <url> Use another Terminus API (default:
882
+ https://api.terminus.build)
883
+
884
+ Environment:
885
+ TERMINUS_TOKEN Use this access token instead of your saved login
886
+ TERMINUS_API_BASE Same as --api-base
887
+
888
+ Exit codes:
889
+ 0 success, 1 error, 2 bad usage, 3 not signed in, 69 Terminus unreachable,
890
+ 75 rate limited, 77 not allowed. With --json, errors are printed to stderr
891
+ as JSON.
892
+
893
+ Run 'terminus help <command>' for details on a command.
894
+ Docs: https://www.terminus.build/docs/
895
+ ```
896
+
897
+ Flags are parsed strictly: an option a command does not declare (or a typo
898
+ such as `--limt`) fails before any network call.
899
+
900
+ ## The agentd binary
901
+
902
+ `terminus dev <agent-dir>` runs the agent on `terminus-agentd`. Installing
903
+ this CLI installs it too: the four `@terminus-ai/agentd-*` packages are
904
+ `optionalDependencies` carrying one prebuilt binary each, and their `os`
905
+ and `cpu` fields mean npm unpacks only the one your machine can run. Once
906
+ those packages are on npm there is no download on first use and nothing to
907
+ authenticate against. Until their first publish, which waits for initial
908
+ development to finish, agentd runs from a build of your own: lane 1, or
909
+ lane 3 staged by hand.
910
+
911
+ The CLI takes the first of these it finds:
912
+
913
+ 1. `--agentd PATH` or `$TERMINUS_AGENTD` — point either at your own build;
914
+ 2. a `terminus-agentd` on `$PATH`;
915
+ 3. the installed `@terminus-ai/agentd-*` package.
916
+
917
+ On a platform with no build — Windows, or an unusual architecture — npm
918
+ skips all four packages and installs the CLI anyway; everything except
919
+ `terminus dev` on an agent works, and that command asks for
920
+ `$TERMINUS_AGENTD`.
921
+
922
+ To run `terminus dev` against an agentd you built yourself, set
923
+ `$TERMINUS_AGENTD` (lane 1) — or, to exercise the packaging itself, follow
924
+ `docs/LOCAL_DEVELOPMENT.md` in the terminus-agent repo.
925
+
926
+ ## Sealed secrets
927
+
928
+ Outbound calls a package declares can use credentials the platform keeps sealed. Set them per
929
+ linked package; values come from stdin (or `$TERMINUS_SECRET_VALUE`), never the command line, and
930
+ can never be read back:
931
+
932
+ ```sh
933
+ pbpaste | terminus secrets set OPENAI_API_KEY
934
+ terminus secrets list
935
+ terminus secrets delete OPENAI_API_KEY
936
+ ```
937
+
938
+ ## Exit codes
939
+
940
+ | Code | Meaning | `--json` error code |
941
+ | ---- | ------- | ------------------- |
942
+ | `0` | success | — |
943
+ | `1` | any other failure (another API 4xx/5xx, an invalid package, …) | `error` |
944
+ | `2` | usage: unknown command or option, missing argument | `usage` |
945
+ | `3` | not signed in: no login, an expired one, or the API answered 401 (`unauthorized`, `session_ended`) | `auth` |
946
+ | `69` | Terminus unreachable: connection failure, timeout, 502/503/504 | `unavailable` |
947
+ | `75` | rate limited (429; the message carries the retry hint) | `rate_limited` |
948
+ | `77` | not allowed: the API answered 403 (`forbidden`, `grant_required`), or a closed skill's content was asked for | `forbidden` |
949
+
950
+ With `--json`, a failure writes exactly one JSON object to stderr —
951
+ `{"error":{"code":"…","message":"…"}}` — so agents can branch on the code
952
+ without parsing prose. A failure the Terminus API answered also carries its
953
+ HTTP `status`, the API's own `api_code` (for example `grant_required` or
954
+ `price_confirmation_required`), and its `details` when it gave any. Without
955
+ `--json` the message is printed as plain text.
956
+
957
+ Reads (and any request carrying an Idempotency-Key) are retried twice, with
958
+ backoff, when Terminus is briefly unavailable or the connection drops; other
959
+ writes are sent once. A request has 30 seconds to be answered, plus time for
960
+ what it uploads, and a download may take as long as its bytes keep arriving.
961
+
962
+ ## Version history
963
+
964
+ Artifacts whose listing enables source visibility (set on the web) expose their
965
+ code and release history publicly — on the web (the artifact page's Code and History
966
+ tabs) and locally:
967
+
968
+ ```sh
969
+ terminus log @terminus/chats # release list with publish messages
970
+ terminus diff @terminus/chats # latest release vs the previous one
971
+ terminus diff @terminus/chats --from 1.0.0 --to 1.2.0
972
+ terminus diff @terminus/chats --from 1 --to 3 # release numbers work too
973
+ ```
974
+
975
+ The note you write when publishing on the web annotates the release, like a
976
+ commit message. Skills use the same commands over their revision ledger. Versions follow the
977
+ one grammar everywhere: `"version": "1.2.0"` in `terminus.json` (`version:`
978
+ frontmatter for skills) names the release; a named version can never be
979
+ reused for different content, and every publish must climb past the latest —
980
+ bump patch, minor, or major per release, your call. `terminus outdated <ref>`
981
+ reports every declared dependency's pin against its current version.
982
+
983
+ The CLI compiles the authored package into the backend's internal wire
984
+ contract. Every runtime and source file carries its decoded byte length and SHA-256 digest, and a sorted
985
+ aggregate digest identifies the complete package. The backend verifies both
986
+ the file index and file bytes before accepting or materializing a release.
987
+
988
+ Presentation, visibility, and commerce settings change live on the web without
989
+ a republish because they are artifact metadata, not package or release fields.
990
+
991
+ ## Environment
992
+
993
+ - `TERMINUS_TOKEN`: a session token every command uses instead of the saved login.
994
+ - `TERMINUS_API_BASE`: defaults to `https://api.terminus.build`; `/v1` is added automatically if omitted.
995
+ - `TERMINUS_WEB_BASE`: the web app browser login opens. Defaults to `https://www.terminus.build`.
996
+ - `TERMINUS_AGENTD`: a `terminus-agentd` binary for `terminus dev` on agents (same as `--agentd`).
997
+ - `TERMINUS_SECRET_VALUE`: the value for `terminus secrets set` when stdin is not used.
998
+
999
+ ## Platform services
1000
+
1001
+ `terminus service inspect @terminus/brave-search --json` reads the published
1002
+ operation contract without making a provider call. Every provider is an atomic
1003
+ service under the same address/operation permissions. In app source,
1004
+ `services.search("@terminus/brave-search", input)` and
1005
+ `services.fetch("@terminus/obscura", input)` infer `search` and `fetch` grants.
1006
+ Use literal addresses so the release can show its exact dependencies. Provider
1007
+ credentials stay with each service on Terminus and never enter a package; the
1008
+ agent tools `web.search` and `web.fetch` are routed across providers by the
1009
+ platform. `terminus service test` tests your own external service package.
1010
+
1011
+ ### Durable service jobs
1012
+
1013
+ Submit a managed image, document, code, or email operation with a stable key:
1014
+
1015
+ ```sh
1016
+ terminus service submit SERVICE_UUID execute --input '{"runtime":"python","code":"print(2 + 2)"}' --idempotency-key calculation-123
1017
+ terminus service job JOB_UUID --wait
1018
+ terminus service jobs --service-id SERVICE_UUID
1019
+ terminus service cancel JOB_UUID
1020
+ terminus service save JOB_UUID FILE_UUID home/result.txt
1021
+ ```
1022
+
1023
+ Use `--file input.json` instead of `--input` for larger inputs. File references
1024
+ must name a `home/` path and its SHA-256 version. `save` creates by default; add
1025
+ `--expected-sha256 PREVIOUS_SHA` to replace exactly the version you inspected.
1026
+ Temporary results expire unless saved. The list command returns the latest 50
1027
+ jobs; API consumers can page using the last `(created_at, job_id)` tuple.
1028
+
1029
+ Operations are `generate`/`edit`, `convert`, `execute`, and `send`. SDK helper
1030
+ calls compile to the corresponding service-operation grants during app build.
1031
+ Image helpers grant both generation and editing. Execution supports bounded
1032
+ shell/Python/JavaScript, not native packages or network access. Private jobs
1033
+ require the hosted runtime; the local app harness does not emulate provider
1034
+ side effects. `--wait` stops after five minutes or at a terminal/`reconciling`
1035
+ state. Reuse the same key to inspect an uncertain submission; do not generate
1036
+ a fresh key automatically. SMTP acceptance is distinct from inbox delivery.
1037
+
1038
+ ## Working on this CLI
1039
+
1040
+ ```bash
1041
+ npm test # the offline suite (no network)
1042
+ npm link # use this checkout as `terminus`
1043
+ npm uninstall -g @terminus-ai/cli # remove the linked CLI
1044
+ ```
1045
+
1046
+ Pull-request CI runs `npm test` and `npm run build` and cancels superseded runs
1047
+ for the same PR; a change to docs alone skips them, except this README, whose
1048
+ help blocks the suite checks. The suite is not repeated after merge; run it
1049
+ locally before pushing code changes.
1050
+
1051
+ Releases are published from GitHub, never from a laptop: a maintainer
1052
+ dispatches the manual `cli-publish` workflow on `main`, which runs the suite on
1053
+ that commit and publishes it to npm as `@terminus-ai/cli` (with provenance once
1054
+ this repository is public). The unscoped name `terminus` belongs to another
1055
+ npm package; never publish under it.