@beunec/rivine 0.2.1 → 0.3.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/README.md CHANGED
@@ -16,35 +16,35 @@ files it reads and writes are **the ones already on your disk**, in the
16
16
  directory you started it in, open in your editor, tracked by your git.
17
17
 
18
18
  Nothing is uploaded. There is no "attach your repo" step and no sync. The
19
- engine asks for a file, this client reads it, and the answer goes back — one
19
+ engine asks for a file, this client reads it, and the answer goes back, one
20
20
  file at a time, only the ones it actually opens. A 40,000-file monorepo costs
21
21
  nothing to start working in, because nothing is copied anywhere.
22
22
 
23
23
  ## What it does
24
24
 
25
25
  - Reads, writes and edits files in your working directory
26
- - Runs commands — your shell, your PATH, your nvm/pyenv shims
27
- - Delegates to the same sub-agent lanes as the web engine: frontend, backend,
28
- integration, security review, CI/CD, deploy
26
+ - Runs commands: your shell, your PATH, your nvm/pyenv shims
27
+ - Runs as one mode, the same five as the web engine: Frontend, Backend,
28
+ Full Stack, Plan/Ask, or Security
29
29
  - Asks you a question when a decision is genuinely yours to make
30
30
  - Keeps a per-project memory in `.rivine/`, so tomorrow's session knows what
31
31
  yesterday's did
32
32
 
33
33
  ## Running commands
34
34
 
35
- Commands run in your own shell, in your working directory, streaming — you see
35
+ Commands run in your own shell, in your working directory, streaming. You see
36
36
  output as it is produced, not in one block when the command exits. A two-minute
37
37
  install and a hung one no longer look the same.
38
38
 
39
39
  Where the platform allows it the command gets a real terminal (a PTY), which
40
40
  means `isatty` is true and tools behave the way they do when you run them by
41
41
  hand: colour, progress bars, and the interactive prompts some of them open with.
42
- If a command asks you something, type the answer and press Enter — while a
42
+ If a command asks you something, type the answer and press Enter. While a
43
43
  command is running your input goes to the command, not to the agent. `esc`
44
44
  stops it; the agent sees the non-zero exit and decides what to do about it.
45
45
 
46
46
  The terminal comes from `node-pty`, an optional dependency. If it cannot be
47
- installed or a PTY cannot be allocated, commands still run and still stream —
47
+ installed or a PTY cannot be allocated, commands still run and still stream,
48
48
  just without a terminal. Nothing fails. `RIVINE_NO_PTY=1` forces the pipe path.
49
49
 
50
50
  What you see and what the model is sent are deliberately different. Escape codes
@@ -56,15 +56,15 @@ cost more per turn than not having one.
56
56
  ## Mode and Rate
57
57
 
58
58
  Two choices, both required, and neither is inferred from what you type. There is
59
- no router — no keyword matcher, no classifier, no extra model turn that reads
59
+ no router: no keyword matcher, no classifier, no extra model turn that reads
60
60
  your request and picks a lane. A wrong guess sends a database migration to the
61
61
  frontend agent and you never see the decision that was made for you.
62
62
 
63
- ### Mode — which agent runs
63
+ ### Mode: what Rivine Code works on
64
64
 
65
- Each Mode is one standalone agent that owns the task end to end: it searches the
66
- codebase, reads what it needs, writes across as many files as the change
67
- requires, runs the build and tests, and fixes what the terminal reports.
65
+ Each Mode owns the task end to end: it searches the codebase, reads what it
66
+ needs, writes across as many files as the change requires, runs the build and
67
+ tests, and fixes what the terminal reports.
68
68
 
69
69
  | Mode | For | How to choose |
70
70
  |---|---|---|
@@ -74,47 +74,52 @@ requires, runs the build and tests, and fixes what the terminal reports.
74
74
  | `simple` | Plan, ask, document, refresh, ship | `4`, `-m simple`, `--plan` |
75
75
  | `security` | Find, verify, and patch vulnerabilities | `5`, `-m security` |
76
76
 
