@beunec/rivine 0.2.0 → 0.3.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
@@ -60,11 +60,11 @@ 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
  |---|---|---|
@@ -93,9 +93,14 @@ 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
94
  deploy).
95
95
 
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.
96
+ The first interactive session asks for Mode, then Rate, before the prompt
97
+ opens — including for a one-shot `rivine "..."`, which used to assume Full
98
+ Stack. `-m …` and `--rate …` skip the question you already answered on the
99
+ command line. Shift+Tab toggles the Rate. `/mode` and `/rate` reopen either
100
+ 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
 
@@ -138,9 +143,10 @@ 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` `/mode` `/plan` `/accept`
142
- `/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. `/mode` opens the
143
- permission picker. Shift+Tab cycles plan / default / accept edits. Ctrl+C
146
+ In a session: type `/` and press Tab. `/help` `/mode` `/rate` `/plan` `/accept`
147
+ `/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. Startup asks for
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
144
150
  quits cleanly.
145
151
 
146
152
  `/diff` shows what changed in the working tree. `/pr` prepares a pull request
@@ -120,12 +120,20 @@ export async function resumeTurn(args) {
120
120
  decision: args.approved ? 'approved' : 'rejected',
121
121
  feedback: args.feedback ?? '',
122
122
  modified_args: args.modifiedArgs ?? null,
123
+ code_mode: args.codeMode ?? null,
124
+ code_rate: args.codeRate ?? null,
123
125
  cli_session_key: args.sessionKey ?? null,
124
126
  cli_session_id: args.sessionId,
125
127
  }),
126
128
  });
127
129
  if (!res.ok) {
128
- throw new Error(`Could not resume the turn (HTTP ${res.status}).`);
130
+ const detail = await res
131
+ .json()
132
+ .then((b) => String(b?.detail ?? b?.error ?? '').trim())
133
+ .catch(() => '');
134
+ throw new Error(detail
135
+ ? `Could not resume the turn (HTTP ${res.status}): ${detail}`
136
+ : `Could not resume the turn (HTTP ${res.status}).`);
129
137
  }
130
138
  return (await res.json());
131
139
  }
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(' | ')}`)
266
- .option('--rate <rate>', `how edits apply: ${RATES.join(' | ')}`, 'manual')
267
+ .option('-m, --mode <mode>', `what Rivine Code works on: ${CODE_MODES.join(' | ')}`)
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,17 +250,13 @@ 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':
260
256
  return 'plan';
261
257
  case 'default':
262
- return 'default';
263
258
  case 'acceptEdits':
264
- return 'accept edits';
259
+ return 'manual';
265
260
  case 'bypass':
266
261
  return 'bypass';
267
262
  }
@@ -271,10 +266,23 @@ export function describeMode(mode) {
271
266
  case 'plan':
272
267
  return 'plan · reads only';
273
268
  case 'default':
274
- return 'default · asks before changes';
275
269
  case 'acceptEdits':
276
- return 'accept edits · writes apply, commands ask';
270
+ return 'manual · asks before edits';
277
271
  case 'bypass':
278
- return 'bypass · no prompts';
272
+ return 'bypass · edits without asking';
273
+ }
274
+ }
275
+ export function codeModeName(mode) {
276
+ switch (mode) {
277
+ case 'frontend':
278
+ return 'Frontend';
279
+ case 'backend':
280
+ return 'Backend';
281
+ case 'fullstack':
282
+ return 'Full Stack';
283
+ case 'simple':
284
+ return 'Plan/Ask';
285
+ case 'security':
286
+ return 'Security';
279
287
  }
280
288
  }
@@ -16,6 +16,7 @@ import { isSealedEnvelope, SessionKey } from './sessionKey.js';
16
16
  import { permissionModeToRate } from './permissions.js';
17
17
  import { selectRelevantTools } from '../mcp/relevance.js';
18
18
  import { readTurnStream } from './stream.js';
19
+ import { displayStage } from '../ui/render.js';
19
20
  const PENDING_POLL_MS = 1200;
