@beunec/rivine 0.1.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 +423 -0
- package/action.yml +165 -0
- package/dist/api/workspace.js +152 -0
- package/dist/auth/device.js +73 -0
- package/dist/auth/keychain.js +88 -0
- package/dist/auth/pkce.js +142 -0
- package/dist/auth/session.js +101 -0
- package/dist/config.js +37 -0
- package/dist/index.js +300 -0
- package/dist/mcp/config.js +73 -0
- package/dist/mcp/manager.js +154 -0
- package/dist/mcp/oauth.js +234 -0
- package/dist/mcp/relevance.js +74 -0
- package/dist/protocol/edit.js +247 -0
- package/dist/protocol/fsExecutor.js +471 -0
- package/dist/protocol/git.js +157 -0
- package/dist/protocol/permissions.js +210 -0
- package/dist/protocol/runner.js +325 -0
- package/dist/protocol/sessionKey.js +57 -0
- package/dist/protocol/shell.js +260 -0
- package/dist/protocol/snapshot.js +84 -0
- package/dist/protocol/stream.js +41 -0
- package/dist/runHeadless.js +140 -0
- package/dist/session.js +57 -0
- package/dist/ui/App.js +444 -0
- package/dist/ui/components.js +92 -0
- package/dist/ui/headless.js +130 -0
- package/dist/ui/render.js +109 -0
- package/dist/ui/slashCommands.js +57 -0
- package/dist/ui/theme.js +82 -0
- package/package.json +45 -0
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
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
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import { Box, Static, Text, useApp, useInput } from 'ink';
|
|
14
|
+
import { TextInput } from '@inkjs/ui';
|
|
15
|
+
import { InsufficientCredits } from '../api/workspace.js';
|
|
16
|
+
import { openBrowser } from '../auth/pkce.js';
|
|
17
|
+
import { siteUrl } from '../config.js';
|
|
18
|
+
import { TurnRunner } from '../protocol/runner.js';
|
|
19
|
+
import { describeMode, isBatchable, mcpServerOf, nextMode, } from '../protocol/permissions.js';
|
|
20
|
+
import { creditShortfall, startSession } from '../session.js';
|
|
21
|
+
import { sanitizeForModel } from '../protocol/shell.js';
|
|
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, terminalHeight, terminalWidth } from './render.js';
|
|
25
|
+
import { color } from './theme.js';
|
|
26
|
+
import { completionsFor } from './slashCommands.js';
|
|
27
|
+
function trimToPane(text) {
|
|
28
|
+
const lines = text.split('\n');
|
|
29
|
+
return lines.length > SHELL_PANE_LINES * 2
|
|
30
|
+
? lines.slice(-SHELL_PANE_LINES * 2).join('\n')
|
|
31
|
+
: text;
|
|
32
|
+
}
|
|
33
|
+
function grantKey(category, request) {
|
|
34
|
+
if (category !== 'mcp')
|
|
35
|
+
return category;
|
|
36
|
+
return `mcp:${mcpServerOf(String(request.args?.tool ?? '')) ?? 'unknown'}`;
|
|
37
|
+
}
|
|
38
|
+
export function App({ cwd, initialMode, oneShot }) {
|
|
39
|
+
const { exit } = useApp();
|
|
40
|
+
const [phase, setPhase] = useState('starting');
|
|
41
|
+
const [lines, setLines] = useState([]);
|
|
42
|
+
const [live, setLive] = useState('');
|
|
43
|
+
const [shellView, setShellView] = useState('');
|
|
44
|
+
const [shellActive, setShellActive] = useState(false);
|
|
45
|
+
const [stage, setStage] = useState('');
|
|
46
|
+
const [tool, setTool] = useState('');
|
|
47
|
+
const [mode, setMode] = useState(initialMode);
|
|
48
|
+
const [input, setInput] = useState('');
|
|
49
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
50
|
+
const [approval, setApproval] = useState(null);
|
|
51
|
+
const [question, setQuestion] = useState(null);
|
|
52
|
+
const [hitl, setHitl] = useState(null);
|
|
53
|
+
const [status, setStatus] = useState({
|
|
54
|
+
project: '',
|
|
55
|
+
cloudhandle: '',
|
|
56
|
+
bcidMasked: '',
|
|
57
|
+
balance: null,
|
|
58
|
+
});
|
|
59
|
+
const [fatal, setFatal] = useState('');
|
|
60
|
+
const runnerRef = useRef(null);
|
|
61
|
+
const mcpRef = useRef(null);
|
|
62
|
+
const snapshotRef = useRef(null);
|
|
63
|
+
const snapshotSentRef = useRef(false);
|
|
64
|
+
const answerRef = useRef(null);
|
|
65
|
+
const lineId = useRef(0);
|
|
66
|
+
const liveText = useRef('');
|
|
67
|
+
const shellBuf = useRef('');
|
|
68
|
+
const shellDirty = useRef(false);
|
|
69
|
+
const shellRef = useRef(null);
|
|
70
|
+
const sessionFactsRef = useRef({ continuing: false, hasLocalMemory: false });
|
|
71
|
+
const alwaysAllow = useRef(new Set());
|
|
72
|
+
const tokens = useMemo(() => new TokenBuffer((text) => {
|
|
73
|
+
liveText.current += text;
|
|
74
|
+
setLive(liveText.current);
|
|
75
|
+
}), []);
|
|
76
|
+
const push = useCallback((kind, text, detail) => {
|
|
77
|
+
if (!text)
|
|
78
|
+
return;
|
|
79
|
+
lineId.current += 1;
|
|
80
|
+
setLines((prev) => [...prev, { id: lineId.current, kind, text, detail }]);
|
|
81
|
+
}, []);
|
|
82
|
+
const settleLive = useCallback(() => {
|
|
83
|
+
tokens.flush();
|
|
84
|
+
const text = liveText.current.trim();
|
|
85
|
+
liveText.current = '';
|
|
86
|
+
setLive('');
|
|
87
|
+
if (text)
|
|
88
|
+
push('agent', text);
|
|
89
|
+
}, [push, tokens]);
|
|
90
|
+
const quit = useCallback(() => {
|
|
91
|
+
void runnerRef.current?.shutdown().finally(() => exit());
|
|
92
|
+
}, [exit]);
|
|
93
|
+
const cycleMode = useCallback(() => {
|
|
94
|
+
setMode((current) => {
|
|
95
|
+
const next = nextMode(current);
|
|
96
|
+
push('system', `Mode: ${describeMode(next)}`);
|
|
97
|
+
return next;
|
|
98
|
+
});
|
|
99
|
+
}, [push]);
|
|
100
|
+
const runCommand = useCallback((raw) => {
|
|
101
|
+
const cmd = raw.trim().toLowerCase();
|
|
102
|
+
if (!cmd.startsWith('/'))
|
|
103
|
+
return false;
|
|
104
|
+
switch (cmd) {
|
|
105
|
+
case '/help':
|
|
106
|
+
setShowHelp((v) => !v);
|
|
107
|
+
return true;
|
|
108
|
+
case '/exit':
|
|
109
|
+
case '/quit':
|
|
110
|
+
quit();
|
|
111
|
+
return true;
|
|
112
|
+
case '/clear':
|
|
113
|
+
setLines([]);
|
|
114
|
+
return true;
|
|
115
|
+
case '/mode':
|
|
116
|
+
cycleMode();
|
|
117
|
+
return true;
|
|
118
|
+
case '/plan':
|
|
119
|
+
setMode('plan');
|
|
120
|
+
push('system', `Mode: ${describeMode('plan')}`);
|
|
121
|
+
return true;
|
|
122
|
+
case '/accept':
|
|
123
|
+
setMode('acceptEdits');
|
|
124
|
+
push('system', `Mode: ${describeMode('acceptEdits')}`);
|
|
125
|
+
return true;
|
|
126
|
+
case '/credits':
|
|
127
|
+
openBrowser(`${siteUrl()}/credits`);
|
|
128
|
+
push('system', `Opened ${siteUrl()}/credits`);
|
|
129
|
+
return true;
|
|
130
|
+
case '/mcp': {
|
|
131
|
+
const reports = mcpRef.current?.getReports() ?? [];
|
|
132
|
+
if (reports.length === 0) {
|
|
133
|
+
push('system', 'No MCP servers configured. Add one with: rivine mcp add <name> <command…>');
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
for (const r of reports) {
|
|
137
|
+
push(r.ok ? 'system' : 'notice', r.ok ? `${r.name} — ${r.toolCount} tool(s)` : `${r.name} — ${r.error}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
case '/status': {
|
|
143
|
+
push('system', `${status.cloudhandle || 'signed in'} ${status.bcidMasked} · ${status.project} · ` +
|
|
144
|
+
`${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} · ${describeMode(mode)}`);
|
|
145
|
+
const facts = sessionFactsRef.current;
|
|
146
|
+
push('system', `${facts.continuing ? "continuing this directory's thread" : 'new thread'} · ` +
|
|
147
|
+
`${facts.hasLocalMemory ? 'project notes on disk' : 'no project notes yet'}`);
|
|
148
|
+
const branch = gitBranchLine(cwd);
|
|
149
|
+
if (branch)
|
|
150
|
+
push('system', branch);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
case '/diff': {
|
|
154
|
+
push('system', 'git diff --stat');
|
|
155
|
+
void gitDiff(cwd).then((res) => {
|
|
156
|
+
push(res.ok ? 'system' : 'notice', res.output || 'No changes.');
|
|
157
|
+
});
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
case '/pr':
|
|
161
|
+
case '/pr push': {
|
|
162
|
+
const withPush = cmd === '/pr push';
|
|
163
|
+
push('system', withPush ? 'Pushing and opening a pull request…' : 'Opening a pull request with gh…');
|
|
164
|
+
void prCreate(cwd, { push: withPush }).then((res) => {
|
|
165
|
+
push(res.ok ? 'system' : 'notice', res.output || 'Done.');
|
|
166
|
+
});
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
default:
|
|
170
|
+
push('notice', `Unknown command ${cmd} — /help for the list.`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}, [cwd, cycleMode, mode, push, quit, status]);
|
|
174
|
+
const submit = useCallback(async (message) => {
|
|
175
|
+
const text = message.trim();
|
|
176
|
+
setInput('');
|
|
177
|
+
if (!runnerRef.current)
|
|
178
|
+
return;
|
|
179
|
+
if (runnerRef.current.interactiveShell) {
|
|
180
|
+
runnerRef.current.writeToShell(`${message}\n`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!text)
|
|
184
|
+
return;
|
|
185
|
+
if (runCommand(text))
|
|
186
|
+
return;
|
|
187
|
+
const needed = creditShortfall(status.balance, { rivine_code: 10 });
|
|
188
|
+
if (needed !== null) {
|
|
189
|
+
push('error', `Rivine Code needs $${needed.toFixed(2)} in credits to run.`);
|
|
190
|
+
push('notice', `Top up at ${siteUrl()}/credits`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
setShowHelp(false);
|
|
194
|
+
push('user', text);
|
|
195
|
+
setPhase('running');
|
|
196
|
+
setStage('Thinking…');
|
|
197
|
+
setTool('');
|
|
198
|
+
const snapshot = snapshotSentRef.current ? null : snapshotRef.current;
|
|
199
|
+
snapshotSentRef.current = true;
|
|
200
|
+
try {
|
|
201
|
+
const result = await runnerRef.current.run(text, snapshot);
|
|
202
|
+
settleLive();
|
|
203
|
+
if (result.error)
|
|
204
|
+
push('error', result.error);
|
|
205
|
+
else if (result.answer)
|
|
206
|
+
push('agent', result.answer);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
settleLive();
|
|
210
|
+
if (err instanceof InsufficientCredits) {
|
|
211
|
+
push('error', err.detail);
|
|
212
|
+
push('notice', `Top up at ${siteUrl()}/credits`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
push('error', err.message);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
setStage('');
|
|
220
|
+
setTool('');
|
|
221
|
+
setPhase('ready');
|
|
222
|
+
if (oneShot)
|
|
223
|
+
quit();
|
|
224
|
+
}
|
|
225
|
+
}, [oneShot, push, quit, runCommand, settleLive, status.balance]);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
let cancelled = false;
|
|
228
|
+
(async () => {
|
|
229
|
+
try {
|
|
230
|
+
const session = await startSession({
|
|
231
|
+
cwd,
|
|
232
|
+
onNotice: (text) => push('system', text),
|
|
233
|
+
onLoginUrl: (url) => push('notice', `If your browser did not open: ${url}`),
|
|
234
|
+
});
|
|
235
|
+
if (cancelled)
|
|
236
|
+
return;
|
|
237
|
+
setStatus((s) => ({
|
|
238
|
+
...s,
|
|
239
|
+
project: session.project,
|
|
240
|
+
cloudhandle: session.bootstrap.cloudhandle ?? '',
|
|
241
|
+
bcidMasked: session.bootstrap.bcid_masked ?? '',
|
|
242
|
+
}));
|
|
243
|
+
sessionFactsRef.current = {
|
|
244
|
+
continuing: session.continuing,
|
|
245
|
+
hasLocalMemory: session.hasLocalMemory,
|
|
246
|
+
};
|
|
247
|
+
void session.snapshot.then((snap) => {
|
|
248
|
+
if (!cancelled)
|
|
249
|
+
snapshotRef.current = snap;
|
|
250
|
+
});
|
|
251
|
+
void session.balance.then((balance) => {
|
|
252
|
+
if (cancelled)
|
|
253
|
+
return;
|
|
254
|
+
setStatus((s) => ({ ...s, balance }));
|
|
255
|
+
if (balance === null) {
|
|
256
|
+
push('notice', 'Could not read your credit balance — the local $10 check is not running.');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
mcpRef.current = session.mcp;
|
|
260
|
+
runnerRef.current = new TurnRunner({
|
|
261
|
+
workspaceId: session.bootstrap.workspace_id,
|
|
262
|
+
chatSpaceId: session.bootstrap.chat_space_id,
|
|
263
|
+
sessionId: session.bootstrap.session_id,
|
|
264
|
+
root: cwd,
|
|
265
|
+
mode,
|
|
266
|
+
}, {
|
|
267
|
+
onToken: (text) => tokens.push(text),
|
|
268
|
+
onStage: (label) => setStage(label),
|
|
269
|
+
onTool: (name, detail) => {
|
|
270
|
+
setTool(detail ? `${name} ${detail}` : name);
|
|
271
|
+
if (name === 'execute' && detail) {
|
|
272
|
+
settleLive();
|
|
273
|
+
push('run', detail);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
onFileTouched: (path, fileStatus) => {
|
|
277
|
+
if (fileStatus === 'written') {
|
|
278
|
+
settleLive();
|
|
279
|
+
push('file', path.replace(/^\/workspace\//, ''));
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
onNotice: (text) => push('notice', text),
|
|
283
|
+
onOutput: (text) => {
|
|
284
|
+
shellBuf.current = trimToPane(shellBuf.current + sanitizeForModel(text));
|
|
285
|
+
shellDirty.current = true;
|
|
286
|
+
},
|
|
287
|
+
onShell: (handle) => {
|
|
288
|
+
shellRef.current = handle;
|
|
289
|
+
setShellActive(Boolean(handle));
|
|
290
|
+
if (!handle) {
|
|
291
|
+
setShellView(shellBuf.current);
|
|
292
|
+
shellBuf.current = '';
|
|
293
|
+
shellDirty.current = false;
|
|
294
|
+
setTimeout(() => setShellView(''), 1200);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
requestApproval: (pending) => new Promise((resolve) => {
|
|
298
|
+
if (alwaysAllow.current.has(grantKey(pending.category, pending.request))) {
|
|
299
|
+
resolve(true);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
settleLive();
|
|
303
|
+
answerRef.current = resolve;
|
|
304
|
+
setApproval(pending);
|
|
305
|
+
setPhase('approval');
|
|
306
|
+
}),
|
|
307
|
+
requestDecision: (request) => new Promise((resolve) => {
|
|
308
|
+
settleLive();
|
|
309
|
+
answerRef.current = resolve;
|
|
310
|
+
setHitl(request);
|
|
311
|
+
setPhase('hitl');
|
|
312
|
+
}),
|
|
313
|
+
askQuestion: (q) => new Promise((resolve) => {
|
|
314
|
+
settleLive();
|
|
315
|
+
answerRef.current = resolve;
|
|
316
|
+
setQuestion(q);
|
|
317
|
+
setPhase('question');
|
|
318
|
+
}),
|
|
319
|
+
}, session.mcp);
|
|
320
|
+
void session.mcpReady.then((reports) => {
|
|
321
|
+
if (cancelled || reports.length === 0)
|
|
322
|
+
return;
|
|
323
|
+
const good = reports.filter((r) => r.ok);
|
|
324
|
+
const tools = session.mcp.listTools().length;
|
|
325
|
+
if (good.length)
|
|
326
|
+
push('system', `MCP: ${good.length} server(s), ${tools} tool(s) available.`);
|
|
327
|
+
for (const bad of reports.filter((r) => !r.ok)) {
|
|
328
|
+
push('notice', `MCP server "${bad.name}" did not start: ${bad.error}`);
|
|
329
|
+
}
|
|
330
|
+
runnerRef.current?.setMcpTools(session.mcp.listTools());
|
|
331
|
+
});
|
|
332
|
+
setPhase('ready');
|
|
333
|
+
push('system', `${session.project}${session.bootstrap.created ? ' · new session' : ''} · ${describeMode(mode)}`);
|
|
334
|
+
if (session.continuing) {
|
|
335
|
+
push('notice', "Continuing this directory's thread — earlier turns are still in context.");
|
|
336
|
+
}
|
|
337
|
+
if (session.hasLocalMemory) {
|
|
338
|
+
push('notice', 'Project notes found in .rivine/local_memory.md.');
|
|
339
|
+
}
|
|
340
|
+
if (!oneShot)
|
|
341
|
+
push('system', 'Ask for anything. /help for commands.');
|
|
342
|
+
if (oneShot)
|
|
343
|
+
void submit(oneShot);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
if (cancelled)
|
|
347
|
+
return;
|
|
348
|
+
setFatal(err.message || String(err));
|
|
349
|
+
setPhase('fatal');
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
return () => {
|
|
353
|
+
cancelled = true;
|
|
354
|
+
tokens.dispose();
|
|
355
|
+
};
|
|
356
|
+
}, []);
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
runnerRef.current?.setMode(mode);
|
|
359
|
+
}, [mode]);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!shellActive)
|
|
362
|
+
return undefined;
|
|
363
|
+
const timer = setInterval(() => {
|
|
364
|
+
if (!shellDirty.current)
|
|
365
|
+
return;
|
|
366
|
+
shellDirty.current = false;
|
|
367
|
+
setShellView(shellBuf.current);
|
|
368
|
+
}, 80);
|
|
369
|
+
return () => clearInterval(timer);
|
|
370
|
+
}, [shellActive]);
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (!shellActive)
|
|
373
|
+
return undefined;
|
|
374
|
+
const onResize = () => {
|
|
375
|
+
runnerRef.current?.resizeShell(process.stdout.columns || 120, process.stdout.rows || 30);
|
|
376
|
+
};
|
|
377
|
+
process.stdout.on('resize', onResize);
|
|
378
|
+
return () => {
|
|
379
|
+
process.stdout.off('resize', onResize);
|
|
380
|
+
};
|
|
381
|
+
}, [shellActive]);
|
|
382
|
+
useInput((ch, key) => {
|
|
383
|
+
if (key.tab && key.shift) {
|
|
384
|
+
cycleMode();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (key.escape && runnerRef.current?.killShell()) {
|
|
388
|
+
push('notice', 'Stopped the running command.');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (key.ctrl && ch === 'c')
|
|
392
|
+
quit();
|
|
393
|
+
}, { isActive: phase === 'ready' || phase === 'running' });
|
|
394
|
+
const answerApproval = useCallback((choice) => {
|
|
395
|
+
const resolve = answerRef.current;
|
|
396
|
+
const pending = approval;
|
|
397
|
+
answerRef.current = null;
|
|
398
|
+
setApproval(null);
|
|
399
|
+
setPhase('running');
|
|
400
|
+
if (choice === 'always' && pending) {
|
|
401
|
+
const key = grantKey(pending.category, pending.request);
|
|
402
|
+
alwaysAllow.current.add(key);
|
|
403
|
+
push('system', pending.category === 'mcp'
|
|
404
|
+
? `Allowing further calls to ${key.replace('mcp:', '')} this session.`
|
|
405
|
+
: `Allowing further ${pending.category} operations this session.`);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
push(choice === 'no' ? 'notice' : 'system', choice === 'no' ? 'Skipped.' : 'Allowed.');
|
|
409
|
+
}
|
|
410
|
+
resolve?.(choice !== 'no');
|
|
411
|
+
}, [approval, push]);
|
|
412
|
+
const answerQuestion = useCallback((value) => {
|
|
413
|
+
const resolve = answerRef.current;
|
|
414
|
+
answerRef.current = null;
|
|
415
|
+
setQuestion(null);
|
|
416
|
+
setPhase('running');
|
|
417
|
+
push('user', value || '(skipped)');
|
|
418
|
+
resolve?.(value);
|
|
419
|
+
}, [push]);
|
|
420
|
+
const answerHitl = useCallback((answer) => {
|
|
421
|
+
const resolve = answerRef.current;
|
|
422
|
+
answerRef.current = null;
|
|
423
|
+
setHitl(null);
|
|
424
|
+
setPhase('running');
|
|
425
|
+
push(answer.approved ? 'system' : 'notice', answer.approved ? 'Approved.' : 'Declined.');
|
|
426
|
+
resolve?.(answer);
|
|
427
|
+
}, [push]);
|
|
428
|
+
if (phase === 'fatal') {
|
|
429
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { color: color('danger'), children: fatal }) }));
|
|
430
|
+
}
|
|
431
|
+
const compactBanner = terminalHeight() < 20 || terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
|
|
432
|
+
const matches = completionsFor(input);
|
|
433
|
+
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
|
|
435
|
+
? { approved: true, feedback: value }
|
|
436
|
+
: { 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
|
+
mode,
|
|
438
|
+
project: status.project,
|
|
439
|
+
cloudhandle: status.cloudhandle,
|
|
440
|
+
bcidMasked: status.bcidMasked,
|
|
441
|
+
balance: status.balance,
|
|
442
|
+
busy,
|
|
443
|
+
} })] }));
|
|
444
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
12
|
+
import { Box, Text, useInput } from 'ink';
|
|
13
|
+
import { Spinner } from '@inkjs/ui';
|
|
14
|
+
import { ASCII_ONLY, BOX, color, glyph, lineColor } from './theme.js';
|
|
15
|
+
import { fit, terminalWidth, wrap } from './render.js';
|
|
16
|
+
import { SLASH_COMMANDS } from './slashCommands.js';
|
|
17
|
+
import { describeMode } from '../protocol/permissions.js';
|
|
18
|
+
const WORDMARK = [
|
|
19
|
+
'█▀▄ ▀█▀ █ █ ▀█▀ █▄ █ █▀▀ █▀▀ █▀█ █▀▄ █▀▀',
|
|
20
|
+
'█▀▄ █ ▀▄ ▄▀ █ █ ▀█ █▀ █ █ █ █ █ █▀ ',
|
|
21
|
+
'▀ ▀ ▄█▄ ▀ ▄█▄ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀',
|
|
22
|
+
];
|
|
23
|
+
export const WORDMARK_MIN_COLUMNS = 49;
|
|
24
|
+
const WORDMARK_ASCII = ['R I V I N E C O D E'];
|
|
25
|
+
export function Banner({ compact }) {
|
|
26
|
+
if (compact) {
|
|
27
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: color('brand'), bold: true, children: "Rivine Code" }), _jsx(Text, { color: color('faint'), children: " \u00B7 Beunec" })] }));
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [(ASCII_ONLY ? WORDMARK_ASCII : WORDMARK).map((row, i) => (_jsx(Text, { color: color('brand'), bold: true, children: row }, i))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: "by Beunec Technologies, Inc." }) })] }));
|
|
30
|
+
}
|
|
31
|
+
export function TranscriptLine({ line }) {
|
|
32
|
+
const width = terminalWidth();
|
|
33
|
+
const body = width - 4;
|
|
34
|
+
const lines = line.kind === 'agent' ? wrap(line.text, body) : [fit(line.text, body, line.kind === 'file')];
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [lines.map((text, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: lineColor(line.kind), children: [i === 0 ? glyph(line.kind) : ' ', " "] }), _jsx(Text, { color: line.kind === 'agent' ? color('text') : lineColor(line.kind), children: text })] }, i))), line.detail ? (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: color('faint'), children: fit(line.detail, body - 2) }) })) : null] }));
|
|
36
|
+
}
|
|
37
|
+
export function Activity({ stage, tool }) {
|
|
38
|
+
const width = terminalWidth();
|
|
39
|
+
const label = tool ? `${stage} · ${tool}` : stage;
|
|
40
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Spinner, { label: fit(label, width - 6) }) }));
|
|
41
|
+
}
|
|
42
|
+
export const SHELL_PANE_LINES = 12;
|
|
43
|
+
export function ShellPane({ text, live }) {
|
|
44
|
+
if (!text)
|
|
45
|
+
return null;
|
|
46
|
+
const width = terminalWidth();
|
|
47
|
+
const lines = text.split('\n').slice(-SHELL_PANE_LINES);
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [lines.map((line, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: color('faint'), children: [BOX.v, " "] }), _jsx(Text, { color: color('muted'), children: fit(line, width - 5) })] }, i))), live ? (_jsxs(Box, { children: [_jsxs(Text, { color: color('faint'), children: [BOX.v, " "] }), _jsx(Text, { color: color('faint'), children: "type to answer it \u00B7 esc to stop" })] })) : null] }));
|
|
49
|
+
}
|
|
50
|
+
export function ApprovalPrompt({ approval, allowAlways, onChoice, }) {
|
|
51
|
+
const width = terminalWidth();
|
|
52
|
+
useInput((input, key) => {
|
|
53
|
+
const k = input.toLowerCase();
|
|
54
|
+
if (k === 'y' || key.return)
|
|
55
|
+
onChoice('once');
|
|
56
|
+
else if (k === 'a' && allowAlways)
|
|
57
|
+
onChoice('always');
|
|
58
|
+
else if (k === 'n' || key.escape)
|
|
59
|
+
onChoice('no');
|
|
60
|
+
});
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: color('warn'), children: [glyph('notice'), " "] }), _jsx(Text, { color: color('text'), bold: true, children: fit(approval.summary, width - 4) })] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: color('muted'), children: fit(approval.reason, width - 6) }) }), _jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: color('success'), children: "y" }), _jsx(Text, { color: color('faint'), children: " allow " }), allowAlways ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('accent'), children: "a" }), _jsx(Text, { color: color('faint'), children: " allow all this session " })] })) : null, _jsx(Text, { color: color('danger'), children: "n" }), _jsx(Text, { color: color('faint'), children: " skip " }), _jsx(Text, { color: color('faint'), children: "esc cancel" })] })] }));
|
|
62
|
+
}
|
|
63
|
+
export function ChoicePrompt({ prompt, options, onPick, }) {
|
|
64
|
+
const width = terminalWidth();
|
|
65
|
+
const shown = options.slice(0, 9);
|
|
66
|
+
useInput((input, key) => {
|
|
67
|
+
if (key.escape) {
|
|
68
|
+
onPick('');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const index = Number.parseInt(input, 10);
|
|
72
|
+
if (Number.isInteger(index) && index >= 1 && index <= shown.length) {
|
|
73
|
+
onPick(shown[index - 1]);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: wrap(prompt, width - 4).map((text, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: color('accent'), children: [i === 0 ? '?' : ' ', " "] }), _jsx(Text, { color: color('text'), bold: true, children: text })] }, i))) }), _jsx(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, children: shown.map((option, i) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: i + 1 }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('text'), children: fit(option, width - 8) })] }, option))) }), _jsx(Box, { paddingLeft: 2, marginTop: 1, children: _jsx(Text, { color: color('faint'), children: "press a number \u00B7 esc to skip" }) })] }));
|
|
77
|
+
}
|
|
78
|
+
export function Completions({ matches }) {
|
|
79
|
+
if (matches.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const width = terminalWidth();
|
|
82
|
+
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] }));
|
|
83
|
+
}
|
|
84
|
+
export function HelpPanel() {
|
|
85
|
+
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" })] })] }));
|
|
86
|
+
}
|
|
87
|
+
export function StatusBar({ info }) {
|
|
88
|
+
const width = terminalWidth();
|
|
89
|
+
const narrow = width < 76;
|
|
90
|
+
const dot = info.busy ? color('warn') : color('success');
|
|
91
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: dot, children: [BOX.dot, " "] }), _jsx(Text, { color: color('muted'), children: describeMode(info.mode) }), info.project ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('muted'), children: fit(info.project, 18, true) })] })) : null, !narrow && info.cloudhandle ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('brand'), children: fit(info.cloudhandle, 22) }), info.bcidMasked ? _jsx(Text, { color: color('faint'), children: ` ${info.bcidMasked}` }) : null] })) : null, info.balance !== null ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsxs(Text, { color: info.balance < 10 ? color('warn') : color('muted'), children: ["$", info.balance.toFixed(2)] })] })) : null, !narrow ? _jsx(Text, { color: color('faint'), children: ' /help' }) : null] }));
|
|
92
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
import { fit, tail, terminalWidth, wrap } from './render.js';
|
|
12
|
+
import { glyph } from './theme.js';
|
|
13
|
+
export function isInteractiveTerminal() {
|
|
14
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
15
|
+
}
|
|
16
|
+
export class HeadlessRenderer {
|
|
17
|
+
mode;
|
|
18
|
+
json;
|
|
19
|
+
answered = false;
|
|
20
|
+
constructor(mode, json = false) {
|
|
21
|
+
this.mode = mode;
|
|
22
|
+
this.json = json;
|
|
23
|
+
}
|
|
24
|
+
write(text) {
|
|
25
|
+
if (this.json)
|
|
26
|
+
process.stderr.write(text);
|
|
27
|
+
else
|
|
28
|
+
process.stdout.write(text);
|
|
29
|
+
}
|
|
30
|
+
line(kind, text) {
|
|
31
|
+
if (!text)
|
|
32
|
+
return;
|
|
33
|
+
const width = terminalWidth();
|
|
34
|
+
for (const [i, part] of wrap(text, width - 2).entries()) {
|
|
35
|
+
this.write(`${i === 0 ? glyph(kind) : ' '} ${part}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
token(text) {
|
|
39
|
+
if (!this.answered && text) {
|
|
40
|
+
this.answered = true;
|
|
41
|
+
this.write('\n');
|
|
42
|
+
}
|
|
43
|
+
this.write(text);
|
|
44
|
+
}
|
|
45
|
+
endAnswer() {
|
|
46
|
+
if (this.answered) {
|
|
47
|
+
this.write('\n');
|
|
48
|
+
this.answered = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
shellOutput(text) {
|
|
52
|
+
if (text)
|
|
53
|
+
process.stderr.write(text);
|
|
54
|
+
}
|
|
55
|
+
status(text) {
|
|
56
|
+
if (text)
|
|
57
|
+
process.stderr.write(` ${fit(text, terminalWidth() - 4)}\n`);
|
|
58
|
+
}
|
|
59
|
+
output(text, maxLines = 20) {
|
|
60
|
+
const { lines, hidden } = tail(text, maxLines);
|
|
61
|
+
if (hidden > 0)
|
|
62
|
+
this.write(` … ${hidden} earlier lines\n`);
|
|
63
|
+
for (const l of lines)
|
|
64
|
+
this.write(` ${l}\n`);
|
|
65
|
+
}
|
|
66
|
+
result(payload) {
|
|
67
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
68
|
+
}
|
|
69
|
+
refused = 0;
|
|
70
|
+
get refusalCount() {
|
|
71
|
+
return this.refused;
|
|
72
|
+
}
|
|
73
|
+
refuseApproval(approval) {
|
|
74
|
+
const fix = this.mode === 'plan'
|
|
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';
|
|
77
|
+
this.refused += 1;
|
|
78
|
+
this.line('notice', `Skipped: ${approval.summary}`);
|
|
79
|
+
this.line('notice', ` ${approval.reason} (${fix})`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
refuseQuestion(prompt) {
|
|
83
|
+
this.refused += 1;
|
|
84
|
+
this.line('notice', `Question skipped (no terminal to answer on): ${fit(prompt, 100)}`);
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function installTeardown(onExit) {
|
|
89
|
+
let finished = false;
|
|
90
|
+
const restore = () => {
|
|
91
|
+
try {
|
|
92
|
+
if (process.stdin.isTTY && process.stdin.setRawMode)
|
|
93
|
+
process.stdin.setRawMode(false);
|
|
94
|
+
if (process.stdout.isTTY)
|
|
95
|
+
process.stdout.write('[?25h[0m');
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const finish = async (code) => {
|
|
101
|
+
if (finished)
|
|
102
|
+
return;
|
|
103
|
+
finished = true;
|
|
104
|
+
try {
|
|
105
|
+
await onExit();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
}
|
|
109
|
+
restore();
|
|
110
|
+
process.exit(code);
|
|
111
|
+
};
|
|
112
|
+
const onSigint = () => void finish(130);
|
|
113
|
+
const onSigterm = () => void finish(143);
|
|
114
|
+
const onFatal = (err) => {
|
|
115
|
+
restore();
|
|
116
|
+
process.stderr.write(`\n ${err?.message ?? String(err)}\n\n`);
|
|
117
|
+
void finish(1);
|
|
118
|
+
};
|
|
119
|
+
process.on('SIGINT', onSigint);
|
|
120
|
+
process.on('SIGTERM', onSigterm);
|
|
121
|
+
process.on('uncaughtException', onFatal);
|
|
122
|
+
process.on('unhandledRejection', onFatal);
|
|
123
|
+
return () => {
|
|
124
|
+
process.off('SIGINT', onSigint);
|
|
125
|
+
process.off('SIGTERM', onSigterm);
|
|
126
|
+
process.off('uncaughtException', onFatal);
|
|
127
|
+
process.off('unhandledRejection', onFatal);
|
|
128
|
+
restore();
|
|
129
|
+
};
|
|
130
|
+
}
|