77
- `simple` is the only Mode with live web access — its work is the work that
77
+ `simple` is the only Mode with live web access. Its work is the work that
78
78
  depends on the world outside the repository. The build Modes are measured on
79
79
  whether the software runs.
80
80
 
81
- ### Rate — how edits apply
81
+ ### Rate: how edits apply
82
82
 
83
83
  The engine runs on our servers. The decision about what happens to your machine
84
- is made **here**, on your machine, before anything runs — never on our servers.
84
+ is made **here**, on your machine, before anything runs, never on our servers.
85
85
 
86
86
  | Rate | Applies without asking | How to choose |
87
87
  |---|---|---|
88
88
  | `manual` | Reads and read-only commands. Asks before every write, edit, or delete | `1`, `--rate manual` (default) |
89
89
  | `bypass` | Writes and ordinary commands inside this project | `2`, `--rate bypass`, `--bypass` |
90
90
 
91
- Three things ask in **both** Rates, because they are what you would want to see
91
+ Three things still ask in `manual`, because they are what you would want to see
92
92
  even having said "just do it": credentials, anything outside the project
93
93
  directory, and commands that are hard to undo (`rm -rf`, `git push --force`, a
94
- deploy).
94
+ deploy). `bypass` does not ask. A path outside the project is refused there,
95
+ not approved.
95
96
 
96
- The first interactive session asks for Mode, then Rate. `-m …` and `--rate …`
97
- skip the questions because you already answered them. Shift+Tab toggles the
98
- Rate.
97
+ The first interactive session asks for Mode, then Rate, before the prompt
98
+ opens, including for a one-shot `rivine "..."`. `-m …` and `--rate …` skip the
99
+ question you already answered on the command line. Shift+Tab toggles the Rate.
100
+ `/mode` and `/rate` reopen either picker later.
101
+
102
+ Without a terminal to ask on (a pipe, CI, another agent driving this), `-m`
103
+ is required and the run stops with the list rather than guessing.
99
104
 
100
105
  What stays gated:
101
106
 
102
107
  - **Anything outside the project.** Refused in `bypass` (set
103
- `RIVINE_ALLOW_OUTSIDE_PROJECT=1` if you mean it). Other modes ask. The jail
108
+ `RIVINE_ALLOW_OUTSIDE_PROJECT=1` if you mean it). Other Rates ask. The jail
104
109
  resolves symlinks first, so a link pointing out of the directory is caught
105
110
  rather than followed.
106
- - **Credentials** — every mode except `bypass`. `.env`, `*.pem`, `~/.ssh`,
107
- `~/.aws`, service-account JSON. These ask on *read* too — reading one puts
111
+ - **Credentials:** every Rate except `bypass`. `.env`, `*.pem`, `~/.ssh`,
112
+ `~/.aws`, service-account JSON. These ask on *read* too. Reading one puts
108
113
  a live secret into a model prompt.
109
- - **Commands that are hard to undo** — every mode except `bypass`. `rm -rf`,
114
+ - **Commands that are hard to undo:** every Rate except `bypass`. `rm -rf`,
110
115
  `git push`, `--force`, `git reset --hard`, `sudo`, `npm publish`, any
111
116
  deploy, `terraform apply`.
112
- - **Batch writes** — every mode except `bypass`. Judged by their most
117
+ - **Batch writes:** every Rate except `bypass`. Judged by their most
113
118
  dangerous entry, so a safe first file cannot clear a whole batch.
114
119
 
115
- One exemption: `.rivine/` — the agent's own notebook for this project, seeded at
116
- session start and appended to at the end of each turn. It writes without asking
117
- (except in `plan` mode), for the same reason `.git/` does. It ships with its own
120
+ One exemption: `.rivine/`, the agent's own notebook for this project, seeded at
121
+ session start and appended to at the end of each turn. It writes without asking,
122
+ for the same reason `.git/` does. It ships with its own
118
123
  `.gitignore`.
119
124
 
120
125
  ## Commands
@@ -138,10 +143,11 @@ rivine mcp login <name> sign in to a remote MCP server
138
143
  rivine --output-format json one JSON object on stdout, for CI
