@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.
- package/LICENSE +21 -0
- package/README.md +1055 -0
- package/bin/agent-discovery.mjs +71 -0
- package/bin/agent-icon.mjs +77 -0
- package/bin/agent-models.mjs +77 -0
- package/bin/agent-type.mjs +51 -0
- package/bin/agentdev.mjs +657 -0
- package/bin/app-route-script.mjs +59 -0
- package/bin/app-runtime-contract.mjs +2 -0
- package/bin/appdev-remote.mjs +346 -0
- package/bin/appdev.mjs +4446 -0
- package/bin/apps.mjs +5512 -0
- package/bin/capability-calls.mjs +437 -0
- package/bin/capsule-data.mjs +260 -0
- package/bin/client.mjs +189 -0
- package/bin/commands.mjs +1194 -0
- package/bin/dev-capsules.mjs +1599 -0
- package/bin/dev-contract.mjs +262 -0
- package/bin/dev-data.mjs +287 -0
- package/bin/dev-members.mjs +18 -0
- package/bin/dev-net.mjs +316 -0
- package/bin/dev-notification-popup.mjs +628 -0
- package/bin/dev-ports.mjs +567 -0
- package/bin/dev-server-binding.mjs +35 -0
- package/bin/dev-server-ops.mjs +1086 -0
- package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
- package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
- package/bin/dev-ui/OFL.txt +92 -0
- package/bin/dev-ui/agent-robot.webp +0 -0
- package/bin/dev-ui/app.js +5217 -0
- package/bin/dev-ui/highlight.js +195 -0
- package/bin/dev-ui/index.html +34 -0
- package/bin/dev-ui/style.css +3640 -0
- package/bin/devlint.mjs +112 -0
- package/bin/devserver.mjs +2127 -0
- package/bin/devtriggers.mjs +367 -0
- package/bin/endpoints.mjs +156 -0
- package/bin/errors.mjs +61 -0
- package/bin/files.mjs +169 -0
- package/bin/horizontal-capabilities/v1/contract.json +280 -0
- package/bin/http.mjs +500 -0
- package/bin/lint-manifests/justbash-commands.json +88 -0
- package/bin/lint-manifests/python-stdlib.json +295 -0
- package/bin/login-page.mjs +488 -0
- package/bin/schedules.mjs +664 -0
- package/bin/server-sandbox.mjs +204 -0
- package/bin/servicedev.mjs +425 -0
- package/bin/sync.mjs +357 -0
- package/bin/terminus.js +3666 -0
- package/bin/toolchain.mjs +125 -0
- package/bin/vendor/app-runtime-v1/app-host.json +124 -0
- package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
- package/bin/vendor/app-runtime-v1/doors.json +2867 -0
- package/bin/vendor/appd/node-harness.mjs +209 -0
- package/bin/vendor/appd/python-harness.py +12 -0
- package/bin/vendor/appd/server-protocol.json +84 -0
- package/bin/vendor/where.mjs +541 -0
- package/bin/versioning.mjs +72 -0
- package/bin/write-rules.mjs +398 -0
- 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.
|