@beunec/rivine 0.1.1 → 0.1.2
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/dist/index.js +6 -5
- package/dist/protocol/permissions.js +20 -2
- package/dist/ui/App.js +47 -13
- package/dist/ui/components.js +19 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,13 +43,13 @@ program
|
|
|
43
43
|
.version(readPackageVersion());
|
|
44
44
|
function resolveMode(options) {
|
|
45
45
|
if (options.bypass)
|
|
46
|
-
return 'bypass';
|
|
46
|
+
return { mode: 'bypass', explicit: true };
|
|
47
47
|
if (options.plan)
|
|
48
|
-
return 'plan';
|
|
48
|
+
return { mode: 'plan', explicit: true };
|
|
49
49
|
const requested = options.mode;
|
|
50
50
|
if (requested && PERMISSION_MODES.includes(requested))
|
|
51
|
-
return requested;
|
|
52
|
-
return 'default';
|
|
51
|
+
return { mode: requested, explicit: true };
|
|
52
|
+
return { mode: 'default', explicit: false };
|
|
53
53
|
}
|
|
54
54
|
program
|
|
55
55
|
.command('login', { isDefault: false })
|
|
@@ -265,7 +265,7 @@ program
|
|
|
265
265
|
.action(async (prompt, options) => {
|
|
266
266
|
const cwd = options.cwd ? resolvePath(options.cwd) : process.cwd();
|
|
267
267
|
const oneShot = options.print ?? (prompt.length ? prompt.join(' ') : undefined);
|
|
268
|
-
const mode = resolveMode(options);
|
|
268
|
+
const { mode, explicit: modeExplicit } = resolveMode(options);
|
|
269
269
|
const format = options.outputFormat === 'json' ? 'json' : 'text';
|
|
270
270
|
if (!isInteractiveTerminal()) {
|
|
271
271
|
if (!oneShot) {
|
|
@@ -284,6 +284,7 @@ program
|
|
|
284
284
|
const { waitUntilExit } = render(React.createElement(App, {
|
|
285
285
|
cwd,
|
|
286
286
|
initialMode: mode,
|
|
287
|
+
askForMode: !modeExplicit && !oneShot,
|
|
287
288
|
oneShot,
|
|
288
289
|
}), { exitOnCtrlC: false });
|
|
289
290
|
const removeTeardown = installTeardown(() => { });
|
|
@@ -91,12 +91,30 @@ export function isNotebookSeedCommand(command) {
|
|
|
91
91
|
export function isSecretPath(relPath) {
|
|
92
92
|
return SECRET_PATTERNS.some((re) => re.test(relPath));
|
|
93
93
|
}
|
|
94
|
+
const OPAQUE_SHELL = /[<>]|\$\(|`/;
|
|
95
|
+
const COMMAND_SEPARATORS = /\|\||&&|[;|]/;
|
|
96
|
+
const MUTATING_FLAGS = {
|
|
97
|
+
sed: /(^|\s)-[a-zA-Z]*i/,
|
|
98
|
+
find: /(^|\s)-(delete|exec|execdir|ok|okdir|fls|fprint|fprintf)(\s|$)/,
|
|
99
|
+
env: /\S+=\S+/,
|
|
100
|
+
};
|
|
94
101
|
export function isReadOnlyCommand(command) {
|
|
95
102
|
const trimmed = command.trim();
|
|
96
|
-
if (!trimmed ||
|
|
103
|
+
if (!trimmed || OPAQUE_SHELL.test(trimmed))
|
|
97
104
|
return false;
|
|
105
|
+
const segments = trimmed
|
|
106
|
+
.split(COMMAND_SEPARATORS)
|
|
107
|
+
.map((s) => s.trim())
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
if (segments.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
if (segments.length > 1)
|
|
112
|
+
return segments.every((s) => isReadOnlyCommand(s));
|
|
98
113
|
const tokens = trimmed.split(/\s+/);
|
|
99
114
|
const head = (tokens[0] ?? '').replace(/^.*\//, '');
|
|
115
|
+
const mutating = MUTATING_FLAGS[head];
|
|
116
|
+
if (mutating && mutating.test(trimmed.slice(head.length)))
|
|
117
|
+
return false;
|
|
100
118
|
if (READ_ONLY_COMMANDS.has(head))
|
|
101
119
|
return true;
|
|
102
120
|
const subcommands = READ_ONLY_SUBCOMMANDS[head];
|
|
@@ -160,7 +178,7 @@ export function decide(input) {
|
|
|
160
178
|
if (isNotebookSeedCommand(cmd) && mode !== 'plan') {
|
|
161
179
|
return { verdict: 'allow' };
|
|
162
180
|
}
|
|
163
|
-
if (mode
|
|
181
|
+
if (mode !== 'plan' && isReadOnlyCommand(cmd)) {
|
|
164
182
|
return { verdict: 'allow' };
|
|
165
183
|
}
|
|
166
184
|
if (mode === 'plan') {
|
package/dist/ui/App.js
CHANGED
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
13
|
import { Box, Static, Text, useApp, useInput } from 'ink';
|
|
14
14
|
import { TextInput } from '@inkjs/ui';
|
|
15
|
-
import { InsufficientCredits } from '../api/workspace.js';
|
|
15
|
+
import { fetchCredits, InsufficientCredits } from '../api/workspace.js';
|
|
16
16
|
import { openBrowser } from '../auth/pkce.js';
|
|
17
17
|
import { siteUrl } from '../config.js';
|
|
18
18
|
import { TurnRunner } from '../protocol/runner.js';
|
|
@@ -20,8 +20,8 @@ import { describeMode, isBatchable, mcpServerOf, nextMode, } from '../protocol/p
|
|
|
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, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
|
|
24
|
-
import { TokenBuffer,
|
|
23
|
+
import { Activity, ApprovalPrompt, Banner, ModePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
|
|
24
|
+
import { TokenBuffer, terminalWidth } from './render.js';
|
|
25
25
|
import { color } from './theme.js';
|
|
26
26
|
import { completionsFor } from './slashCommands.js';
|
|
27
27
|
function trimToPane(text) {
|
|
@@ -35,9 +35,11 @@ function grantKey(category, request) {
|
|
|
35
35
|
return category;
|
|
36
36
|
return `mcp:${mcpServerOf(String(request.args?.tool ?? '')) ?? 'unknown'}`;
|
|
37
37
|
}
|
|
38
|
-
export function App({ cwd, initialMode, oneShot }) {
|
|
38
|
+
export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
39
39
|
const { exit } = useApp();
|
|
40
|
-
const [phase, setPhase] = useState('starting');
|
|
40
|
+
const [phase, setPhase] = useState(askForMode ? 'mode' : 'starting');
|
|
41
|
+
const sessionReadyRef = useRef(false);
|
|
42
|
+
const modeChosenRef = useRef(!askForMode);
|
|
41
43
|
const [lines, setLines] = useState([]);
|
|
42
44
|
const [live, setLive] = useState('');
|
|
43
45
|
const [shellView, setShellView] = useState('');
|
|
@@ -68,6 +70,7 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
68
70
|
const shellDirty = useRef(false);
|
|
69
71
|
const shellRef = useRef(null);
|
|
70
72
|
const sessionFactsRef = useRef({ continuing: false, hasLocalMemory: false });
|
|
73
|
+
const creditsUrlRef = useRef(null);
|
|
71
74
|
const alwaysAllow = useRef(new Set());
|
|
72
75
|
const tokens = useMemo(() => new TokenBuffer((text) => {
|
|
73
76
|
liveText.current += text;
|
|
@@ -77,7 +80,8 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
77
80
|
if (!text)
|
|
78
81
|
return;
|
|
79
82
|
lineId.current += 1;
|
|
80
|
-
|
|
83
|
+
const id = lineId.current;
|
|
84
|
+
setLines((prev) => [...prev, { id, kind, text, detail }]);
|
|
81
85
|
}, []);
|
|
82
86
|
const settleLive = useCallback(() => {
|
|
83
87
|
tokens.flush();
|
|
@@ -87,6 +91,29 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
87
91
|
if (text)
|
|
88
92
|
push('agent', text);
|
|
89
93
|
}, [push, tokens]);
|
|
94
|
+
const refreshBalance = useCallback(async () => {
|
|
95
|
+
const url = creditsUrlRef.current;
|
|
96
|
+
if (!url)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
const credits = await fetchCredits(url);
|
|
100
|
+
if (credits)
|
|
101
|
+
setStatus((s) => ({ ...s, balance: credits.balance }));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
const pickMode = useCallback((chosen) => {
|
|
107
|
+
setMode(chosen);
|
|
108
|
+
modeChosenRef.current = true;
|
|
109
|
+
runnerRef.current?.setMode(chosen);
|
|
110
|
+
setPhase(sessionReadyRef.current ? 'ready' : 'starting');
|
|
111
|
+
push('system', `Mode: ${describeMode(chosen)}`);
|
|
112
|
+
if (chosen === 'bypass') {
|
|
113
|
+
push('notice', 'Running without prompts. Files outside this project are still refused.');
|
|
114
|
+
}
|
|
115
|
+
push('system', 'Ask for anything. /help for commands.');
|
|
116
|
+
}, [push]);
|
|
90
117
|
const quit = useCallback(() => {
|
|
91
118
|
void runnerRef.current?.shutdown().finally(() => exit());
|
|
92
119
|
}, [exit]);
|
|
@@ -148,6 +175,7 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
148
175
|
const branch = gitBranchLine(cwd);
|
|
149
176
|
if (branch)
|
|
150
177
|
push('system', branch);
|
|
178
|
+
void refreshBalance();
|
|
151
179
|
return true;
|
|
152
180
|
}
|
|
153
181
|
case '/diff': {
|
|
@@ -170,7 +198,7 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
170
198
|
push('notice', `Unknown command ${cmd} — /help for the list.`);
|
|
171
199
|
return true;
|
|
172
200
|
}
|
|
173
|
-
}, [cwd, cycleMode, mode, push, quit, status]);
|
|
201
|
+
}, [cwd, cycleMode, mode, push, quit, refreshBalance, status]);
|
|
174
202
|
const submit = useCallback(async (message) => {
|
|
175
203
|
const text = message.trim();
|
|
176
204
|
setInput('');
|
|
@@ -219,10 +247,11 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
219
247
|
setStage('');
|
|
220
248
|
setTool('');
|
|
221
249
|
setPhase('ready');
|
|
250
|
+
void refreshBalance();
|
|
222
251
|
if (oneShot)
|
|
223
252
|
quit();
|
|
224
253
|
}
|
|
225
|
-
}, [oneShot, push, quit, runCommand, settleLive, status.balance]);
|
|
254
|
+
}, [oneShot, push, quit, refreshBalance, runCommand, settleLive, status.balance]);
|
|
226
255
|
useEffect(() => {
|
|
227
256
|
let cancelled = false;
|
|
228
257
|
(async () => {
|
|
@@ -244,6 +273,7 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
244
273
|
continuing: session.continuing,
|
|
245
274
|
hasLocalMemory: session.hasLocalMemory,
|
|
246
275
|
};
|
|
276
|
+
creditsUrlRef.current = session.bootstrap.credits_url ?? null;
|
|
247
277
|
void session.snapshot.then((snap) => {
|
|
248
278
|
if (!cancelled)
|
|
249
279
|
snapshotRef.current = snap;
|
|
@@ -329,16 +359,20 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
329
359
|
}
|
|
330
360
|
runnerRef.current?.setMcpTools(session.mcp.listTools());
|
|
331
361
|
});
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
sessionReadyRef.current = true;
|
|
363
|
+
setPhase(modeChosenRef.current ? 'ready' : 'mode');
|
|
364
|
+
runnerRef.current?.setMode(mode);
|
|
365
|
+
push('system', `${session.project}${session.bootstrap.created ? ' · new session' : ''}`);
|
|
334
366
|
if (session.continuing) {
|
|
335
367
|
push('notice', "Continuing this directory's thread — earlier turns are still in context.");
|
|
336
368
|
}
|
|
337
369
|
if (session.hasLocalMemory) {
|
|
338
370
|
push('notice', 'Project notes found in .rivine/local_memory.md.');
|
|
339
371
|
}
|
|
340
|
-
if (!oneShot)
|
|
372
|
+
if (!oneShot && modeChosenRef.current && !askForMode) {
|
|
373
|
+
push('system', `Mode: ${describeMode(mode)}`);
|
|
341
374
|
push('system', 'Ask for anything. /help for commands.');
|
|
375
|
+
}
|
|
342
376
|
if (oneShot)
|
|
343
377
|
void submit(oneShot);
|
|
344
378
|
}
|
|
@@ -428,10 +462,10 @@ export function App({ cwd, initialMode, oneShot }) {
|
|
|
428
462
|
if (phase === 'fatal') {
|
|
429
463
|
return (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { color: color('danger'), children: fatal }) }));
|
|
430
464
|
}
|
|
431
|
-
const compactBanner =
|
|
465
|
+
const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
|
|
432
466
|
const matches = completionsFor(input);
|
|
433
467
|
const busy = phase !== 'ready';
|
|
434
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [{ id: 'banner' }], children: (item) => _jsx(Banner, { compact: compactBanner }, item.id) }), _jsx(Static, { items: lines, children: (line) => _jsx(TranscriptLine, { line: line }, line.id) }), 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 === '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
|
|
468
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [{ id: 'banner' }], children: (item) => _jsx(Banner, { compact: compactBanner }, item.id) }), _jsx(Static, { items: lines, children: (line) => _jsx(TranscriptLine, { line: line }, line.id) }), 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 === 'mode' ? _jsx(ModePicker, { onPick: pickMode }) : 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
|
|
435
469
|
? { approved: true, feedback: value }
|
|
436
470
|
: { 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, { placeholder: "What should Rivine build?", onChange: setInput, onSubmit: (value) => void submit(value) })] }), _jsx(Completions, { matches: matches })] })) : null, phase === 'starting' ? _jsx(Activity, { stage: "Starting\u2026", tool: "" }) : null, _jsx(StatusBar, { info: {
|
|
437
471
|
mode,
|
package/dist/ui/components.js
CHANGED
|
@@ -39,6 +39,25 @@ export function Activity({ stage, tool }) {
|
|
|
39
39
|
const label = tool ? `${stage} · ${tool}` : stage;
|
|
40
40
|
return (_jsx(Box, { paddingX: 1, children: _jsx(Spinner, { label: fit(label, width - 6) }) }));
|
|
41
41
|
}
|
|
42
|
+
export function ModePicker({ onPick }) {
|
|
43
|
+
const width = terminalWidth();
|
|
44
|
+
const options = [
|
|
45
|
+
{ key: '1', mode: 'plan', label: 'Plan', detail: 'reads and answers, changes nothing' },
|
|
46
|
+
{ key: '2', mode: 'default', label: 'Ask me', detail: 'approve each write and command' },
|
|
47
|
+
{ key: '3', mode: 'acceptEdits', label: 'Accept edits', detail: 'writes apply, commands still ask' },
|
|
48
|
+
{ key: '4', mode: 'bypass', label: 'Full speed', detail: 'no prompts inside this project' },
|
|
49
|
+
];
|
|
50
|
+
useInput((input, key) => {
|
|
51
|
+
if (key.return) {
|
|
52
|
+
onPick('acceptEdits');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const found = options.find((o) => o.key === input);
|
|
56
|
+
if (found)
|
|
57
|
+
onPick(found.mode);
|
|
58
|
+
});
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "How should Rivine work in this project?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${o.key}` }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('text'), children: o.label.padEnd(13) }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 24)) })] }, o.key))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit('press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
|
|
60
|
+
}
|
|
42
61
|
export const SHELL_PANE_LINES = 12;
|
|
43
62
|
export function ShellPane({ text, live }) {
|
|
44
63
|
if (!text)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beunec/rivine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
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",
|