139
144
  ```
140
145
 
141
- In a session: type `/` and press Tab. `/help` `/agent` `/mode` `/plan` `/accept`
146
+ In a session: type `/` and press Tab. `/help` `/mode` `/rate` `/plan` `/accept`
142
147
  `/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. Startup asks for
143
- **Mode** (which agent) then **Rate** (Manual or Bypass). `/agent` and `/mode`
144
- reopen those pickers. Shift+Tab toggles Manual ⇄ Bypass. Ctrl+C quits cleanly.
148
+ **Mode** (what Rivine Code works on) then **Rate** (Manual or Bypass); `/mode`
149
+ and `/rate` reopen those pickers. Shift+Tab toggles Manual ⇄ Bypass. Ctrl+C
150
+ quits cleanly.
145
151
 
146
152
  `/diff` shows what changed in the working tree. `/pr` prepares a pull request
147
153
  through your own `gh` CLI. It **never pushes without being asked**: on a
@@ -152,11 +158,11 @@ form that writes to the remote. It also refuses on `main`, and refuses if
152
158
  There is no `/compact` or `/summarize`. Context is compacted on the server as
153
159
  part of how the engine runs, so there is nothing here to trigger by hand.
154
160
  Opening `rivine` in a directory you have used before continues that
155
- directory's thread — earlier turns are still in context, and the session says
161
+ directory's thread. Earlier turns are still in context, and the session says
156
162
  so on startup.
157
163
 
158
164
  Answers are single keys, and the keys are always printed: `y` allow, `n` skip,
159
- `a` allow all this session, `esc` cancel. Choices are numbered — press the
165
+ `a` allow all this session, `esc` cancel. Choices are numbered. Press the
160
166
  number. Nothing here makes you type a word to answer a question.
161
167
 
162
168
  `a` is offered for ordinary writes only. Credentials, paths outside the project
@@ -166,7 +172,7 @@ ones where per-operation consent still means something.
166
172
  ### Without a terminal
167
173
 
168
174
  Piped, redirected, in CI, or driven by another tool, there is no keyboard to
169
- read — so `rivine -p "..."` switches to a plain renderer with no cursor
175
+ read, so `rivine -p "..."` switches to a plain renderer with no cursor
170
176
  movement and no repaint, safe to redirect to a log.
171
177
 
172
178
  Anything that would prompt is **refused**, not auto-approved, and the refusal
@@ -182,7 +188,7 @@ rivine -m fullstack --rate bypass -p "..." # CI, on a machine you accept chang
182
188
 
183
189
  Authorization Code with PKCE over a loopback redirect. Your browser opens to
184
190
  `aselius.beunec.com/cli/authorize`, which recognises your existing session and
185
- asks you to confirm this terminal, then redirects to `127.0.0.1:1455` — a port
191
+ asks you to confirm this terminal, then redirects to `127.0.0.1:1455`, a port
186
192
  only a process on your own machine can listen on.
187
193
 
188
194
  Device code (`--device`) exists for headless machines and is not the default,
@@ -199,19 +205,19 @@ is in use. In CI, set `RIVINE_TOKEN` and neither is touched.
199
205
  **It is the same Aselius account, not a second one.** Auth0 only answers "who
200
206
  is this person"; your cloudhandle, your BCID and your wallet live in Beunec's
201
207
  own systems and are looked up from that answer. The terminal signs in through a
202
- different OAuth client than the website, which is why you approve it separately
203
- — but it resolves to the same identity, the same taskspaces and the same
208
+ different OAuth client than the website, which is why you approve it separately,
209
+ but it resolves to the same identity, the same taskspaces and the same
204
210
  credits. Sign in as the account you use on aselius.beunec.com and it is
205
211
  literally the same you.
206
212
 
207
213
  `rivine status` shows a `Renews:` line. If it says `no`, your session cannot
208
- refresh itself and ends when the token expires — sign in again when it does.
214
+ refresh itself and ends when the token expires. Sign in again when it does.
209
215
  That depends on a tenant setting, not on anything you can change locally.
210
216
 
211
217
  ## MCP servers
212
218
 
213
219
  Rivine hosts MCP servers **from your machine**, the same way Claude Code and