20
21
  export class TurnRunner {
21
22
  ctx;
@@ -72,6 +73,14 @@ export class TurnRunner {
72
73
  this.ctx.mcpTools = tools;
73
74
  }
74
75
  async run(message, snapshot) {
76
+ if (!this.ctx.codeMode) {
77
+ return {
78
+ answer: '',
79
+ cancelled: false,
80
+ error: 'No Mode selected. Pick one with /mode — Frontend Only, Backend Only, ' +
81
+ 'Full Stack, Plan/Ask Only, or Security Scan/Fix.',
82
+ };
83
+ }
75
84
  this.stopped = false;
76
85
  this.startPolling();
77
86
  const selection = selectRelevantTools(message, this.ctx.mcpTools ?? []);
@@ -112,9 +121,12 @@ export class TurnRunner {
112
121
  answer += event.content ?? '';
113
122
  this.cb.onToken(event.content ?? '');
114
123
  break;
115
- case 'stage':
116
- this.cb.onStage(String(event.label ?? ''));
124
+ case 'stage': {
125
+ const label = displayStage(String(event.label ?? ''));
126
+ if (label)
127
+ this.cb.onStage(label);
117
128
  break;
129
+ }
118
130
  case 'tool_call_start': {
119
131
  const e = event;
120
132
  const detail = e.args?.command ?? e.args?.filename ?? '';
@@ -158,6 +170,7 @@ export class TurnRunner {
158
170
  options: extractOptions(e),
159
171
  multi: Boolean(e.ui_schema?.multi ?? e.args?.multi),
160
172
  threadId: String(e.thread_id ?? `${this.ctx.workspaceId}_${this.ctx.chatSpaceId}`),
173
+ kind: hitlKind(e.hitl_type, e.args),
161
174
  };
162
175
  break;
163
176
  }
@@ -184,6 +197,8 @@ export class TurnRunner {
184
197
  let answer = '';
185
198
  for (let round = 0; current && round < 25; round += 1) {
186
199
  const decision = await this.enqueuePrompt(() => this.cb.requestDecision(current));
200
+ if (decision.codeMode)
201
+ this.setCodeMode(decision.codeMode);
187
202
  const body = (await resumeTurn({
188
203
  workspaceId: this.ctx.workspaceId,
189
204
  chatSpaceId: this.ctx.chatSpaceId,
@@ -193,6 +208,8 @@ export class TurnRunner {
193
208
  approved: decision.approved,
194
209
  feedback: decision.feedback,
195
210
  modifiedArgs: decision.modifiedArgs,
211
+ codeMode: decision.codeMode ?? null,
212
+ codeRate: decision.codeMode ? permissionModeToRate(this.ctx.mode) : null,
196
213
  }));
197
214
  answer = String(body.response ?? answer);
198
215
  const meta = body.context_metadata ?? {};
@@ -203,6 +220,7 @@ export class TurnRunner {
203
220
  options: extractOptions({ ui_schema: meta.hitl_ui_schema, args: meta.hitl_args }),
204
221
  multi: Boolean(meta.hitl_ui_schema?.multi),
205
222
  threadId: String(meta.thread_id ?? current.threadId),
223
+ kind: hitlKind(meta.hitl_type, meta.hitl_args),
206
224
  }
207
225
  : null;
208
226
  }
@@ -339,9 +357,26 @@ export class TurnRunner {
339
357
  await closeSession(this.ctx.sessionId);
340
358
  }
341
359
  }
342
- function extractOptions(event) {
360
+ export function hitlKind(hitlType, args) {
361
+ const field = args && typeof args === 'object' ? String(args.field ?? '') : '';
362
+ return String(hitlType ?? '') === 'code_clarification' && field === 'mode'
363
+ ? 'code_mode'
364
+ : 'other';
365
+ }
366
+ export function extractOptions(event) {
367
+ const schemaEnum = (() => {
368
+ const props = event?.ui_schema?.properties;
369
+ if (!props || typeof props !== 'object')
370
+ return undefined;
371
+ for (const value of Object.values(props)) {
372
+ if (Array.isArray(value?.enum) && value.enum.length)
373
+ return value.enum;
374
+ }
375
+ return undefined;
376
+ })();
343
377
  const raw = event?.ui_schema?.options ??
344
378
  event?.ui_schema?.choices ??
379
+ schemaEnum ??
345
380
  event?.args?.options ??
346
381
  event?.options;
347
382
  if (!Array.isArray(raw) || raw.length === 0)
@@ -11,6 +11,7 @@
11
11
  import { InsufficientCredits } from './api/workspace.js';
12
12
  import { siteUrl } from './config.js';
13
13
  import { TurnRunner } from './protocol/runner.js';
14
+ import { permissionModeToRate } from './protocol/permissions.js';
14
15
  import { creditShortfall, startSession } from './session.js';
15
16
  import { HeadlessRenderer, installTeardown } from './ui/headless.js';
16
17
  export async function runHeadless(options) {
@@ -84,7 +85,9 @@ export async function runHeadless(options) {
84
85
  },
85
86
  requestDecision: async (request) => {
86
87
  out.endAnswer();
87
- out.line('notice', `Approval needed but no terminal to ask on: ${request.message}`);
88
+ out.line('notice', request.kind === 'code_mode'
89
+ ? 'No Mode selected. Pass -m frontend|backend|fullstack|simple|security.'
90
+ : `Approval needed but no terminal to ask on: ${request.message}`);
88
91
  return { approved: false, feedback: 'No interactive terminal available.' };
89
92
  },
90
93
  askQuestion: async (question) => {
@@ -111,7 +114,7 @@ export async function runHeadless(options) {
111
114
  if (out.refusalCount > 0) {
112
115
  const advice = options.mode === 'plan'
113
116
  ? 'plan mode makes no changes — re-run without --plan'
114
- : `re-run with -m acceptEdits, or --bypass in CI (mode was ${options.mode})`;
117
+ : `re-run with --rate bypass (Rate was ${permissionModeToRate(options.mode)})`;
115
118
  const message = `${out.refusalCount} operation(s) were skipped because there is no terminal ` +
116
119
  `to approve them: ${advice}.`;
117
120
  out.line('error', message);
package/dist/ui/App.js CHANGED
@@ -20,10 +20,12 @@ import { CODE_MODE_LABELS, describeMode, isBatchable, mcpServerOf, nextMode, } f
20
20
  import { creditShortfall, startSession } from '../session.js';
21
21
  import { sanitizeForModel } from '../protocol/shell.js';
22
22
  import { diff as gitDiff, gitBranchLine, prCreate } from '../protocol/git.js';
23
- import { Activity, ApprovalPrompt, Banner, CodeModePicker, ModePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
23
+ import { Activity, ApprovalPrompt, Banner, CodeModePicker, RatePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
24
24
  import { TokenBuffer, terminalWidth } from './render.js';
25
+ import { initialPhase, phaseAfterBootstrap, phaseAfterModePicked, phaseAfterRatePicked, } from './phase.js';
25
26
  import { color } from './theme.js';
26
27
  import { completeInput, completionsFor } from './slashCommands.js';
28
+ import { setNoticeSink } from './notices.js';
27
29
  function trimToPane(text) {
28
30
  const lines = text.split('\n');
29
31
  return lines.length > SHELL_PANE_LINES * 2
@@ -40,7 +42,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
40
42
  const { exit } = useApp();
41
43
  const [codeMode, setCodeMode] = useState(initialCodeMode);
42
44
  const codeModeChosenRef = useRef(Boolean(initialCodeMode));
43
- const [phase, setPhase] = useState(initialCodeMode ? (askForMode ? 'mode' : 'starting') : 'codeMode');
45
+ const [phase, setPhase] = useState(initialPhase(Boolean(initialCodeMode), Boolean(askForMode)));
44
46
  const sessionReadyRef = useRef(false);
45
47
  const modeChosenRef = useRef(!askForMode);
46
48
  const [lines, setLines] = useState([]);
@@ -74,6 +76,8 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
74
76
  const mcpRef = useRef(null);
75
77
  const snapshotRef = useRef(null);
76
78
  const snapshotSentRef = useRef(false);
79
+ const minCreditsRef = useRef({ rivine_code: 10 });
80
+ const queuedPromptRef = useRef(null);
77
81
  const answerRef = useRef(null);
78
82
  const lineId = useRef(0);
79
83
  const liveText = useRef('');
@@ -119,9 +123,10 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
119
123
  codeModeChosenRef.current = true;
120
124
  runnerRef.current?.setCodeMode(chosen);
121
125
  push('system', `Mode: ${CODE_MODE_LABELS[chosen]}`);
122
- setPhase(modeChosenRef.current
123
- ? (sessionReadyRef.current ? 'ready' : 'starting')
124
- : 'mode');
126
+ setPhase(phaseAfterModePicked({
127
+ sessionReady: sessionReadyRef.current,
128
+ rateChosen: modeChosenRef.current,
129
+ }));
125
130
  }, [push]);
126
131
  const cancelCodeModePick = useCallback(() => {
127
132
  setPhase(sessionReadyRef.current ? 'ready' : 'starting');
@@ -130,8 +135,11 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
130
135
  setMode(chosen);
131
136
  modeChosenRef.current = true;
132
137
  runnerRef.current?.setMode(chosen);
133
- setPhase(sessionReadyRef.current ? 'ready' : 'starting');
134
- push('system', `Mode: ${describeMode(chosen)}`);
138
+ setPhase(phaseAfterRatePicked({
139
+ sessionReady: sessionReadyRef.current,
140
+ rateChosen: true,
141
+ }));
142
+ push('system', `Rate: ${describeMode(chosen)}`);
135
143
  if (chosen === 'bypass') {
136
144
  push('notice', 'Running without prompts. Files outside this project are still refused.');
137
145
  }
@@ -140,13 +148,21 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
140
148
  const cancelModePick = useCallback(() => {
141
149
  setPhase(sessionReadyRef.current ? 'ready' : 'starting');
142
150
  }, []);
151
+ useEffect(() => {
152
+ setNoticeSink((text) => push('notice', text));
153
+ return () => setNoticeSink(null);
154
+ }, [push]);
143
155
  const quit = useCallback(() => {
144
- void runnerRef.current?.shutdown().finally(() => exit());
156
+ void runnerRef.current
157
+ ?.shutdown()
158
+ .catch(() => {
159
+ })
160
+ .finally(() => exit());
145
161
  }, [exit]);
146
162
  const cycleMode = useCallback(() => {
147
163
  setMode((current) => {
148
164
  const next = nextMode(current);
149
- push('system', `Mode: ${describeMode(next)}`);
165
+ push('system', `Rate: ${describeMode(next)}`);
150
166
  return next;
151
167
  });
152
168
  }, [push]);
@@ -169,15 +185,26 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
169
185
  setScreenGeneration((n) => n + 1);
170
186
  return true;
171
187
  case '/mode':
188
+ setPhase('codeMode');
189
+ return true;
190
+ case '/rate':
172
191
  setPhase('mode');
173
192
  return true;
174
193
  case '/plan':
175
- setMode('plan');
176
- push('system', `Mode: ${describeMode('plan')}`);
194
+ setCodeMode('simple');
195
+ codeModeChosenRef.current = true;
196
+ runnerRef.current?.setCodeMode('simple');
197
+ push('system', `Mode: ${CODE_MODE_LABELS.simple}`);
198
+ setPhase(phaseAfterModePicked({
199
+ sessionReady: sessionReadyRef.current,
200
+ rateChosen: modeChosenRef.current,
201
+ }));
177
202
  return true;
178
203
  case '/accept':
179
- setMode('acceptEdits');
180
- push('system', `Mode: ${describeMode('acceptEdits')}`);
204
+ setMode('bypass');
205
+ runnerRef.current?.setMode('bypass');
206
+ push('system', `Rate: ${describeMode('bypass')}`);
207
+ push('notice', 'Accept-edits is gone — Bypass applies in-project edits without asking.');
181
208
  return true;
182
209
  case '/credits':
183
210
  openBrowser(`${siteUrl()}/credits`);
@@ -197,7 +224,8 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
197
224
  }
198
225
  case '/status': {
199
226
  push('system', `${status.cloudhandle || 'signed in'} ${status.bcidMasked} · ${status.project} · ` +
200
- `${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} · ${describeMode(mode)}`);
227
+ `${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} · ` +
228
+ `${codeMode ? CODE_MODE_LABELS[codeMode] : 'Mode unset'} · ${describeMode(mode)}`);
201
229
  const facts = sessionFactsRef.current;
202
230
  push('system', `${facts.continuing ? "continuing this directory's thread" : 'new thread'} · ` +
203
231
  `${facts.hasLocalMemory ? 'project notes on disk' : 'no project notes yet'}`);
@@ -227,7 +255,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
227
255
  push('notice', `Unknown command ${cmd} — /help for the list.`);
228
256
  return true;
229
257
  }
230
- }, [cwd, cycleMode, mode, push, quit, refreshBalance, status]);
258
+ }, [codeMode, cwd, cycleMode, mode, push, quit, refreshBalance, status]);
231
259
  const submit = useCallback(async (message) => {
232
260
  const text = message.trim();
233
261
  setPromptText('');
@@ -241,7 +269,13 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
241
269
  return;
242
270
  if (runCommand(text))
243
271
  return;
244
- const needed = creditShortfall(status.balance, { rivine_code: 10 });
272
+ if (!codeMode) {
273
+ queuedPromptRef.current = text;
274
+ push('notice', 'Pick a Mode first — it decides what Rivine Code works on. Your message is saved.');
275
+ setPhase('codeMode');
276
+ return;
277
+ }
278
+ const needed = creditShortfall(status.balance, minCreditsRef.current);
245
279
  if (needed !== null) {
246
280
  push('error', `Rivine Code needs $${needed.toFixed(2)} in credits to run.`);
247
281
  push('notice', `Top up at ${siteUrl()}/credits`);
@@ -280,7 +314,16 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
280
314
  if (oneShot)
281
315
  quit();
282
316
  }
283
- }, [oneShot, push, quit, refreshBalance, runCommand, settleLive, status.balance]);
317
+ }, [codeMode, oneShot, push, quit, refreshBalance, runCommand, settleLive, status.balance]);
318
+ useEffect(() => {
319
+ if (phase !== 'ready' || !codeMode)
320
+ return;
321
+ const queued = queuedPromptRef.current;
322
+ if (!queued)
323
+ return;
324
+ queuedPromptRef.current = null;
325
+ void submit(queued);
326
+ }, [phase, codeMode, submit]);
284
327
  useEffect(() => {
285
328
  let cancelled = false;
286
329
  (async () => {
@@ -392,20 +435,29 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
392
435
  setPhase('question');
393
436
  }),
394
437
  }, session.mcp);
395
- void session.mcpReady.then((reports) => {
438
+ void session.mcpReady
439
+ .then((reports) => {
396
440
  if (cancelled || reports.length === 0)
397
441
  return;
398
442
  const good = reports.filter((r) => r.ok);
399
443
  const tools = session.mcp.listTools().length;
400
- if (good.length)
444
+ if (good.length) {
401
445
  push('system', `MCP: ${good.length} server(s), ${tools} tool(s) available.`);
446
+ }
402
447
  for (const bad of reports.filter((r) => !r.ok)) {
403
448
  push('notice', `MCP server "${bad.name}" did not start: ${bad.error}`);
404
449
  }
405
450
  runnerRef.current?.setMcpTools(session.mcp.listTools());
451
+ })
452
+ .catch((err) => {
453
+ push('notice', `MCP servers could not be listed: ${err.message}`);
406
454
  });
455
+ minCreditsRef.current = session.bootstrap.engine_min_credits ?? minCreditsRef.current;
407
456
  sessionReadyRef.current = true;
408
- setPhase(modeChosenRef.current ? 'ready' : 'mode');
457
+ setPhase((current) => phaseAfterBootstrap(current, {
458
+ sessionReady: true,
459
+ rateChosen: modeChosenRef.current,
460
+ }));
409
461
  runnerRef.current?.setMode(mode);
410
462
  push('system', `${session.project}${session.bootstrap.created ? ' · new session' : ''}`);
411
463
  if (session.continuing) {
@@ -415,7 +467,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
415
467
  push('notice', 'Project notes found in .rivine/local_memory.md.');
416
468
  }
417
469
  if (!oneShot && modeChosenRef.current && !askForMode) {
418
- push('system', `Mode: ${describeMode(mode)}`);
470
+ push('system', `Rate: ${describeMode(mode)}`);
419
471
  push('system', 'Ask for anything. /help for commands.');
420
472
  }
421
473
  if (oneShot)
@@ -517,10 +569,17 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
517
569
  const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
518
570
  const matches = completionsFor(input);
519
571
  const busy = phase !== 'ready';
520
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'banner' ? (_jsx(Banner, { compact: compactBanner }, "banner")) : (_jsx(TranscriptLine, { line: item.line }, item.line.id)) }, screenGeneration), live ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: color('text'), children: live }) })) : null, shellView ? _jsx(ShellPane, { text: shellView, live: shellActive }) : null, phase === 'running' && stage ? _jsx(Activity, { stage: stage, tool: tool }) : null, phase === 'codeMode' ? (_jsx(CodeModePicker, { onPick: pickCodeMode, current: codeModeChosenRef.current ? codeMode : undefined, onCancel: codeModeChosenRef.current ? cancelCodeModePick : undefined })) : null, phase === 'mode' ? (_jsx(ModePicker, { onPick: pickMode, current: modeChosenRef.current ? mode : undefined, onCancel: modeChosenRef.current ? cancelModePick : undefined })) : null, phase === 'approval' && approval ? (_jsx(ApprovalPrompt, { approval: approval, allowAlways: isBatchable(approval.category), onChoice: answerApproval })) : null, phase === 'question' && question ? (question.options?.length ? (_jsx(ChoicePrompt, { prompt: question.prompt, options: question.options, onPick: answerQuestion })) : (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('accent'), bold: true, children: question.prompt }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "your answer", onSubmit: answerQuestion })] })] }))) : null, phase === 'hitl' && hitl ? (_jsx(ChoicePrompt, { prompt: hitl.tool ? `${hitl.tool} — ${hitl.message}` : hitl.message, options: hitl.options?.length ? hitl.options : ['Approve', 'Decline'], onPick: (value) => answerHitl(hitl.options?.length
572
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'banner' ? (_jsx(Banner, { compact: compactBanner }, "banner")) : (_jsx(TranscriptLine, { line: item.line }, item.line.id)) }, screenGeneration), live ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: color('text'), children: live }) })) : null, shellView ? _jsx(ShellPane, { text: shellView, live: shellActive }) : null, phase === 'running' && stage ? _jsx(Activity, { stage: stage, tool: tool }) : null, phase === 'codeMode' ? (_jsx(CodeModePicker, { onPick: pickCodeMode, current: codeModeChosenRef.current ? codeMode : undefined, onCancel: codeModeChosenRef.current ? cancelCodeModePick : undefined })) : null, phase === 'mode' ? (_jsx(RatePicker, { onPick: pickMode, current: modeChosenRef.current ? mode : undefined, onCancel: modeChosenRef.current ? cancelModePick : undefined })) : null, phase === 'approval' && approval ? (_jsx(ApprovalPrompt, { approval: approval, allowAlways: isBatchable(approval.category), onChoice: answerApproval })) : null, phase === 'question' && question ? (question.options?.length ? (_jsx(ChoicePrompt, { prompt: question.prompt, options: question.options, onPick: answerQuestion })) : (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('accent'), bold: true, children: question.prompt }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "your answer", onSubmit: answerQuestion })] })] }))) : null, phase === 'hitl' && hitl ? (hitl.kind === 'code_mode' ? (_jsx(CodeModePicker, { onPick: (chosen) => {
573
+ setCodeMode(chosen);
574
+ codeModeChosenRef.current = true;
575
+ runnerRef.current?.setCodeMode(chosen);
576
+ push('system', `Mode: ${CODE_MODE_LABELS[chosen]}`);
577
+ answerHitl({ approved: true, feedback: CODE_MODE_LABELS[chosen], codeMode: chosen });
578
+ }, current: codeMode })) : (_jsx(ChoicePrompt, { prompt: hitl.tool ? `${hitl.tool} — ${hitl.message}` : hitl.message, options: hitl.options?.length ? hitl.options : ['Approve', 'Decline'], onPick: (value) => answerHitl(hitl.options?.length
521
579
  ? { approved: true, feedback: value }
522
- : { approved: value === 'Approve' }) })) : null, showHelp && phase === 'ready' ? _jsx(HelpPanel, {}) : null, phase === 'ready' && !oneShot ? (_jsxs(_Fragment, { children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { defaultValue: inputSeed, placeholder: "What should Rivine build?", onChange: setInput, onSubmit: (value) => void submit(value) }, inputNonce)] }), _jsx(Completions, { matches: matches })] })) : null, phase === 'starting' ? _jsx(Activity, { stage: "Starting\u2026", tool: "" }) : null, _jsx(StatusBar, { info: {
580
+ : { approved: value === 'Approve' }) }))) : null, showHelp && phase === 'ready' ? _jsx(HelpPanel, {}) : null, phase === 'ready' && !oneShot ? (_jsxs(_Fragment, { children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { defaultValue: inputSeed, placeholder: "What should Rivine build?", onChange: setInput, onSubmit: (value) => void submit(value) }, inputNonce)] }), _jsx(Completions, { matches: matches })] })) : null, phase === 'starting' ? _jsx(Activity, { stage: "Starting\u2026", tool: "" }) : null, _jsx(StatusBar, { info: {
523
581
  mode,
582
+ codeMode,
524
583
  project: status.project,
525
584
  cloudhandle: status.cloudhandle,
526
585
  bcidMasked: status.bcidMasked,
@@ -15,7 +15,7 @@ import { Spinner } from '@inkjs/ui';
15
15
  import { ASCII_ONLY, BOX, color, glyph, lineColor } from './theme.js';
16
16
  import { fit, terminalWidth, wrap, terminalColumns } from './render.js';
17
17
  import { SLASH_COMMANDS } from './slashCommands.js';
18
- import { describeMode, modeName } from '../protocol/permissions.js';
18
+ import { codeModeName, describeMode, modeName, } from '../protocol/permissions.js';
19
19
  const WORDMARK = [
20
20
  '█▀▄ ▀█▀ █ █ ▀█▀ █▄ █ █▀▀ █▀▀ █▀█ █▀▄ █▀▀',
21
21
  '█▀▄ █ ▀▄ ▄▀ █ █ ▀█ █▀ █ █ █ █ █ █▀ ',
@@ -62,16 +62,16 @@ export function CodeModePicker({ onPick, current, onCancel, }) {
62
62
  if (found)
63
63
  onPick(found.mode);
64
64
  });
65
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Mode \u2014 which agent should handle this?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
65
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Mode \u2014 what should Rivine Code work on?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
66
66
  const active = o.mode === current;
67
67
  return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' \u203a' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label }), _jsx(Text, { color: color('faint'), children: " \u2014 " }), _jsx(Text, { color: color('faint'), children: fit(o.detail, Math.max(10, width - 34)) })] }, o.key));
