@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 +79 -72
- package/dist/api/workspace.js +41 -5
- package/dist/index.js +20 -7
- package/dist/protocol/permissions.js +0 -4
- package/dist/protocol/runner.js +194 -18
- package/dist/protocol/shell.js +5 -2
- package/dist/runHeadless.js +10 -2
- package/dist/session.js +69 -3
- package/dist/ui/App.js +80 -19
- package/dist/ui/components.js +2 -3
- package/dist/ui/headless.js +7 -4
- package/dist/ui/notices.js +20 -0
- package/dist/ui/phase.js +28 -0
- package/dist/ui/render.js +3 -0
- package/dist/ui/slashCommands.js +6 -6
- package/package.json +1 -1
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
|
|
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
|
|
27
|
-
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
+
### Mode: what Rivine Code works on
|
|
64
64
|
|
|
65
|
-
Each Mode
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
97
|
-
|
|
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
|
|
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
|
|
107
|
-
`~/.aws`, service-account JSON. These ask on *read* too
|
|
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
|
|
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
|
|
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
|
|
116
|
-
session start and appended to at the end of each turn. It writes without asking
|
|
117
|
-
|
|
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` `/
|
|
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** (
|
|
144
|
-
reopen those pickers. Shift+Tab toggles Manual ⇄ Bypass. Ctrl+C
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
248
|
-
Every call asks
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
`
|
|
319
|
-
anything on, and the headless renderer **refuses** rather than auto-approves
|
|
320
|
-
so
|
|
321
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
370
|
-
| `RIVINE_NO_PTY` | unset
|
|
371
|
-
| `RIVINE_ALLOW_OUTSIDE_PROJECT` | unset
|
|
372
|
-
| `RIVINE_ASCII` | unset
|
|
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
|
|
392
|
-
`edit`, `execute
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
package/dist/api/workspace.js
CHANGED
|
@@ -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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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>', `
|
|
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
|
|
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
|
-
|
|
302
|
-
|
|
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 ??
|
|
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':
|