214
- Codex do — not from our cloud.
220
+ Codex do, not from our cloud.
215
221
 
216
222
  ```
217
223
  rivine mcp add playwright npx @playwright/mcp@latest
@@ -223,7 +229,7 @@ rivine mcp remove <name>
223
229
 
224
230
  In a session, `/mcp` shows what connected and what didn't.
225
231
 
226
- **Why on your machine.** A stdio server *is* a local process — `npx`, `uvx`, a
232
+ **Why on your machine.** A stdio server *is* a local process: `npx`, `uvx`, a
227
233
  docker container, a bridge to your own database. Nothing running in our cloud
228
234
  can spawn it. And for remote servers, calling from here means your tokens stay
229
235
  on your machine instead of in our infrastructure, and tool results never transit
@@ -235,7 +241,7 @@ as it asks it to read a file.
235
241
 
236
242
  | File | For |
237
243
  |---|---|
238
- | `<project>/.rivine/mcp.json` | servers this repo needs — committable, so teammates get them |
244
+ | `<project>/.rivine/mcp.json` | servers this repo needs, committable, so teammates get them |
239
245
  | `~/.rivine/mcp.json` | servers you use everywhere (`--user`) |
240
246
 
241
247
  Project wins on a name collision. Secrets are never stored: `env` values written
@@ -244,9 +250,8 @@ remote server's token is referenced by variable name (`--token-env`), never by
244
250
  value.
245
251
 
246
252
  **Permissions.** An MCP tool can read a database, drive a browser or post to an
247
- API — closer to running a command than reading a file, so it is gated like one.
248
- Every call asks; `plan` mode refuses them outright, because a tool's schema does
249
- not say whether it writes. "Allow all this session" is scoped to **one server**:
253
+ API, closer to running a command than reading a file, so it is gated like one.
254
+ Every call asks, because a tool's schema does not say whether it writes. "Allow all this session" is scoped to **one server**:
250
255
  approving Playwright never silently approves a database server that connects
251
256
  later.
252
257
 
@@ -255,20 +260,20 @@ startup. The others still connect and their tools are still offered.
255
260
 
256
261
  **Only the tools a turn needs.** Rivine does not put every connected tool in
257
262
  front of the model on every turn. Servers are scored against your prompt, and an
258
- unrelated request gets none of them — asking to fix a typo does not hand the
263
+ unrelated request gets none of them. Asking to fix a typo does not hand the
259
264
  model a browser. Naming a server always includes it (`use playwright to…`), and
260
265
  a server comes in as a whole set, because a browser you can open but not drive
261
266
  is worse than no browser. The session line tells you which servers were used and
262
267
  which were skipped.
263
268
 
264
- That is not only a cost control, though it is one — every tool definition is
269
+ That is not only a cost control, though it is one. Every tool definition is
265
270
  tokens you pay for on every turn. It is mostly a quality control: a model
266
271
  holding a capability tends to find a reason to use it.
267
272
 
268
273
  ### Remote servers that need a sign-in
269
274
 
270
275
  `--token-env` covers servers with a static API key. For hosted servers that
271
- authenticate per user, there is nothing to paste — the credential does not
276
+ authenticate per user, there is nothing to paste. The credential does not
272
277
  exist until you consent:
273
278
 
274
279
  ```bash
@@ -279,18 +284,18 @@ rivine mcp logout notion # forget it again
279
284
 
280
285
  This opens your browser, runs the standard OAuth loopback flow against the
281
286
  server's **own** authorization server, and stores the token in your OS
282
- keychain — never in `.rivine/mcp.json`, which is a file people commit. If a
287
+ keychain, never in `.rivine/mcp.json`, which is a file people commit. If a
283
288
  server publishes no OAuth metadata the command says so rather than guessing at
284
289
  endpoints.
285
290
 
286
291
  ## In CI
287
292
 
288
293
  ```bash
289
- rivine --output-format json -m bypass -p "fix the failing type errors"
294
+ rivine --output-format json -m fullstack --rate bypass -p "fix the failing type errors"
290
295
  ```
291
296
 