68
68
  }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: current ? 'esc keeps the current mode' : 'pick a number to begin' }) })] }));
69
69
  }
70
- export function ModePicker({ onPick, current, onCancel, }) {
70
+ export function RatePicker({ onPick, current, onCancel, }) {
71
71
  const width = terminalWidth();
72
72
  const options = [
73
- { key: '1', mode: 'default', label: 'Manual', detail: 'ask for approval before each edit' },
74
- { key: '2', mode: 'bypass', label: 'Bypass', detail: 'edit files in this project without asking' },
73
+ { key: '1', mode: 'default', label: 'Manual', detail: 'ask before each edit' },
74
+ { key: '2', mode: 'bypass', label: 'Bypass', detail: 'edit in this project without asking' },
75
75
  ];
76
76
  useInput((input, key) => {
77
77
  if (key.escape && onCancel) {
@@ -88,11 +88,10 @@ export function ModePicker({ onPick, current, onCancel, }) {
88
88
  });
89
89
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Rate \u2014 how should Rivine apply edits?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
90
90
  const active = o.mode === current;
91
- return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' ›' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label.padEnd(13) }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 26)) })] }, o.key));
91
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' ›' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label.padEnd(8) }), _jsx(Text, { color: color('faint'), children: " \u2014 " }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 24)) })] }, o.key));
92
92
  }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit(current
93
- ?
94
- `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
95
- : 'press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
93
+ ? `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
94
+ : 'press a number · enter for Manual · shift+tab toggles later', width - 4) }) })] }));
96
95
  }
