@stratusagent/cli 0.2.5 → 0.4.0

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/README.md CHANGED
@@ -2,27 +2,53 @@
2
2
 
3
3
  Stratus Agent CLI — create always-on agents that get smarter over time. Quickly set up Stratus Agent on your machine, create new agents, connect AI providers, and configure channels.
4
4
 
5
+ <img width="2400" height="1004" alt="image" src="https://github.com/user-attachments/assets/ef16d58f-694c-406e-b201-aee61b093753" />
6
+
5
7
  ## Install
6
8
 
9
+ Needs **Node.js 22.13+** — the gateway's session store uses `node:sqlite`, which runs unflagged from 22.13. On the 23.x line that landed separately, in **23.4**, so 23.0–23.3 are too old despite the higher number. Every package declares the range, and the CLI checks it at startup, so an unsupported Node gets a line naming what it needs rather than a missing-builtin error later on.
10
+
7
11
  ```bash
8
12
  npm install -g @stratusagent/cli
9
13
  stratus setup
10
14
  ```
11
15
 
16
+ That one package is everything you need to run agents: the runtime, the
17
+ providers, the agent roster, and the always-on gateway all come with it.
18
+
19
+ ### Optional channel packages
20
+
21
+ Channels are the exception — the CLI ships **no** transport, so an install that
22
+ never touches Slack never carries the Slack SDKs (~9 MB). Add the one you want
23
+ alongside the CLI and `stratus serve` picks it up automatically:
24
+
25
+ ```bash
26
+ npm install -g @stratusagent/channel-slack
27
+ ```
28
+
29
+ If tokens are stored for an agent but the package isn't installed, `stratus
30
+ serve` says so and starts anyway, serving every other channel.
31
+
32
+ ## Setup
33
+
12
34
  `stratus setup` is the whole onboarding, as a small navigable menu:
13
35
 
14
36
  ```
15
37
  1) Providers anthropic — signed in with your Claude subscription
16
38
  2) Models default claude-opus-5 · fallback gpt-4.1-mini
17
39
  3) Agent ~/.stratus/agents/ava.md
18
- 4) Test run say hello with the current settings
19
- 5) Save & finish
40
+ 4) Channels Slack: 1 agent connected
41
+ 5) Always on stratusd runs after setup, and at every login
42
+ 6) Test run say hello with the current settings
43
+ 7) Save & finish
20
44
  ```
21
45
 
22
46
  - **Providers** — sign in to one or more. For Claude, choose how you pay: a **Claude Pro/Max subscription** (run `claude setup-token`, paste the token — runs route through the Claude Code runtime, so your plan covers usage; requires Claude Code installed and signed in. Tool runs and memory work there too, so it's the same agent as on an API key) or an **Anthropic API key**, pasted straight into the prompt (input is hidden) and checked against the live API when the endpoint supports it — a rejected key is refused; an unreachable endpoint saves the key and verifies it on your first run. OpenAI-compatible services work like API keys, including local models and proxies via a custom base URL.
23
47
  - **Models** — pick a **default** and a **fallback**, listed live from the provider APIs where possible (subscription sign-ins and offline setups fall back to the known Claude lineup). If the default model errors mid-run, the run automatically retries on the fallback — even across providers.
24
48
  - **Menus are keyboard-driven** — arrow keys (or `j`/`k`) to move, Enter to pick, digits to jump, Esc to go back.
25
49
  - **Agent** — name your agent (or accept a generated identity), describe their personality, and their soul file lands in `~/.stratus/agents/`, ready to edit.
50
+ - **Channels** — put an agent on Slack without opening a file. Pick the agent, and setup prints the app manifest with their name already filled in, walks you through the two tokens (input hidden), verifies each against Slack before accepting it, and stores them where `stratus serve` looks. The list marks who is connected; picking a connected agent offers to replace their tokens or disconnect.
51
+ - **Always on** — whether the roster keeps answering once you close the terminal. On by default, because an agent you have to remember to start is not always-on, and every Slack app you connected above stays silent until `stratusd` runs. Save & finish installs it (see [Always on](#always-on)); choose *do not run it for me* and setup removes any service it previously installed.
26
52
  - **Test run** — say hello with the current settings before saving anything.
27
53
 
28
54
  Credentials are stored in `~/.stratus/credentials.json` (owner-read-only) and settings in `~/.stratus/config.json`, so `stratus run` works from any directory afterwards. No env vars to export, no config files to hand-edit.
@@ -30,18 +56,315 @@ Credentials are stored in `~/.stratus/credentials.json` (owner-read-only) and se
30
56
  ## Usage
31
57
 
32
58
  ```bash
59
+ stratus setup # onboarding menu: providers, models, agent, channels
33
60
  stratus chat # talk — the conversation persists
34
61
  stratus chat --soul ./ava.md
35
62
  stratus run "say hello"
36
63
  stratus run --soul ./ava.md "introduce yourself"
37
64
  stratus run --provider anthropic --model claude-opus-5 "hello"
38
65
  stratus run --prompt "use the echo tool" --format json
66
+ stratus serve # stratusd: the whole roster, always on
67
+ stratus serve --idle-timeout 120 --no-events
68
+ stratus serve --approvals remote # ask a human in Slack instead of refusing
69
+ stratus service install # keep stratusd running under launchd/systemd
70
+ stratus service status
71
+ stratus logs -f # what the daemon has been doing
72
+ stratus logs --agent ava -n 200
73
+ stratus doctor # what a run would use right now, and why
39
74
  stratus agent new # create an agent (guided on a terminal)