292
- In `json` mode **stdout carries exactly one object** and everything else —
293
- tokens, notices, command output — goes to stderr, so the stream stays safe to
297
+ In `json` mode **stdout carries exactly one object** and everything else,
298
+ tokens, notices, and command output, goes to stderr, so the stream stays safe to
294
299
  pipe into `jq`:
295
300
 
296
301
  ```json
@@ -298,7 +303,7 @@ pipe into `jq`:
298
303
  ```
299
304
 
300
305
  Exit codes are `0` success, `1` error, `2` out of credits (also if you run
301
- `rivine` with no prompt and no terminal), and `3` nothing could be done —
306
+ `rivine` with no prompt and no terminal), and `3` nothing could be done:
302
307
  every operation needed approval and there was no terminal to give it. `3`
303
308
  exists because that run still costs a turn: without it the step would go
304
309
  green having changed nothing. The result object carries `refused` alongside
@@ -313,12 +318,14 @@ There is a composite action in `action.yml`:
313
318
  RIVINE_TOKEN: ${{ secrets.RIVINE_TOKEN }}
314
319
  with:
315
320
  prompt: fix the failing type errors
321
+ mode: fullstack
316
322
  ```
317
323
 
318
- `mode` defaults to `bypass` because a workflow has no terminal to approve
319
- anything on, and the headless renderer **refuses** rather than auto-approves —
320
- so any other mode stops at the first write. Run it on a checkout you are
321
- willing to have changed, and review the diff before merging.
324
+ `rate` defaults to `bypass` because a workflow has no terminal to approve
325
+ anything on, and the headless renderer **refuses** rather than auto-approves,
326
+ so `manual` stops at the first write. `mode` is required: frontend, backend,
327
+ fullstack, simple, or security. Run it on a checkout you are willing to have
328
+ changed, and review the diff before merging.
322
329
 
323
330
  One turn has to finish inside the engine's **15-minute ceiling**. Local
324
331
  commands stream and are not the constraint; a single request that needs longer
@@ -327,19 +334,19 @@ than that is. Split the work across steps rather than raising the timeout.
327
334
  ## What the server stores
328
335
 
329
336
  File operations travel through a mailbox table so the engine and your terminal
330
- can hand work back and forth. Those rows carry real source — a write payload is
337
+ can hand work back and forth. Those rows carry real source. A write payload is
331
338
  the file being written.
332
339
 
333
340
  Each session generates an **AES-256 key on your machine**. It is sent once per
334
341
  turn in the request body and held in memory for the length of that turn. It is
335
- never written to disk here and never written to storage there — so no database
342
+ never written to disk here and never written to storage there, so no database
336
343
  we keep, and no backup, replica or restore of one, contains the material needed
337
344
  to open those rows. Rows are also deleted as soon as the engine has read them.
338
345
 
339
346
  **This is not end-to-end encryption, and we will not call it that.** The engine
340
347
  has to read your code to reason about it, so plaintext exists in server memory
341
348
  while a turn is running. What the session key removes is every path that goes
342
- through storage rather than through a live process — a restored snapshot, a
349
+ through storage rather than through a live process: a restored snapshot, a
343
350
  backup bucket, a replica, a support query. That is the larger and longer-lived
344
351
  half of the exposure, not all of it.
345
352
 
@@ -366,10 +373,10 @@ deployments:
366
373
  |---|---|
367
374
  | `RIVINE_API_URL` | `https://workspace.beunec.com` |
368
375
  | `RIVINE_SITE_URL` | `https://aselius.beunec.com` |
369
- | `RIVINE_TOKEN` | unset — a pre-issued token for CI |
370
- | `RIVINE_NO_PTY` | unset — set to `1` to run commands without a PTY |
371
- | `RIVINE_ALLOW_OUTSIDE_PROJECT` | unset — set to `1` only if `bypass` should be allowed to leave the project directory |
372
- | `RIVINE_ASCII` | unset — set to `1` for ASCII glyphs (narrow panes, no Unicode) |
376
+ | `RIVINE_TOKEN` | unset. A pre-issued token for CI |
377
+ | `RIVINE_NO_PTY` | unset. Set to `1` to run commands without a PTY |
378
+ | `RIVINE_ALLOW_OUTSIDE_PROJECT` | unset. Set to `1` only if `bypass` should be allowed to leave the project directory |
379
+ | `RIVINE_ASCII` | unset. Set to `1` for ASCII glyphs (narrow panes, no Unicode) |
373
380
 