97
96
  export const SHELL_PANE_LINES = 12;
98
97
  export function ShellPane({ text, live }) {
@@ -137,7 +136,7 @@ export function Completions({ matches }) {
137
136
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [matches.slice(0, 6).map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: c.name.padEnd(10) }), _jsx(Text, { color: color('faint'), children: fit(c.summary, width - 14) })] }, c.name))), matches.length === 1 ? (_jsx(Text, { color: color('faint'), children: " tab to complete" })) : null] }));
138
137
  }
139
138
  export function HelpPanel() {
140
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Commands" }), SLASH_COMMANDS.map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${c.name}`.padEnd(12) }), _jsx(Text, { color: color('muted'), children: c.summary })] }, c.name))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " shift+tab change permission mode" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
139
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Commands" }), SLASH_COMMANDS.map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${c.name}`.padEnd(12) }), _jsx(Text, { color: color('muted'), children: c.summary })] }, c.name))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " Mode what Rivine Code works on \u2014 /mode" }), _jsx(Text, { color: color('faint'), children: " Rate how edits apply \u2014 /rate, or shift+tab" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " shift+tab toggle Rate (Manual \u21C4 Bypass)" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
141
140
  }
142
141
  export function StatusBar({ info }) {
143
142
  const width = terminalColumns();
@@ -157,18 +156,27 @@ export function StatusBar({ info }) {
157
156
  segments.push({ node, text });
158
157
  return true;
159
158
  };
160
- const modeText = describeMode(info.mode);
161
- const modeShort = modeName(info.mode);
159
+ const rateText = describeMode(info.mode);
160
+ const rateShort = modeName(info.mode);
161
+ const agentText = info.codeMode ? codeModeName(info.codeMode) : '';
162
162
  const brand = 'Rivine';
163
- const fitsBrandAndFullMode = 2 + brand.length + 3 + modeText.length <= width - 2;
164
- if (2 + brand.length + 3 + modeShort.length <= width - 2) {
163
+ const fitsBrandAndFullRate = 2 +
164
+ brand.length +
165
+ (agentText ? 3 + agentText.length : 0) +
166
+ 3 +
167
+ rateText.length <=
168
+ width - 2;
169
+ if (2 + brand.length + 3 + rateShort.length <= width - 2) {
165
170
  add(brand, (_jsx(Text, { color: color('brand'), bold: true, children: brand })));
166
171
  }
167
- const shownMode = fitsBrandAndFullMode ? modeText : modeShort;
168
- used += (segments.length > 0 ? 3 : 0) + shownMode.length;
172
+ if (agentText) {
173
+ add(agentText, _jsx(Text, { color: color('accent'), children: agentText }));
174
+ }
175
+ const shownRate = fitsBrandAndFullRate ? rateText : rateShort;
176
+ used += (segments.length > 0 ? 3 : 0) + shownRate.length;
169
177
  segments.push({
170
- node: _jsx(Text, { color: color('muted'), children: shownMode }),
171
- text: shownMode,
178
+ node: _jsx(Text, { color: color('muted'), children: shownRate }),
179
+ text: shownRate,
172
180
  });
173
181
  if (info.balance !== null) {
174
182
  const money = `$${info.balance.toFixed(2)}`;
@@ -73,7 +73,7 @@ export class HeadlessRenderer {
73
73
  refuseApproval(approval) {
74
74
  const fix = this.mode === 'plan'
75
75
  ? 'plan mode makes no changes; re-run without --plan'
76
- : 'no terminal to ask on — re-run with -m acceptEdits, or --bypass in CI';
76
+ : 'no terminal to ask on — re-run with --rate bypass';
77
77
  this.refused += 1;
78
78
  this.line('notice', `Skipped: ${approval.summary}`);
79
79
  this.line('notice', ` ${approval.reason} (${fix})`);
@@ -85,7 +85,7 @@ export class HeadlessRenderer {
85
85
  return '';
86
86
  }
87
87
  }
88
- export function installTeardown(onExit) {
88
+ export function installTeardown(onExit, options = {}) {
89
89
  let finished = false;
90
90
  const restore = () => {
91
91
  try {
@@ -116,15 +116,18 @@ export function installTeardown(onExit) {
116
116
  process.stderr.write(`\n ${err?.message ?? String(err)}\n\n`);
117
117
  void finish(1);
118
118
  };
119
+ const onRejection = options.onRejection
120
+ ? (err) => options.onRejection(err)
121
+ : onFatal;
119
122
  process.on('SIGINT', onSigint);
120
123
  process.on('SIGTERM', onSigterm);
121
124
  process.on('uncaughtException', onFatal);
122
- process.on('unhandledRejection', onFatal);
125
+ process.on('unhandledRejection', onRejection);
123
126
  return () => {
124
127
  process.off('SIGINT', onSigint);
125
128
  process.off('SIGTERM', onSigterm);
126
129
  process.off('uncaughtException', onFatal);
127
- process.off('unhandledRejection', onFatal);
130
+ process.off('unhandledRejection', onRejection);
128
131
  restore();
129
132
  };
130
133
  }
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * Rivine Code CLI
3
+ * Licensed and owned by Beunec Technologies, Inc.
4
+ *
5
+ * https://aselius.beunec.com
6
+ *
7
+ * This software is proprietary. Use is governed by the licence agreement
8
+ * accompanying it; it is not open source and is not licensed for
9
+ * redistribution or derivative works.
10
+ */
11
+ let sink = null;
12
+ export function setNoticeSink(next) {
13
+ sink = next;
14
+ }
15
+ export function emitNotice(text) {
16
+ if (!sink || !text)
17
+ return false;
18
+ sink(text);
19
+ return true;
20
+ }
@@ -0,0 +1,28 @@
1
+ /*!
2
+ * Rivine Code CLI
3
+ * Licensed and owned by Beunec Technologies, Inc.
4
+ *
5
+ * https://aselius.beunec.com
6
+ *
7
+ * This software is proprietary. Use is governed by the licence agreement
8
+ * accompanying it; it is not open source and is not licensed for
9
+ * redistribution or derivative works.
10
+ */
11
+ export function phaseAfterBootstrap(current, s) {
12
+ if (current === 'codeMode' || current === 'mode')
13
+ return current;
14
+ return s.rateChosen ? 'ready' : 'mode';
15
+ }
16
+ export function phaseAfterModePicked(s) {
17
+ if (!s.rateChosen)
18
+ return 'mode';
19
+ return s.sessionReady ? 'ready' : 'starting';
20
+ }
21
+ export function phaseAfterRatePicked(s) {
22
+ return s.sessionReady ? 'ready' : 'starting';
23
+ }
24
+ export function initialPhase(hasMode, askRate) {
25
+ if (!hasMode)
26
+ return 'codeMode';
27
+ return askRate ? 'mode' : 'starting';
28
+ }
package/dist/ui/render.js CHANGED
@@ -111,3 +111,6 @@ export function tail(text, max) {
111
111
  return { lines, hidden: 0 };
112
112
  return { lines: lines.slice(-max), hidden: lines.length - max };
113
113
  }
114
+ export function displayStage(label) {
115
+ return /^routed to\b/i.test(label.trim()) ? '' : label;
116
+ }
@@ -12,12 +12,25 @@ export const SLASH_COMMANDS = [
12
12
  { name: '/help', summary: 'show these commands' },
13
13
  {
14
14
  name: '/mode',
15
- summary: 'show or cycle the permission mode',
16
- detail: 'Shift+Tab cycles plan → default → accept edits.',
15
+ summary: 'pick the Mode — what Rivine Code works on',
16
+ detail: 'Frontend Only, Backend Only, Full Stack, Plan/Ask Only, or Security Scan/Fix.',
17
17
  },
18
- { name: '/plan', summary: 'switch to plan mode (reads only)' },
19
- { name: '/accept', summary: 'switch to accept-edits mode' },
20
- { name: '/status', summary: 'account, project and balance' },
18
+ {
19
+ name: '/rate',
20
+ summary: 'pick the Rate — Manual or Bypass',
21
+ detail: 'Manual asks before each edit; Bypass applies them. Shift+Tab toggles.',
22
+ },
23
+ {
24
+ name: '/plan',
25
+ summary: 'switch to Plan/Ask Only Mode',
26
+ detail: 'Same as picking Plan/Ask Only in /mode.',
27
+ },
28
+ {
29
+ name: '/accept',
30
+ summary: 'switch Rate to Bypass',
31
+ detail: 'Accept-edits is gone; Bypass applies in-project edits without asking.',
32
+ },
33
+ { name: '/status', summary: 'account, project, Mode, Rate and balance' },
21
34
  { name: '/diff', summary: 'what changed in the working tree' },
22
35
  {
23
36
  name: '/pr',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beunec/rivine",
3
- "version": "0.2.0",
4
- "description": "Rivine Code in your terminal \u2014 Beunec's software engineering runtime constructs, working on the files you already have open.",
3
+ "version": "0.3.0",
4
+ "description": "Rivine Code in your terminal — Beunec's software engineering runtime constructs, working on the files you already have open.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Beunec Technologies",
7
7
  "homepage": "https://aselius.beunec.com",