@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
|
@@ -0,0 +1,260 @@
|
|
|
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 { spawn } from 'node:child_process';
|
|
12
|
+
import { chmodSync, existsSync, statSync } from 'node:fs';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
export const MAX_OUTPUT_CHARS = 120_000;
|
|
16
|
+
const KILL_GRACE_MS = 5_000;
|
|
17
|
+
const CSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
18
|
+
const OSC = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g;
|
|
19
|
+
const STRAY = /\u001b[@-Z\\-_]|[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
20
|
+
export function sanitizeForModel(raw) {
|
|
21
|
+
const clean = raw.replace(OSC, '').replace(CSI, '').replace(/\r\n/g, '\n');
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const line of clean.split('\n')) {
|
|
24
|
+
const settled = line.includes('\r') ? line.slice(line.lastIndexOf('\r') + 1) : line;
|
|
25
|
+
out.push(settled.replace(STRAY, ''));
|
|
26
|
+
}
|
|
27
|
+
return out.join('\n').replace(/\n{4,}/g, '\n\n\n');
|
|
28
|
+
}
|
|
29
|
+
let ptyChecked = false;
|
|
30
|
+
let ptyModule = null;
|
|
31
|
+
function ensureSpawnHelperIsExecutable() {
|
|
32
|
+
try {
|
|
33
|
+
const require_ = createRequire(import.meta.url);
|
|
34
|
+
const root = dirname(dirname(require_.resolve('node-pty')));
|
|
35
|
+
const helperPath = join(root, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper');
|
|
36
|
+
if (!existsSync(helperPath))
|
|
37
|
+
return;
|
|
38
|
+
if (statSync(helperPath).mode & 0o111)
|
|
39
|
+
return;
|
|
40
|
+
chmodSync(helperPath, 0o755);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function loadPty() {
|
|
46
|
+
if (ptyChecked)
|
|
47
|
+
return ptyModule;
|
|
48
|
+
ptyChecked = true;
|
|
49
|
+
if (process.env.RIVINE_NO_PTY)
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const specifier = 'node-pty';
|
|
53
|
+
const mod = await import(specifier);
|
|
54
|
+
if (process.platform !== 'win32')
|
|
55
|
+
ensureSpawnHelperIsExecutable();
|
|
56
|
+
if (typeof mod?.spawn === 'function')
|
|
57
|
+
ptyModule = mod;
|
|
58
|
+
else if (typeof mod?.default?.spawn === 'function')
|
|
59
|
+
ptyModule = mod.default;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
ptyModule = null;
|
|
63
|
+
}
|
|
64
|
+
return ptyModule;
|
|
65
|
+
}
|
|
66
|
+
export function resetPtyProbeForTests() {
|
|
67
|
+
ptyChecked = false;
|
|
68
|
+
ptyModule = null;
|
|
69
|
+
}
|
|
70
|
+
class Tail {
|
|
71
|
+
limit;
|
|
72
|
+
buffer = '';
|
|
73
|
+
dropped = 0;
|
|
74
|
+
constructor(limit) {
|
|
75
|
+
this.limit = limit;
|
|
76
|
+
}
|
|
77
|
+
push(text) {
|
|
78
|
+
this.buffer += text;
|
|
79
|
+
if (this.buffer.length > this.limit * 2) {
|
|
80
|
+
const excess = this.buffer.length - this.limit;
|
|
81
|
+
this.dropped += excess;
|
|
82
|
+
this.buffer = this.buffer.slice(excess);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
settle() {
|
|
86
|
+
let text = sanitizeForModel(this.buffer);
|
|
87
|
+
let dropped = this.dropped;
|
|
88
|
+
if (text.length > this.limit) {
|
|
89
|
+
dropped += text.length - this.limit;
|
|
90
|
+
text = text.slice(-this.limit);
|
|
91
|
+
}
|
|
92
|
+
if (dropped > 0) {
|
|
93
|
+
return {
|
|
94
|
+
output: `…[${dropped} characters trimmed from the start]…\n${text}`,
|
|
95
|
+
truncated: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { output: text, truncated: false };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function shellInvocation(command) {
|
|
102
|
+
if (process.platform === 'win32') {
|
|
103
|
+
return { file: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command] };
|
|
104
|
+
}
|
|
105
|
+
return { file: process.env.SHELL || '/bin/sh', args: ['-c', command] };
|
|
106
|
+
}
|
|
107
|
+
function runWithPty(pty, command, options) {
|
|
108
|
+
const { file, args } = shellInvocation(command);
|
|
109
|
+
const tail = new Tail(MAX_OUTPUT_CHARS);
|
|
110
|
+
const child = pty.spawn(file, args, {
|
|
111
|
+
name: 'xterm-256color',
|
|
112
|
+
cols: options.cols ?? 120,
|
|
113
|
+
rows: options.rows ?? 30,
|
|
114
|
+
cwd: options.cwd,
|
|
115
|
+
env: process.env,
|
|
116
|
+
});
|
|
117
|
+
let settled = false;
|
|
118
|
+
let timedOut = false;
|
|
119
|
+
let killTimer = null;
|
|
120
|
+
const result = new Promise((resolvePromise) => {
|
|
121
|
+
const finish = (code, note) => {
|
|
122
|
+
if (settled)
|
|
123
|
+
return;
|
|
124
|
+
settled = true;
|
|
125
|
+
clearTimeout(deadline);
|
|
126
|
+
if (killTimer)
|
|
127
|
+
clearTimeout(killTimer);
|
|
128
|
+
if (note)
|
|
129
|
+
tail.push(`\n${note}\n`);
|
|
130
|
+
const { output, truncated } = tail.settle();
|
|
131
|
+
resolvePromise({ output, exit_code: code, truncated, tty: true });
|
|
132
|
+
};
|
|
133
|
+
child.onData((data) => {
|
|
134
|
+
tail.push(data);
|
|
135
|
+
options.onChunk?.(data);
|
|
136
|
+
});
|
|
137
|
+
child.onExit(({ exitCode, signal }) => {
|
|
138
|
+
if (timedOut) {
|
|
139
|
+
finish(124, `[command exceeded ${Math.round(options.timeoutMs / 1000)}s and was terminated]`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
finish(exitCode || (signal ? 128 + signal : 0));
|
|
143
|
+
});
|
|
144
|
+
const deadline = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
try {
|
|
147
|
+
child.kill();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
}
|
|
151
|
+
killTimer = setTimeout(() => {
|
|
152
|
+
try {
|
|
153
|
+
child.kill('SIGKILL');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
}
|
|
157
|
+
finish(124, `[command exceeded ${Math.round(options.timeoutMs / 1000)}s and was killed]`);
|
|
158
|
+
}, KILL_GRACE_MS);
|
|
159
|
+
}, options.timeoutMs);
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
result,
|
|
163
|
+
tty: true,
|
|
164
|
+
write: (data) => {
|
|
165
|
+
try {
|
|
166
|
+
child.write(data);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
resize: (cols, rows) => {
|
|
172
|
+
try {
|
|
173
|
+
child.resize(Math.max(2, cols), Math.max(2, rows));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
kill: () => {
|
|
179
|
+
try {
|
|
180
|
+
child.kill();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function runWithPipes(command, options) {
|
|
188
|
+
const { file, args } = shellInvocation(command);
|
|
189
|
+
const tail = new Tail(MAX_OUTPUT_CHARS);
|
|
190
|
+
const child = spawn(file, args, {
|
|
191
|
+
cwd: options.cwd,
|
|
192
|
+
env: process.env,
|
|
193
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
194
|
+
});
|
|
195
|
+
let settled = false;
|
|
196
|
+
let timedOut = false;
|
|
197
|
+
let killTimer = null;
|
|
198
|
+
const result = new Promise((resolvePromise) => {
|
|
199
|
+
const finish = (code, note) => {
|
|
200
|
+
if (settled)
|
|
201
|
+
return;
|
|
202
|
+
settled = true;
|
|
203
|
+
clearTimeout(deadline);
|
|
204
|
+
if (killTimer)
|
|
205
|
+
clearTimeout(killTimer);
|
|
206
|
+
if (note)
|
|
207
|
+
tail.push(`\n${note}\n`);
|
|
208
|
+
const { output, truncated } = tail.settle();
|
|
209
|
+
resolvePromise({ output, exit_code: code, truncated, tty: false });
|
|
210
|
+
};
|
|
211
|
+
const absorb = (chunk) => {
|
|
212
|
+
const text = chunk.toString();
|
|
213
|
+
tail.push(text);
|
|
214
|
+
options.onChunk?.(text);
|
|
215
|
+
};
|
|
216
|
+
child.stdout?.on('data', absorb);
|
|
217
|
+
child.stderr?.on('data', absorb);
|
|
218
|
+
child.on('error', (err) => {
|
|
219
|
+
tail.push(String(err.message));
|
|
220
|
+
finish(127);
|
|
221
|
+
});
|
|
222
|
+
child.on('close', (code, signal) => {
|
|
223
|
+
if (timedOut) {
|
|
224
|
+
finish(124, `[command exceeded ${Math.round(options.timeoutMs / 1000)}s and was terminated]`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
finish(code ?? (signal ? 128 : 0));
|
|
228
|
+
});
|
|
229
|
+
const deadline = setTimeout(() => {
|
|
230
|
+
timedOut = true;
|
|
231
|
+
child.kill('SIGTERM');
|
|
232
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), KILL_GRACE_MS);
|
|
233
|
+
}, options.timeoutMs);
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
result,
|
|
237
|
+
tty: false,
|
|
238
|
+
write: (data) => {
|
|
239
|
+
try {
|
|
240
|
+
child.stdin?.write(data);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
resize: () => {
|
|
246
|
+
},
|
|
247
|
+
kill: () => child.kill('SIGTERM'),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function runShell(command, options) {
|
|
251
|
+
const pty = await loadPty();
|
|
252
|
+
if (pty) {
|
|
253
|
+
try {
|
|
254
|
+
return runWithPty(pty, command, options);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return runWithPipes(command, options);
|
|
260
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { exec } from 'node:child_process';
|
|
12
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
13
|
+
import { basename, join, relative, sep } from 'node:path';
|
|
14
|
+
const SKIP_DIRS = new Set([
|
|
15
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.turbo', '.venv', 'venv',
|
|
16
|
+
'__pycache__', '.mypy_cache', '.pytest_cache', 'target', '.cache', 'coverage',
|
|
17
|
+
'.gradle', '.idea', 'vendor', '.terraform',
|
|
18
|
+
]);
|
|
19
|
+
const MAX_FILES = 2000;
|
|
20
|
+
const MARKER_FILES = [
|
|
21
|
+
'package.json', 'tsconfig.json', 'pyproject.toml', 'requirements.txt',
|
|
22
|
+
'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle', 'Gemfile', 'composer.json',
|
|
23
|
+
'Dockerfile', 'docker-compose.yml', 'Makefile', 'README.md', 'CLAUDE.md',
|
|
24
|
+
'AGENTS.md', 'RIVINE.md',
|
|
25
|
+
];
|
|
26
|
+
function run(command, cwd, timeout = 4000) {
|
|
27
|
+
return new Promise((resolvePromise) => {
|
|
28
|
+
exec(command, { cwd, timeout, maxBuffer: 1024 * 1024 }, (err, stdout) => {
|
|
29
|
+
resolvePromise(err ? null : stdout.trim());
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function collect(root, dir, out) {
|
|
34
|
+
if (out.length >= MAX_FILES)
|
|
35
|
+
return;
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (out.length >= MAX_FILES)
|
|
45
|
+
return;
|
|
46
|
+
if (SKIP_DIRS.has(entry.name))
|
|
47
|
+
continue;
|
|
48
|
+
const abs = join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
await collect(root, abs, out);
|
|
51
|
+
}
|
|
52
|
+
else if (entry.isFile()) {
|
|
53
|
+
out.push(relative(root, abs).split(sep).join('/'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function buildSnapshot(root) {
|
|
58
|
+
const files = [];
|
|
59
|
+
await collect(root, root, files);
|
|
60
|
+
const markers = [];
|
|
61
|
+
for (const marker of MARKER_FILES) {
|
|
62
|
+
const exists = await stat(join(root, marker)).then(() => true).catch(() => false);
|
|
63
|
+
if (exists)
|
|
64
|
+
markers.push(marker);
|
|
65
|
+
}
|
|
66
|
+
const snapshot = {
|
|
67
|
+
name: basename(root),
|
|
68
|
+
file_count: files.length,
|
|
69
|
+
truncated: files.length >= MAX_FILES,
|
|
70
|
+
files: files.sort(),
|
|
71
|
+
markers,
|
|
72
|
+
};
|
|
73
|
+
const branch = await run('git rev-parse --abbrev-ref HEAD', root);
|
|
74
|
+
if (branch) {
|
|
75
|
+
const porcelain = (await run('git status --porcelain', root)) ?? '';
|
|
76
|
+
const changed = porcelain
|
|
77
|
+
.split('\n')
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.slice(0, 50)
|
|
80
|
+
.map((line) => line.slice(3));
|
|
81
|
+
snapshot.git = { branch, dirty: porcelain.length > 0, changed };
|
|
82
|
+
}
|
|
83
|
+
return snapshot;
|
|
84
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 async function* readTurnStream(response) {
|
|
12
|
+
const body = response.body;
|
|
13
|
+
if (!body) {
|
|
14
|
+
yield { type: 'error', content: 'The engine returned an empty response.' };
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const decoder = new TextDecoder();
|
|
18
|
+
let buffer = '';
|
|
19
|
+
for await (const chunk of body) {
|
|
20
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
21
|
+
let boundary = buffer.indexOf('\n\n');
|
|
22
|
+
while (boundary !== -1) {
|
|
23
|
+
const frame = buffer.slice(0, boundary);
|
|
24
|
+
buffer = buffer.slice(boundary + 2);
|
|
25
|
+
boundary = buffer.indexOf('\n\n');
|
|
26
|
+
if (!frame.startsWith('data:'))
|
|
27
|
+
continue;
|
|
28
|
+
const payload = frame.slice(5).trim();
|
|
29
|
+
if (payload === '[DONE]') {
|
|
30
|
+
yield { type: 'done' };
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
yield JSON.parse(payload);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
yield { type: 'done' };
|
|
41
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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 { InsufficientCredits } from './api/workspace.js';
|
|
12
|
+
import { siteUrl } from './config.js';
|
|
13
|
+
import { TurnRunner } from './protocol/runner.js';
|
|
14
|
+
import { creditShortfall, startSession } from './session.js';
|
|
15
|
+
import { HeadlessRenderer, installTeardown } from './ui/headless.js';
|
|
16
|
+
export async function runHeadless(options) {
|
|
17
|
+
const json = options.format === 'json';
|
|
18
|
+
const out = new HeadlessRenderer(options.mode, json);
|
|
19
|
+
let runner = null;
|
|
20
|
+
const filesWritten = new Set();
|
|
21
|
+
let answer = '';
|
|
22
|
+
const emit = (ok, exitCode, error) => {
|
|
23
|
+
if (!json)
|
|
24
|
+
return;
|
|
25
|
+
out.result({
|
|
26
|
+
ok,
|
|
27
|
+
answer,
|
|
28
|
+
error: error ?? null,
|
|
29
|
+
exit_code: exitCode,
|
|
30
|
+
files_written: [...filesWritten],
|
|
31
|
+
refused: out.refusalCount,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const removeTeardown = installTeardown(async () => {
|
|
35
|
+
await runner?.shutdown();
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
const session = await startSession({
|
|
39
|
+
cwd: options.cwd,
|
|
40
|
+
onNotice: (text) => out.status(text),
|
|
41
|
+
onLoginUrl: (url) => out.status(`Sign in at: ${url}`),
|
|
42
|
+
});
|
|
43
|
+
const balance = await session.balance;
|
|
44
|
+
if (balance === null) {
|
|
45
|
+
out.line('notice', 'Could not read your credit balance — continuing without the local check.');
|
|
46
|
+
}
|
|
47
|
+
const needed = creditShortfall(balance, session.bootstrap.engine_min_credits);
|
|
48
|
+
if (needed !== null) {
|
|
49
|
+
out.line('error', `Rivine Code needs ${needed.toFixed(2)} in credits to run.`);
|
|
50
|
+
out.line('notice', `Top up at ${siteUrl()}/credits`);
|
|
51
|
+
emit(false, 2, `Insufficient credits: ${needed.toFixed(2)} required.`);
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
runner = new TurnRunner({
|
|
55
|
+
workspaceId: session.bootstrap.workspace_id,
|
|
56
|
+
chatSpaceId: session.bootstrap.chat_space_id,
|
|
57
|
+
sessionId: session.bootstrap.session_id,
|
|
58
|
+
root: options.cwd,
|
|
59
|
+
mode: options.mode,
|
|
60
|
+
}, {
|
|
61
|
+
onToken: (text) => out.token(text),
|
|
62
|
+
onStage: (label) => out.status(label),
|
|
63
|
+
onTool: (name, detail) => {
|
|
64
|
+
out.endAnswer();
|
|
65
|
+
out.line(name === 'execute' ? 'run' : 'tool', detail ? `${name} ${detail}` : name);
|
|
66
|
+
},
|
|
67
|
+
onFileTouched: (path, status) => {
|
|
68
|
+
if (status === 'written') {
|
|
69
|
+
const rel = path.replace(/^\/workspace\//, '');
|
|
70
|
+
filesWritten.add(rel);
|
|
71
|
+
out.endAnswer();
|
|
72
|
+
out.line('file', rel);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
onNotice: (text) => {
|
|
76
|
+
out.endAnswer();
|
|
77
|
+
out.line('notice', text);
|
|
78
|
+
},
|
|
79
|
+
onOutput: (text) => out.shellOutput(text),
|
|
80
|
+
requestApproval: async (approval) => {
|
|
81
|
+
out.endAnswer();
|
|
82
|
+
return out.refuseApproval(approval);
|
|
83
|
+
},
|
|
84
|
+
requestDecision: async (request) => {
|
|
85
|
+
out.endAnswer();
|
|
86
|
+
out.line('notice', `Approval needed but no terminal to ask on: ${request.message}`);
|
|
87
|
+
return { approved: false, feedback: 'No interactive terminal available.' };
|
|
88
|
+
},
|
|
89
|
+
askQuestion: async (question) => {
|
|
90
|
+
out.endAnswer();
|
|
91
|
+
return out.refuseQuestion(question.prompt);
|
|
92
|
+
},
|
|
93
|
+
}, session.mcp);
|
|
94
|
+
const reports = await session.mcpReady;
|
|
95
|
+
for (const bad of reports.filter((r) => !r.ok)) {
|
|
96
|
+
out.line('notice', `MCP server "${bad.name}" did not start: ${bad.error}`);
|
|
97
|
+
}
|
|
98
|
+
runner.setMcpTools(session.mcp.listTools());
|
|
99
|
+
const result = await runner.run(options.prompt, await session.snapshot);
|
|
100
|
+
out.endAnswer();
|
|
101
|
+
answer = result.answer ?? '';
|
|
102
|
+
if (result.error) {
|
|
103
|
+
out.line('error', result.error);
|
|
104
|
+
emit(false, 1, result.error);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
if (result.answer) {
|
|
108
|
+
out.line('agent', result.answer);
|
|
109
|
+
}
|
|
110
|
+
if (out.refusalCount > 0) {
|
|
111
|
+
const advice = options.mode === 'plan'
|
|
112
|
+
? 'plan mode makes no changes — re-run without --plan'
|
|
113
|
+
: `re-run with -m acceptEdits, or --bypass in CI (mode was ${options.mode})`;
|
|
114
|
+
const message = `${out.refusalCount} operation(s) were skipped because there is no terminal ` +
|
|
115
|
+
`to approve them: ${advice}.`;
|
|
116
|
+
out.line('error', message);
|
|
117
|
+
emit(false, 3, message);
|
|
118
|
+
return 3;
|
|
119
|
+
}
|
|
120
|
+
emit(true, 0);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
out.endAnswer();
|
|
125
|
+
if (err instanceof InsufficientCredits) {
|
|
126
|
+
out.line('error', err.detail);
|
|
127
|
+
out.line('notice', `Top up at ${siteUrl()}/credits`);
|
|
128
|
+
emit(false, 2, err.detail);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const message = err.message;
|
|
132
|
+
out.line('error', message);
|
|
133
|
+
emit(false, 1, message);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await runner?.shutdown();
|
|
138
|
+
removeTeardown();
|
|
139
|
+
}
|
|
140
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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 { stat } from 'node:fs/promises';
|
|
12
|
+
import { basename, join } from 'node:path';
|
|
13
|
+
import { bootstrap, fetchCredits } from './api/workspace.js';
|
|
14
|
+
import { isSignedIn, login } from './auth/session.js';
|
|
15
|
+
import { McpManager } from './mcp/manager.js';
|
|
16
|
+
import { buildSnapshot } from './protocol/snapshot.js';
|
|
17
|
+
async function localMemoryPresent(cwd) {
|
|
18
|
+
try {
|
|
19
|
+
const info = await stat(join(cwd, '.rivine', 'local_memory.md'));
|
|
20
|
+
return info.isFile() && info.size > 0;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function startSession(options) {
|
|
27
|
+
const { cwd } = options;
|
|
28
|
+
if (!(await isSignedIn())) {
|
|
29
|
+
options.onNotice?.('Opening your browser to sign in…');
|
|
30
|
+
await login({
|
|
31
|
+
onUrl: (url) => options.onLoginUrl?.(url),
|
|
32
|
+
onNoRefreshToken: (hours) => options.onNotice?.(`This session ends in ~${hours}h and cannot renew itself.`),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const project = basename(cwd) || 'project';
|
|
36
|
+
const result = await bootstrap(cwd, project);
|
|
37
|
+
const mcp = new McpManager();
|
|
38
|
+
const mcpReady = mcp.connectAll(cwd).catch(() => []);
|
|
39
|
+
return {
|
|
40
|
+
bootstrap: result,
|
|
41
|
+
project,
|
|
42
|
+
mcp,
|
|
43
|
+
mcpReady,
|
|
44
|
+
continuing: !result.created,
|
|
45
|
+
hasLocalMemory: await localMemoryPresent(cwd),
|
|
46
|
+
snapshot: buildSnapshot(cwd).catch(() => null),
|
|
47
|
+
balance: fetchCredits(result.credits_url)
|
|
48
|
+
.then((c) => (c ? c.balance : null))
|
|
49
|
+
.catch(() => null),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function creditShortfall(balance, minimums) {
|
|
53
|
+
const floor = minimums.rivine_code ?? 10;
|
|
54
|
+
if (balance === null)
|
|
55
|
+
return null;
|
|
56
|
+
return balance < floor ? floor : null;
|
|
57
|
+
}
|