374
381
  Auth0 parameters are fetched from `GET /api/cli/config` at sign-in, so this
375
382
  package carries no tenant configuration and a client-id change never requires
@@ -388,8 +395,8 @@ reads ./src/app.ts
388
395
  ◀─── SSE tokens ──────── answer streams back
389
396
  ```
390
397
 
391
- The engine's file operations arrive as distinct verbs — `read`, `write`,
392
- `edit`, `execute` — rather than as shell commands. That is what lets this
398
+ The engine's file operations arrive as distinct verbs, `read`, `write`,
399
+ `edit`, and `execute`, rather than as shell commands. That is what lets this
393
400
  client tell a read from a write and apply the right policy, and it is why
394
401
  nothing here needs Python on your machine.
395
402
 
@@ -411,15 +418,15 @@ tokens faster than any terminal can usefully repaint:
411
418
  - Nothing is ever wider than your window. Paths truncate in the middle, keeping
412
419
  both ends; command output keeps its tail, because that is where the error is.
413
420
  - Colour degrades: TrueColor, 256, or 16, and `NO_COLOR` is honoured. Colour is
414
- never the only signal — every line kind has its own glyph, so the transcript
421
+ never the only signal. Every line kind has its own glyph, so the transcript
415
422
  reads correctly in monochrome and for colour-blind readers. `RIVINE_ASCII=1`
416
423
  swaps the glyphs and wordmark for pure ASCII.
417
- - Exits restore the cursor and raw-mode state, including the abnormal ones — a
424
+ - Exits restore the cursor and raw-mode state, including the abnormal ones: a
418
425
  kill, or an uncaught error. You should never need to run `reset`.
419
426
 
420
427
  ## Your identity
421
428
 
422
- The status bar shows your **cloudhandle** — the identity you recognise. Your
429
+ The status bar shows your **cloudhandle**, the identity you recognise. Your
423
430
  account key is shown only as `••••••••••••` and the CLI never receives its
424
431
  value.
425
432
 
@@ -437,7 +444,7 @@ leak.
437
444
  - File reads are capped at 2 MB. Larger files are reported rather than
438
445
  truncated silently.
439
446
  - `ls`, `glob` and `grep` skip `node_modules`, `.git`, `dist`, `.next`,
440
- `target`, `venv` and similar — for cost, not secrecy.
447
+ `target`, `venv` and similar, for cost, not secrecy.
441
448
  - Command output is capped at 120,000 characters, keeping the tail, because a
442
449
  failing build puts its error at the end.
443
450
 
@@ -453,11 +460,11 @@ npm run typecheck
453
460
  This package ships compiled JavaScript only; the TypeScript sources are not
454
461
  redistributed. For maintainers working in the repository, the interesting
455
462
  code is in `src/protocol/`: `permissions.ts` decides,
456
- `fsExecutor.ts` acts, `runner.ts` coordinates one turn. `src/ui/` only renders
457
- — nothing with a consequence lives there.
463
+ `fsExecutor.ts` acts, `runner.ts` coordinates one turn. `src/ui/` only renders.
464
+ Nothing with a consequence lives there.
458
465
 
459
466
  ---
460
467
 
461
- Rivine Code CLI — licensed and owned by **Beunec Technologies, Inc.**
468
+ Rivine Code CLI, licensed and owned by **Beunec Technologies, Inc.**
462
469
  Proprietary software. Not open source. Use is governed by the licence
463
470
  agreement accompanying it.
@@ -28,16 +28,26 @@ async function authHeaders() {
28
28
  export function projectKey(cwd) {
29
29
  return createHash('sha256').update(cwd).digest('hex').slice(0, 40);
30
30
  }
31
+ export class OnboardingRequired extends Error {
32
+ url;
33
+ constructor(message, url) {
34
+ super(message);
35
+ this.url = url;
36
+ this.name = 'OnboardingRequired';
37
+ }
38
+ }
31
39
  export async function bootstrap(cwd, label) {
32
40
  const url = new URL(`${apiUrl()}/api/cli/bootstrap`);
33
41
  url.searchParams.set('project_key', projectKey(cwd));
34
42
  url.searchParams.set('label', label);
35
43
  const res = await fetch(url, { headers: await authHeaders() });
36
44
  if (!res.ok) {
37
- const detail = await res
38
- .json()
39
- .then((b) => String(b?.error ?? b?.detail ?? '').trim())
40
- .catch(() => '');
45
+ const body = await res.json().catch(() => null);
46
+ const reason = body?.detail ?? body?.error;
47
+ if (res.status === 403 && reason && typeof reason === 'object' && reason.code === 'onboarding_required') {
48
+ throw new OnboardingRequired(String(reason.message || 'Finish setting up your Aselius account to use Rivine Code.'), String(reason.onboarding_url || 'https://aselius.beunec.com/onboarding'));
49
+ }
50
+ const detail = typeof reason === 'string' ? reason.trim() : '';
41
51
  if (res.status === 401) {
42
52
  throw new Error('Your session is no longer valid. Run `rivine login`.');
43
53
  }
@@ -120,12 +130,20 @@ export async function resumeTurn(args) {
120
130
  decision: args.approved ? 'approved' : 'rejected',
121
131
  feedback: args.feedback ?? '',
122
132
  modified_args: args.modifiedArgs ?? null,
133
+ code_mode: args.codeMode ?? null,
134
+ code_rate: args.codeRate ?? null,
123
135
  cli_session_key: args.sessionKey ?? null,
124
136
  cli_session_id: args.sessionId,
125
137
  }),
126
138
  });
127
139
  if (!res.ok) {
128
- throw new Error(`Could not resume the turn (HTTP ${res.status}).`);
140
+ const detail = await res
141
+ .json()
142
+ .then((b) => String(b?.detail ?? b?.error ?? '').trim())
143
+ .catch(() => '');
144
+ throw new Error(detail
145
+ ? `Could not resume the turn (HTTP ${res.status}): ${detail}`
146
+ : `Could not resume the turn (HTTP ${res.status}).`);
129
147
  }
130
148
  return (await res.json());
131
149
  }
@@ -139,6 +157,24 @@ export async function closeSession(sessionId) {
139
157
  catch {
140
158
  }
141
159
  }
160
+ export async function getTurnStatus(workspaceId, chatSpaceId) {
161
+ const res = await fetch(`${apiUrl()}/api/workspaces/${workspaceId}/chats/${chatSpaceId}/turn-status`, {
162
+ headers: await authHeaders(),
163
+ });
164
+ if (!res.ok)
165
+ throw new Error(`turn-status returned ${res.status}`);
166
+ return (await res.json());
167
+ }
168
+ export async function cancelTurn(workspaceId, chatSpaceId) {
169
+ try {
170
+ await fetch(`${apiUrl()}/api/workspaces/${workspaceId}/chats/${chatSpaceId}/turns/cancel`, {
171
+ method: 'POST',
172
+ headers: await authHeaders(),
173
+ });
174
+ }
175
+ catch {
176
+ }
177
+ }
142
178
  export async function fetchCredits(creditsUrl) {
143
179
  if (!creditsUrl)
144
180
  return null;
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * redistribution or derivative works.
11
11
  */
12
12
  import { readFileSync } from 'node:fs';
13
+ import { realpath } from 'node:fs/promises';
13
14
  import { dirname, join, resolve as resolvePath } from 'node:path';
14
15
  import { fileURLToPath } from 'node:url';
15
16
  import { render } from 'ink';
@@ -18,6 +19,7 @@ import { Command } from 'commander';
18
19
  import { App } from './ui/App.js';
19
20
  import { runHeadless } from './runHeadless.js';
20
21
  import { isInteractiveTerminal, installTeardown } from './ui/headless.js';
22
+ import { emitNotice } from './ui/notices.js';
21
23
  import { login, logout, status } from './auth/session.js';
22
24
  import { openBrowser } from './auth/pkce.js';
23
25
  import { apiUrl, siteUrl } from './config.js';
@@ -262,14 +264,15 @@ function collect(value, previous) {
262
264
  program
263
265
  .argument('[prompt...]', 'send one prompt and exit')
264
266
  .option('-p, --print <prompt>', 'send one prompt, print the answer, and exit')
265
- .option('-m, --mode <mode>', `which agent runs: ${CODE_MODES.join(' | ')}`)
267
+ .option('-m, --mode <mode>', `what Rivine Code works on: ${CODE_MODES.join(' | ')}`)
266
268
  .option('--rate <rate>', `how edits apply: ${RATES.join(' | ')}`)
267
269
  .option('--plan', 'shorthand for --mode simple (Plan/Ask Only)')
268
270
  .option('--bypass', 'never ask for approval — for CI only, on a machine you are willing to have changed')
269
271
  .option('-C, --cwd <dir>', 'run against a different directory')
270
272
  .option('--output-format <format>', 'text | json — json puts one object on stdout and everything else on stderr', 'text')
271
273
  .action(async (prompt, options) => {
272
- const cwd = options.cwd ? resolvePath(options.cwd) : process.cwd();
274
+ const requested = options.cwd ? resolvePath(options.cwd) : process.cwd();
275
+ const cwd = await realpath(requested).catch(() => requested);
273
276
  const oneShot = options.print ?? (prompt.length ? prompt.join(' ') : undefined);
274
277
  const { mode, explicit: modeExplicit } = resolveMode(options);
275
278
  const codeMode = resolveCodeMode(options);
@@ -298,19 +301,29 @@ program
298
301
  return;
299
302
  }
300
303
  if (format === 'json' && oneShot) {
301
- process.exitCode = await runHeadless({
302
- cwd, mode, codeMode: codeMode ?? 'fullstack', prompt: oneShot, format,
303
- });
304
+ if (!codeMode) {
305
+ console.error('\n --mode is required with --output-format json.\n' +
306
+ ` Choose one of: ${CODE_MODES.join(', ')}\n` +
307
+ ' e.g. rivine -m backend --output-format json -p "add tests for auth.ts"\n');
308
+ process.exitCode = 2;
309
+ return;
310
+ }
311
+ process.exitCode = await runHeadless({ cwd, mode, codeMode, prompt: oneShot, format });
304
312
  return;
305
313
  }
306
314
  const { waitUntilExit } = render(React.createElement(App, {
307
315
  cwd,
308
316
  initialMode: mode,
309
317
  askForMode: !modeExplicit && !oneShot,
310
- initialCodeMode: codeMode ?? (oneShot ? 'fullstack' : undefined),
318
+ initialCodeMode: codeMode ?? undefined,
311
319
  oneShot,
312
320
  }), { exitOnCtrlC: false });
313
- const removeTeardown = installTeardown(() => { });
321
+ const removeTeardown = installTeardown(() => { }, {
322
+ onRejection: (err) => {
323
+ const message = err?.message ?? String(err);
324
+ emitNotice(`Background task failed: ${message}`);
325
+ },
326
+ });
314
327
  try {
315
328
  await waitUntilExit();
316
329
  }
@@ -47,7 +47,6 @@ export function parseCodeMode(raw) {
47
47
  return table[key] ?? null;
48
48
  }
49
49
  export const PERMISSION_MODES = ['plan', 'default', 'acceptEdits', 'bypass'];
50
- export const CYCLE_MODES = ['plan', 'default', 'acceptEdits'];
51
50
  export function isBatchable(category) {
52
51
  return category === 'write' || category === 'mcp';
53
52
  }
@@ -251,9 +250,6 @@ export function nextMode(current) {
251
250
  ? rateToPermissionMode('manual')
252
251
  : rateToPermissionMode('bypass');
253
252
  }
254
- export function nextRate(current) {
255
- return current === 'bypass' ? 'manual' : 'bypass';
256
- }
257
253
  export function modeName(mode) {
258
254
  switch (mode) {
259
255
  case 'plan':