40
75
  stratus agent new --format soul > ava.md
41
76
  stratus agents # who's on the team: souls, models, memory
42
77
  stratus dashboard # local browser dashboard
43
78
  ```
44
79
 
80
+ `stratus serve` runs the gateway in the foreground: every agent in your roster
81
+ live at once on its own provider and model, sessions in SQLite so they survive
82
+ restarts, delegation between agents, a watchdog for stalled turns, and any
83
+ installed channels connected. Ctrl+C or SIGTERM drains cleanly.
84
+
85
+ ### What the daemon will do on its own
86
+
87
+ Tools declare how much damage they could do — `safe`, `gated`, or
88
+ `dangerous` — and the daemon runs only the safe ones without asking. Anything
89
+ riskier is refused, with a line in the log saying which agent wanted what:
90
+
91
+ ```text
92
+ 09:14:36 — warning: ava: shell.run is gated and nobody is available to approve it (session slack:ava:…)
93
+ ```
94
+
95
+ That is the honest default behind a service manager, where there is no
96
+ terminal to prompt on. If somebody *is* reachable, `--approvals remote` asks
97
+ them instead — see [Asking a human](#asking-a-human-remote-approval).
98
+
99
+ A tool that declares no risk counts as `gated`, never `safe` — forgetting to
100
+ classify something should cost a prompt, not an unattended command. Every
101
+ tool that ships today (`demo.echo`, `memory.remember`, `agent.delegate`) is
102
+ `safe`, so nothing changes until you add one that isn't.
103
+
104
+ The line is *acting outside Stratus* — the filesystem, the network, another
105
+ service — not cost. Every turn spends provider tokens, including the one
106
+ that decided to call a tool, so a policy that gated on spend would have to
107
+ gate the conversation itself. Delegation stays `safe` for the same reason:
108
+ it hands work to a teammate inside the fleet, that teammate's own tool
109
+ calls face this policy again under their allowlist, and the chain is depth
110
+ bounded.
111
+
112
+ `stratus chat` and `stratus run` are unaffected: at a terminal, `--approvals`
113
+ works exactly as before (`always`, `ask`, `never`).
114
+
115
+ ### Asking a human: remote approval
116
+
117
+ In `remote` mode a gated call does not fail — the turn parks, the request is
118
+ posted to Slack with **Allow once**, **Always allow**, and **Deny**, and the
119
+ turn resumes on the answer. The question goes to the thread the turn is
120
+ happening in, so whoever is talking to the agent sees it where they already
121
+ are.
122
+
123
+ Turn it on for the daemon with `--approvals remote`, or in
124
+ `~/.stratus/config.json` so the installed service picks it up too:
125
+
126
+ ```jsonc
127
+ {
128
+ "approvals": {
129
+ "mode": "remote", // headless (default) or remote
130
+ "timeoutMs": 900000, // unanswered after 15 minutes → denied (max 2147483647)
131
+ "slackApprovers": ["U01OPS"], // who may decide, for every agent
132
+ "slackChannel": "C07OPS", // where to ask when the turn isn't in Slack
133
+ "agents": {
134
+ "ava": { "slackApprovers": ["U01DYLAN"] }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ An agent inherits the top-level route key by key, so `ava` above asks her own
141
+ approver in the shared `C07OPS` fallback channel. An explicit
142
+ `"slackApprovers": []` on an agent excludes it from the default list — that
143
+ agent's gated calls are then denied outright — while omitting the key
144
+ inherits.
145
+
146
+ **Only a config you chose is allowed to set this block** — `--config`,
147
+ `STRATUS_CONFIG`, or the global `~/.stratus/config.json`. An auto-discovered
148
+ project-local `stratus.config.json` outranks the global one for provider
149
+ settings, but it can be checked into any repository, and appointing the
150
+ people who may authorize an agent's tool calls is not something a clone gets
151
+ to do. Its `approvals` block is ignored, with a warning naming the file.
152
+
153
+ Three things are worth knowing before you turn it on:
154
+
155
+ - **Approvers are people, not places.** Posting into a channel does not make
156
+ everyone in it an approver: each request is bound to the ids configured for
157
+ that agent, and anyone else's click is refused with a notice only they see.
158
+ The request stays open for someone who may actually answer it. This matters
159
+ most for **Always allow**, which widens what the agent may do unattended.
160
+ - **An agent with no approver configured is denied immediately**, not left to
161
+ time out — `remote` with nobody listed behaves exactly like `headless`. If
162
+ no channel can ask for an agent at all (no Slack tokens for it, or
163
+ `@stratusagent/channel-slack` not installed) there is nothing to render the
164
+ request, so its gated calls wait out the timeout instead. The daemon names
165
+ those agents at startup, rather than leaving you to find out at 3am:
166
+
167
+ ```text
168
+ approvals: remote — gated calls are parked and asked in Slack (approvers set for ava)
169
+ ```
170
+ - **A parked turn survives a restart.** The daemon records what has not run
171
+ before it asks, so an approval outstanding when it stops is finished when
172
+ it starts again — the question is re-asked in Slack, the call runs on the
173
+ answer, and anything queued behind it still runs too. The re-asked request
174
+ keeps the window it started with rather than getting a fresh one, and a
175
+ wait that has already used up its `timeoutMs` is denied instead of
176
+ re-asked: downtime is not a reason to extend a security decision.
177
+ - **A turn that was mid-flight is failed, not left hanging.** Parking is the
178
+ one state a restart can resume from — a turn that stopped anywhere else
179
+ (waiting on the provider, inside a tool, or on an approval asked by an
180
+ agent billed through a Claude subscription, which is deliberately not
181
+ checkpointed) cannot be. Those sessions come back marked `failed`, with a
182
+ reason saying stratusd stopped while they were running and to send the
183
+ message again, rather than claiming to still be running forever. This is
184
+ only for an ungraceful stop: a normal restart denies what is parked and
185
+ finishes the turns those denials release before it exits.
186
+ - **Always allow lasts for the session**, not forever. It stops the same tool
187
+ asking again in the same conversation, and it is forgotten when the daemon
188
+ restarts. A durable per-agent whitelist is a narrower promise (a normalized
189
+ command scope) and ships with the shell tool that needs one.
190
+
191
+ The request shows the tool's **arguments**, not just its name — for anything
192
+ whose danger lives in what it was called with, approving a bare tool name is
193
+ approving something you cannot see. Arguments are escaped (a model-written
194
+ argument cannot mention or broadcast to the workspace through the prompt)
195
+ and truncated with a visible notice when they are long.
196
+
197
+ Requests are also denied — visibly, with a reason — when they expire, when
198
+ the turn is cancelled, when the daemon shuts down, and when a turn reaches a
199
+ gated call while the daemon is already stopping. Every one of those retracts
200
+ the buttons in Slack, so a message never keeps offering a decision with
201
+ nowhere to land.
202
+
203
+ `timeoutMs` is capped at 2147483647 (~24.8 days), the longest timer Node can
204
+ hold. A larger value is rejected at startup rather than accepted: it would
205
+ not wait longer, it would expire every approval almost immediately.
206
+
207
+ Approval buttons need the Slack app's **Interactivity** switched on. Apps
208
+ created from the manifest that `stratus setup` prints already have it; an app
209
+ created before this shipped needs it enabled once, in its App Manifest.
210
+
211
+ ## Always on
212
+
213
+ `stratus serve` stays a foreground process on purpose — debuggable, and
214
+ composable with whatever supervisor you already run. Surviving logout, crashes,
215
+ and reboots is the platform's job, so `stratus service` hands the daemon to
216
+ launchd on macOS and to systemd on Linux:
217
+
218
+ ```bash
219
+ stratus service install # write the unit, start now, start at every login
220
+ stratus service install --no-login
221
+ stratus service status
222
+ stratus service start
223
+ stratus service stop
224
+ stratus service uninstall
225
+ ```
226
+
227
+ `stratus setup` installs it for you at Save & finish unless you opt out, so
228
+ most people never run these by hand. What they get:
229
+
230
+ - **macOS** — a LaunchAgent at `~/Library/LaunchAgents/com.stratusagent.stratusd.plist`
231
+ - **Linux** — a systemd user unit at `~/.config/systemd/user/stratusd.service`
232
+
233
+ The unit runs the daemon by **absolute path** — the node binary and script of
234
+ the process that installed it, never a bare `stratus`. A service manager starts
235
+ with a minimal environment and never loads the shell profile that puts `stratus`
236
+ on your `PATH`. It stops with SIGTERM, so the gateway's drain actually runs.
237
+
238
+ A default install restarts the daemon if it crashes, but not after a clean
239
+ exit — stopping it yourself keeps it stopped. **`--no-login` gives up crash
240
+ restarts on macOS**, and that is launchd's rule rather than a choice: `KeepAlive`
241
+ implies `RunAtLoad`, so a job that must not start at login cannot ask to be
242
+ revived either. `stratus service install --no-login` says so when it finishes.
243
+ On Linux the two are independent, and `Restart=on-failure` applies either way.
244
+
245
+ `status` asks the service manager, not the unit file, whether the daemon is
246
+ alive, and exits non-zero when it isn't — so it works in a health check:
247
+
248
+ ```text
249
+ stratusd running
250
+ manager launchd
251
+ unit ~/Library/LaunchAgents/com.stratusagent.stratusd.plist
252
+ at login yes
253
+ ```
254
+
255
+ **One limit worth knowing before you rely on it.** A LaunchAgent starts at
256
+ **login, not at power-on**, and only while that user is logged in — an
257
+ unattended Mac needs automatic login turned on too. (A `LaunchDaemon` would
258
+ start without a login, but it runs as a system user, which breaks `~/.stratus`
259
+ paths and the Claude subscription token entirely, so the LaunchAgent is the
260
+ right choice.) The systemd equivalent is `loginctl enable-linger` on a machine
261
+ you don't stay logged in to. Setup says both in the menu rather than leaving
262
+ them to be discovered after a reboot.
263
+
264
+ ## Logs
265
+
266
+ Under a service manager the daemon's stdout is gone, so everything `stratus
267
+ serve` says is also written to `~/.stratus/logs/stratusd.jsonl` (owner-read-only,
268
+ rotated at 8 MB, three generations kept). That file is the record of an
269
+ overnight run, and `stratus logs` reads it from any terminal:
270
+
271
+ ```bash
272
+ stratus logs # the last 50 records
273
+ stratus logs -f # follow, across rotations
274
+ stratus logs -n 200
275
+ stratus logs --agent ava
276
+ stratus logs --session slack:ava:T01ABCDEF:C07GHIJKL:1731900000.123456
277
+ stratus logs --format json # the raw records, for jq
278
+ ```
279
+
280
+ ```text
281
+ 09:14:02 — stratusd ready — 3 agents, slack connected
282
+ 09:14:31 ava session.created [slack:ava:T01ABCDEF:C07GHIJKL:1731900000.123456]
283
+ 09:14:36 ava tool.completed tool=memory.remember ok=true [slack:ava:T01ABCDEF:C07GHIJKL:1731900000.123456]
284
+ 09:21:07 — warning: anthropic returned 529; retrying on the fallback model
285
+ ```
286
+
287
+ Session ids are the channel's own key — `channel:agent:team:conversation[:thread]`
288
+ — so the id in the last column is exactly what `--session` wants, and the same
289
+ conversation keeps it across daemon restarts.
290
+
291
+ The log is a **trace, not a second transcript**: it records that a tool ran and
292
+ that a session completed, with the tool's name, the agent, and the session id.
293
+ Prompts, replies, and tool inputs are not written — what was said lives in the
294
+ session store instead.
295
+
296
+ One exception worth knowing before you paste a log anywhere. A failed session
297
+ records the **provider's error text verbatim**, and providers routinely quote
298
+ the request that failed — so a malformed prompt can end up inside an error
299
+ message. Skim a log before sharing it, and prefer `--agent` or `--session` to
300
+ narrow it to the run you actually mean.
301
+
302
+ ### When the log is empty
303
+
304
+ A daemon that fails *before* it starts serving — a broken install, an
305
+ unreadable credentials file — never gets as far as opening the structured log,
306
+ so `stratus logs` shows nothing or shows yesterday. Those errors go to stderr,
307
+ and where stderr lands is the service manager's business, so it differs by
308
+ platform:
309
+
310
+ ```bash
311
+ tail ~/.stratus/logs/stratusd.err.log # macOS
312
+ journalctl --user-unit=stratusd.service # Linux
313
+ ```
314
+
315
+ That is where a restart loop explains itself. On macOS the LaunchAgent redirects
316
+ both streams to files, so Stratus truncates them when `serve` starts and every
317
+ five minutes while it runs — a crash loop cannot fill the disk with the same
318
+ error a million times. On Linux systemd keeps the same output in the journal
319
+ instead, which does its own rotation, so there is nothing beside the JSONL to
320
+ bound and nothing to clean up.
321
+
322
+ ## When something looks off
323
+
324
+ `stratus doctor` answers one question — *what would a run use right now, and
325
+ who decided that?* Every setting is shown with the file or environment variable
326
+ it came from, because the answer is usually that something outranks what you
327
+ thought you configured:
328
+
329
+ ```bash
330
+ stratus doctor
331
+ stratus doctor --format json
332
+ ```
333
+
334
+ ```text
335
+ Stratus Agent — what a run would use right now
336
+
337
+ provider anthropic
338
+ from ~/.stratus/agents/ava.md (soul frontmatter)
339
+ model claude-opus-5
340
+ from ~/.stratus/agents/ava.md (soul frontmatter)
341
+ soul ~/.stratus/agents/ava.md
342
+ from ~/.stratus/config.json
343
+ agent Ava (ava)
344
+
345
+ Files
346
+ config ~/.stratus/config.json
347
+ agents 1 soul file
348
+
349
+ Sign-ins
350
+ anthropic Claude subscription (Pro/Max) — runs go through the Claude Code runtime
351
+ openai not signed in
352
+
353
+ Channels
354
+ slack no agents connected
355
+ @stratusagent/channel-slack installed
356
+
357
+ 1 problem found:
358
+ ! A fallback model (gpt-4.1-mini) is configured but could not be resolved — usually
359
+ no sign-in for its provider. A failing primary model has nothing to retry on.
360
+ ```
361
+
362
+ It resolves the config exactly the way a run does rather than re-deriving the
363
+ rules, so what it prints is what you would get. It exits non-zero when it finds
364
+ a problem, and it is the fastest answer to the two most common surprises: a run
365
+ that turns out to be on the `demo` provider, and an `ANTHROPIC_API_KEY` in the
366
+ environment quietly demoting a Claude subscription sign-in to per-token billing.
367
+
45
368
  ## Agents are people
46
369
 
47
370
  An agent's identity lives in a **soul file** — markdown with frontmatter for the structured parts (name, provider, model, tool allowlists) and prose for the personality:
@@ -61,6 +384,44 @@ You are a sharp, warm generalist. Answer first, explain second...
61
384
 
62
385
  Agents remember: facts saved with the built-in `memory.remember` tool persist to `~/.stratus/memory.jsonl`, keyed to the agent — so the Ava you talk to tomorrow remembers today, from any directory.
63
386
 
387
+ **Ids are not labels.** Frontmatter may set `id:` explicitly, and it keys the
388
+ agent's sessions, memory, credentials, Slack tokens, and every per-agent path
389
+ on disk. So it has to stay one path segment, and one ordinary map key: an id
390
+ may not start with a dot or contain a slash, a backslash, a control
391
+ character, or leading or trailing whitespace, and it may not be a name every
392
+ object already answers to (`__proto__`, `constructor`, `toString`). Anything that would leave its directory is rejected when the soul
393
+ loads rather than quietly cleaned up — `id: ../../escape` is refused, not
394
+ rewritten to `escape`.
395
+
396
+ Anything else is yours. An id like `Ava_1` or `team.alpha` is unusual but
397
+ harmless, and it is already keying that agent's sessions and sign-ins, so it
398
+ is left exactly as written. Omit `id:` and one is derived from the name as a
399
+ plain slug (`ava`); a generated agent's id is also capped at 64 characters,
400
+ but a slug derived from a name you chose is used whole, because shortening
401
+ an id moves the agent it belongs to.
402
+
403
+ Creating an agent checks the id against every id the served roster holds,
404
+ not against the filenames on disk: what the roster files *declare* (a soul
405
+ at `renamed.md` can declare `id: ava`), the configured default soul even
406
+ when its file lives elsewhere, and the reserved `stratus`. A new agent gets
407
+ a suffixed id (`ava-3f9c`) rather than one that would collide. Its name
408
+ stays the one you chose.
409
+
410
+ **Two souls cannot share an id.** That is not two agents; it is one agent
411
+ whose memory and sign-ins belong to whichever file sorted first. The roster
412
+ refuses to load and names both files, `stratus serve` will not start,
413
+ `stratus doctor` reports it, and `stratus setup` → Channels offers no
414
+ agents at all — nothing is servable while the roster is ambiguous, so
415
+ connecting a Slack app would configure something that cannot run. Neither
416
+ command offers to clear "unmatched" Slack tokens in that state either: a
417
+ roster that would not load cannot prove which ids are missing, and the
418
+ tokens at risk belong to agents that are perfectly fine. (An unreadable *single* soul still degrades to a
419
+ warning — one broken file never takes the team down. A collision has no
420
+ correct winner, which is the difference.) The built-in `stratus` id is
421
+ reserved: souls claiming it are skipped — including two of them, since
422
+ neither was going to get the id, so their agreeing on it is not a collision
423
+ to refuse over.
424
+
64
425
  ## Options
65
426
 
66
427
  | Flag | Purpose |
@@ -70,12 +431,19 @@ Agents remember: facts saved with the built-in `memory.remember` tool persist to
70
431
  | `--model` | Model for real providers (anthropic default: `claude-opus-5`) |
71
432
  | `--base-url` | Override the provider API base URL |
72
433
  | `--config <file>` | Load settings from a specific config file |
73
- | `--approvals` | Tool approval mode: `always`, `ask`, or `never` |
434
+ | `--approvals` | `run`/`chat`: tool approval mode — `always`, `ask`, or `never`. `serve`: how the daemon reaches a human — `headless` (refuse gated calls) or `remote` (ask in Slack); overrides the config's `approvals.mode` |
74
435
  | `--max-turns` | Max provider turns per run (default 8) |
75
436
  | `--format` | `text` or `json` |
437
+ | `--idle-timeout` | `stratus serve`: seconds of provider silence before the watchdog aborts a turn (default 120) |
438
+ | `--no-events` | Hide the event log |
439
+ | `--no-log-file` | `stratus serve`: do not write `~/.stratus/logs/stratusd.jsonl` |
440
+ | `--no-login` | `stratus service install`: install without the start-at-login trigger |
441
+ | `-f`, `--follow` | `stratus logs`: follow the log, across rotations |
442
+ | `-n <count>` | `stratus logs`: how much backlog to print (default 50) |
443
+ | `--agent`, `--session` | `stratus logs`: show only one agent's or one session's records |
76
444
 
77
445
  Precedence: flags → `STRATUS_*` env vars → soul file hints → config file. Project-local `stratus.config.json` outranks the global `~/.stratus/config.json`; stored sign-ins are endpoint-bound and never sent to endpoints a project config selects.
78
446
 
79
- Today the CLI covers setup, chat, one-shot runs, agent creation, and the local dashboard; channels and the always-on agent service are the next milestones.
447
+ Today the CLI covers setup, chat, one-shot runs, agent creation, the always-on gateway and its service integration, logs, diagnostics, Slack, and the local dashboard. More channels — and one control API that every surface talks to — are next.
80
448
 
81
449
  Part of [Stratus Agent](https://github.com/stratuslabs/agent) — a tiny TypeScript agent runtime.
package/dist/bin.js CHANGED
@@ -1,7 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from "./index.js";
3
- const exitCode = await runCli({ argv: process.argv.slice(2) });
4
- if (exitCode !== 0) {
5
- process.exitCode = exitCode;
2
+ import { runCli, unsupportedNodeMessage } from "./index.js";
3
+ // Before anything else imports: the point is to explain the floor rather
4
+ // than to fail somewhere further in with a builtin-module error.
5
+ const unsupported = unsupportedNodeMessage(process.versions.node);
6
+ if (unsupported) {
7
+ process.stderr.write(`${unsupported}\n`);
8
+ process.exitCode = 1;
9
+ }
10
+ else {
11
+ const exitCode = await runCli({ argv: process.argv.slice(2) });
12
+ if (exitCode !== 0) {
13
+ process.exitCode = exitCode;
14
+ }
6
15
  }
7
16
  //# sourceMappingURL=bin.js.map
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC/D,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;IACnB,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE5D,yEAAyE;AACzE,iEAAiE;AACjE,MAAM,WAAW,GAAG,sBAAsB,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAClE,IAAI,WAAW,EAAE,CAAC;IAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,WAAW,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC;KAAM,CAAC;IACN,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC/D,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC9B,CAAC;AACH,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { type JsonValue, type Session, type StratusEvent } from '@stratusagent/core';
2
- import { type RuntimeConfig, type StratusProviderName } from '@stratusagent/state';
2
+ import { type CredentialProviderName, type RuntimeSelection, type RuntimeConfig, type StratusProviderName } from '@stratusagent/state';
3
+ import { type ServiceRunner } from './service.ts';
4
+ export { installService, launchdPlist, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, systemdUnit, uninstallService, SERVICE_LABEL, type ServiceRunner, type ServiceStatus, } from './service.ts';
5
+ export { createLogWriter, currentLogPosition, truncateRedirectLogs, formatLogRecord, parseLogLines, readRecentRecords, tailLog, type LogRecord, } from './logs.ts';
3
6
  export { createFileMemoryStore } from '@stratusagent/state';
4
7
  export type { StoredCredential, RuntimeConfig } from '@stratusagent/state';
5
8
  export interface CliStreams {
@@ -20,6 +23,8 @@ export interface CliEnvironment {
20
23
  dashboardAutoShutdownMs?: number;
21
24
  /** Shuts down `stratus serve` the way SIGTERM would (tests). */
22
25
  shutdownSignal?: AbortSignal;
26
+ /** Runs launchctl/systemctl. Injected so tests never touch the real one. */
27
+ serviceRunner?: ServiceRunner;
23
28
  }
24
29
  export interface CliRunOptions {
25
30
  argv: string[];
@@ -74,27 +79,93 @@ export interface ParsedAgentsCommand {
74
79
  command: 'agents';
75
80
  format: 'text' | 'json';
76
81
  }
82
+ export interface ParsedDoctorCommand {
83
+ command: 'doctor';
84
+ format: 'text' | 'json';
85
+ configPath?: string;
86
+ }
87
+ export interface ParsedServiceCommand {
88
+ command: 'service';
89
+ action: 'install' | 'uninstall' | 'status' | 'start' | 'stop';
90
+ /** install only: start automatically at login. Defaults to true. */
91
+ runAtLogin?: boolean;
92
+ /** install only: the config the managed daemon should load. */
93
+ configPath?: string;
94
+ }
95
+ export interface ParsedLogsCommand {
96
+ command: 'logs';
97
+ /** Keep streaming new records instead of exiting after the backlog. */
98
+ follow: boolean;
99
+ /** How many recent records to show before following. */
100
+ limit: number;
101
+ agentId?: string;
102
+ sessionId?: string;
103
+ format: 'text' | 'json';
104
+ }
105
+ export type ServeApprovalMode = 'headless' | 'remote';
77
106
  export interface ParsedServeCommand {
78
107
  command: 'serve';
79
108
  configPath?: string;
109
+ /** Overrides `approvals.mode` in the config file. */
110
+ approvals?: ServeApprovalMode;
80
111
  /** Watchdog idle timeout in milliseconds. 0 disables. */
81
112
  idleTimeoutMs?: number;
82
113
  events: boolean;
114
+ /** Write the structured log to ~/.stratus/logs. Defaults to true. */
115
+ logToFile?: boolean;
83
116
  }
84
117
  export interface ParsedHelpCommand {
85
118
  command: 'help';
86
119
  }
87
- export type ParsedCommand = ParsedRunCommand | ParsedChatCommand | ParsedDashboardCommand | ParsedSetupCommand | ParsedAgentNewCommand | ParsedAgentsCommand | ParsedServeCommand | ParsedHelpCommand;
120
+ export type ParsedCommand = ParsedRunCommand | ParsedChatCommand | ParsedDashboardCommand | ParsedSetupCommand | ParsedAgentNewCommand | ParsedAgentsCommand | ParsedDoctorCommand | ParsedLogsCommand | ParsedServiceCommand | ParsedServeCommand | ParsedHelpCommand;
88
121
  export interface DashboardServerHandle {
89
122
  url: string;
90
123
  close: () => Promise<void>;
91
124
  }
92
- export declare const CLI_VERSION = "0.2.5";
93
- declare const HELP_TEXT = "Stratus Agent CLI\n\nUsage:\n stratus setup\n stratus chat\n stratus chat --soul ./examples/souls/ava.md\n stratus run --prompt \"Use the demo tool\"\n stratus run \"Say hello\"\n ANTHROPIC_API_KEY=... stratus run --provider anthropic \"Say hello\"\n stratus run --soul ./examples/souls/ava.md \"Say hello\"\n echo \"Use the echo tool\" | stratus run --stdin\n STRATUS_PROVIDER=openai OPENAI_API_KEY=... stratus run \"Say hello\"\n stratus run --config ./stratus.config.json --provider openai \"Say hello\"\n stratus agents\n stratus dashboard\n stratus dashboard --port 4123 --host 0.0.0.0 --no-open\n\nCommands:\n setup Menu-driven onboarding: pick a provider, sign in (Claude\n subscription or API key), create your agent, and test it \u2014\n settings go to ~/.stratus/config.json, sign-ins to\n ~/.stratus/credentials.json (0600)\n chat Talk with your agent \u2014 the conversation persists across turns\n and remembered facts accumulate; /exit or Ctrl+C to leave\n run Execute one local Stratus Agent session\n serve Run stratusd, the always-on gateway: durable sessions, the\n whole roster live at once (each agent on its own provider),\n delegation, and a watchdog \u2014 Ctrl+C / SIGTERM drains cleanly\n (--idle-timeout <seconds>, --no-events, --config <path>)\n agent new Create an agent identity (generates a human-ish name + avatar theme)\n agents List your agents: who they are, where their souls live, what\n they run on, what they remember (also: stratus agent list)\n dashboard Start the local Stratus Agent dashboard and open it in your browser\n help Show this help message\n\nAgent options:\n --name Agent name (omit to have one generated)\n --instructions The agent's persona/instructions\n --format Output format for agent new: text, json, or soul (a ready-to-edit soul file)\n\nOptions:\n --prompt, -p Prompt to send to the local agent loop\n --stdin Read the prompt from stdin\n --provider Provider to use: anthropic, openai, or demo\n --model Model name for real providers (anthropic default: claude-opus-5, openai default: gpt-4.1-mini)\n --base-url Override the provider API base URL\n --soul Run as the agent defined by a soul file (markdown + frontmatter, see examples/souls)\n --config Config file path (run: load settings from it, setup: write it)\n --format Output format: text or json (default: text)\n --no-events Hide event-by-event progress lines in text mode\n --approvals Tool approval mode: always, ask, or never (default: always)\n --max-turns Maximum provider turns per run (default: 8)\n --port Dashboard port, defaults to an open local port\n --host Dashboard host (default: 127.0.0.1)\n --no-open Do not open the browser automatically\n --help, -h Show this help message\n\nConfig file:\n The CLI looks for ./stratus.config.json first, then a path from --config / STRATUS_CONFIG,\n then the global ~/.stratus/config.json written by `stratus setup`.\n A \"soul\" key (or STRATUS_SOUL) points at a soul file so every run uses that agent.\n Legacy STRATUSCLAW_* env vars and stratusclaw.config.json are still supported for compatibility.\n\nSoul files:\n A soul file is markdown with frontmatter (name, provider, model, tools, credentials)\n followed by the agent's persona in prose. See examples/souls/ava.md.\n";
125
+ export declare const CLI_VERSION = "0.4.0";
126
+ /**
127
+ * The range every package declares in its `engines` field. Two floors, not
128
+ * one: `node:sqlite` was unflagged in 22.13.0 on the LTS line and in 23.4.0
129
+ * on the 23.x line, so 23.0 through 23.3 are NEWER than the 22.x floor and
130
+ * still ship it behind `--experimental-sqlite`. A plain `>=22.13` admits
131
+ * exactly those releases.
132
+ */
133
+ export declare const SUPPORTED_NODE_RANGE = ">=22.13 <23 || >=23.4";
134
+ /**
135
+ * Why the CLI checks a version its manifests already declare: `engines` is
136
+ * advisory. npm and pnpm print EBADENGINE and carry on unless the user has
137
+ * turned on engine-strict, so an install on Node 20 succeeds and the floor
138
+ * is discovered later as an ERR_UNKNOWN_BUILTIN_MODULE for `node:sqlite`,
139
+ * thrown from a lazy import inside whichever command first needed the
140
+ * session store. That error names neither Node nor the version required.
141
+ *
142
+ * Returns the message to print, or undefined when the version is fine —
143
+ * including when it cannot be parsed at all, since an unrecognized build
144
+ * string is a bad reason to refuse to run.
145
+ */
146
+ export declare const unsupportedNodeMessage: (version: string) => string | undefined;
147
+ declare const HELP_TEXT = "Stratus Agent CLI\n\nUsage:\n stratus setup\n stratus chat\n stratus chat --soul ./examples/souls/ava.md\n stratus run --prompt \"Use the demo tool\"\n stratus run \"Say hello\"\n ANTHROPIC_API_KEY=... stratus run --provider anthropic \"Say hello\"\n stratus run --soul ./examples/souls/ava.md \"Say hello\"\n echo \"Use the echo tool\" | stratus run --stdin\n STRATUS_PROVIDER=openai OPENAI_API_KEY=... stratus run \"Say hello\"\n stratus run --config ./stratus.config.json --provider openai \"Say hello\"\n stratus agents\n stratus doctor\n stratus service install\n stratus service status\n stratus logs -f\n stratus logs --agent ava -n 200\n stratus dashboard\n stratus dashboard --port 4123 --host 0.0.0.0 --no-open\n\nCommands:\n setup Menu-driven onboarding: pick a provider, sign in (Claude\n subscription or API key), create your agent, connect it to\n Slack, and test it \u2014 settings go to ~/.stratus/config.json,\n sign-ins and channel tokens to ~/.stratus/credentials.json\n (0600)\n chat Talk with your agent \u2014 the conversation persists across turns\n and remembered facts accumulate; /exit or Ctrl+C to leave\n run Execute one local Stratus Agent session\n serve Run stratusd, the always-on gateway: durable sessions, the\n whole roster live at once (each agent on its own provider),\n delegation, and a watchdog \u2014 Ctrl+C / SIGTERM drains cleanly\n (--idle-timeout <seconds>, --approvals <headless|remote>,\n --no-events, --no-log-file, --config <path>); everything it\n says is also written to ~/.stratus/logs, which\n \"stratus logs\" reads\n service Keep stratusd running under launchd (macOS) or systemd\n (Linux): install, uninstall, status, start, stop.\n Installing starts it now and at every login\n (--no-login installs without the login trigger,\n --config <path> pins the daemon to one config file)\n logs Read the daemon's log from any terminal: -f to follow,\n -n <count> for backlog, --agent / --session to filter,\n --format json for the raw records\n agent new Create an agent identity (generates a human-ish name + avatar theme)\n agents List your agents: who they are, where their souls live, what\n they run on, what they remember (also: stratus agent list)\n doctor Show what a run would use right now \u2014 provider, model, soul \u2014\n and which file or environment variable decided each, then\n flag anything that would surprise you (--format json)\n dashboard Start the local Stratus Agent dashboard and open it in your browser\n help Show this help message\n\nAgent options:\n --name Agent name (omit to have one generated)\n --instructions The agent's persona/instructions\n --format Output format for agent new: text, json, or soul (a ready-to-edit soul file)\n\nOptions:\n --prompt, -p Prompt to send to the local agent loop\n --stdin Read the prompt from stdin\n --provider Provider to use: anthropic, openai, or demo\n --model Model name for real providers (anthropic default: claude-opus-5, openai default: gpt-4.1-mini)\n --base-url Override the provider API base URL\n --soul Run as the agent defined by a soul file (markdown + frontmatter, see examples/souls)\n --config Config file path (run: load settings from it, setup: write it)\n --format Output format: text or json (default: text)\n --no-events Hide event-by-event progress lines in text mode\n --approvals run/chat: tool approval mode \u2014 always, ask, or never (default: always)\n serve: how the daemon reaches a human \u2014 headless (refuse every\n gated call) or remote (ask in Slack). Default headless, or\n the config file's \"approvals.mode\"\n --max-turns Maximum provider turns per run (default: 8)\n --port Dashboard port, defaults to an open local port\n --host Dashboard host (default: 127.0.0.1)\n --no-open Do not open the browser automatically\n --help, -h Show this help message\n\nConfig file:\n The CLI looks for ./stratus.config.json first, then a path from --config / STRATUS_CONFIG,\n then the global ~/.stratus/config.json written by `stratus setup`.\n A \"soul\" key (or STRATUS_SOUL) points at a soul file so every run uses that agent.\n Legacy STRATUSCLAW_* env vars and stratusclaw.config.json are still supported for compatibility.\n\nSoul files:\n A soul file is markdown with frontmatter (name, provider, model, tools, credentials)\n followed by the agent's persona in prose. See examples/souls/ava.md.\n";
94
148
  declare const stringifyValue: (value: JsonValue) => string;
95
149
  export declare const parseCommand: (argv: string[], env?: CliEnvironment) => ParsedCommand;
96
150
  export declare const formatEvent: (event: StratusEvent) => string | null;
97
- export declare const resolveRuntimeConfig: (command: ParsedRunCommand, env?: CliEnvironment) => Promise<RuntimeConfig>;
151
+ /**
152
+ * Kept with its historical CLI signature: a parsed run command is a
153
+ * RuntimeSelection plus CLI-only fields the resolver ignores.
154
+ *
155
+ * Those fields are *permitted*, not required. Demanding a whole
156
+ * `ParsedRunCommand` made the type claim the resolver cared about
157
+ * `approvals`, `format`, and `events` — it reads none of them — so every
158
+ * caller with a selection in hand had to invent values to get past it.
159
+ */
160
+ export declare const resolveRuntimeConfig: (command: RuntimeSelection & Partial<Omit<ParsedRunCommand, keyof RuntimeSelection>>, env?: CliEnvironment) => Promise<RuntimeConfig>;
161
+ /**
162
+ * A saved subscription sign-in silently demoted to per-token billing is the
163
+ * one config surprise that costs money — an API key in the environment
164
+ * outranks the stored credential, and the run otherwise looks identical.
165
+ * Detected from the resolved config: an apiKey where the stored credential
166
+ * is a subscription token means the environment won.
167
+ */
168
+ export declare const warnOnCredentialOverride: (runtime: RuntimeConfig, streams: CliStreams, env?: CliEnvironment) => Promise<void>;
98
169
  export declare const runSingleLoop: (prompt: string, streams: CliStreams, options: {
99
170
  events?: boolean;
100
171
  runtime: RuntimeConfig;
@@ -109,8 +180,56 @@ export declare const startDashboardServer: (options: {
109
180
  host: string;
110
181
  port?: number;
111
182
  }) => Promise<DashboardServerHandle>;
183
+ export declare const slackAppManifest: (agentName: string) => string;
112
184
  export declare const runSetup: (command: ParsedSetupCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
113
185
  export declare const runAgents: (command: ParsedAgentsCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
186
+ /** One resolved setting, with the thing that decided it. */
187
+ interface DoctorSetting {
188
+ value: string;
189
+ source: string;
190
+ }
191
+ export interface DoctorReport {
192
+ configInUse?: string;
193
+ /** Config files that exist but lost to `configInUse`, nearest first. */
194
+ configShadowed: string[];
195
+ provider: DoctorSetting;
196
+ model?: DoctorSetting;
197
+ soul?: DoctorSetting;
198
+ agent?: string;
199
+ /** The model a failing run retries on, when one is configured. */
200
+ fallback?: {
201
+ provider: string;
202
+ model: string;
203
+ };
204
+ signIns: Array<{
205
+ provider: CredentialProviderName;
206
+ status: string;
207
+ }>;
208
+ slackAgents: string[];
209
+ slackPackageInstalled: boolean;
210
+ rosterCount: number;
211
+ problems: string[];
212
+ }
213
+ /**
214
+ * `stratus doctor` — what a run would resolve to right now, and which file
215
+ * or variable decided each part. Every "why is it using X" question needs
216
+ * the precedence chain, and reading it out of the source is not something
217
+ * anyone should have to do.
218
+ */
219
+ export declare const collectDoctorReport: (command: ParsedDoctorCommand, warn: (message: string) => void, env?: CliEnvironment) => Promise<DoctorReport>;
220
+ export declare const runDoctor: (command: ParsedDoctorCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
221
+ /**
222
+ * `stratus logs` — the daemon's structured log, filtered. `serve` streams
223
+ * to its own stdout, which is gone the moment it runs under a service
224
+ * manager; this reads the file it also writes, from any terminal.
225
+ */
226
+ export declare const runLogs: (command: ParsedLogsCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
227
+ /**
228
+ * `stratus service` — run the daemon under launchd or systemd, so it
229
+ * survives logout, crashes, and reboots. `serve` itself stays a plain
230
+ * foreground process; this only tells the platform how to keep it up.
231
+ */
232
+ export declare const runService: (command: ParsedServiceCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
114
233
  export declare const runAgentNew: (command: ParsedAgentNewCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
115
234
  export declare const runDashboard: (command: ParsedDashboardCommand, streams: CliStreams, env?: CliEnvironment) => Promise<number>;
116
235
  /**