@beunec/rivine 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -72
- package/dist/api/workspace.js +41 -5
- package/dist/index.js +20 -7
- package/dist/protocol/permissions.js +0 -4
- package/dist/protocol/runner.js +194 -18
- package/dist/protocol/shell.js +5 -2
- package/dist/runHeadless.js +10 -2
- package/dist/session.js +69 -3
- package/dist/ui/App.js +80 -19
- package/dist/ui/components.js +2 -3
- package/dist/ui/headless.js +7 -4
- package/dist/ui/notices.js +20 -0
- package/dist/ui/phase.js +28 -0
- package/dist/ui/render.js +3 -0
- package/dist/ui/slashCommands.js +6 -6
- package/package.json +1 -1
package/dist/protocol/runner.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* accompanying it; it is not open source and is not licensed for
|
|
9
9
|
* redistribution or derivative works.
|
|
10
10
|
*/
|
|
11
|
-
import { closeSession, openTurn, postFsResult, resumeTurn } from '../api/workspace.js';
|
|
11
|
+
import { cancelTurn, closeSession, getTurnStatus, openTurn, postFsResult, resumeTurn } from '../api/workspace.js';
|
|
12
12
|
import { apiUrl } from '../config.js';
|
|
13
13
|
import { accessToken } from '../auth/session.js';
|
|
14
14
|
import { classify, execute } from './fsExecutor.js';
|
|
@@ -16,7 +16,21 @@ 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;
|
|
21
|
+
const FOLLOW_POLL_MS = Number(process.env.RIVINE_FOLLOW_POLL_MS) || 3000;
|
|
22
|
+
export function writtenPaths(request) {
|
|
23
|
+
const args = (request.args ?? {});
|
|
24
|
+
let paths = [];
|
|
25
|
+
if (request.op === 'write' || request.op === 'edit')
|
|
26
|
+
paths = [String(args.file_path ?? args.path ?? '')];
|
|
27
|
+
else if (request.op === 'upload')
|
|
28
|
+
paths = (args.files ?? []).map((f) => String(f?.path ?? ''));
|
|
29
|
+
return paths.filter((p) => p && !/(^|\/)\.rivine\//.test(p));
|
|
30
|
+
}
|
|
31
|
+
function isFollowSignal(value) {
|
|
32
|
+
return value.follow === true;
|
|
33
|
+
}
|
|
20
34
|
export class TurnRunner {
|
|
21
35
|
ctx;
|
|
22
36
|
cb;
|
|
@@ -26,6 +40,8 @@ export class TurnRunner {
|
|
|
26
40
|
approvalChain = Promise.resolve();
|
|
27
41
|
inFlight = new Set();
|
|
28
42
|
stopped = false;
|
|
43
|
+
turnActive = false;
|
|
44
|
+
following = false;
|
|
29
45
|
pollTimer = null;
|
|
30
46
|
constructor(ctx, cb, mcp) {
|
|
31
47
|
this.ctx = ctx;
|
|
@@ -72,7 +88,16 @@ export class TurnRunner {
|
|
|
72
88
|
this.ctx.mcpTools = tools;
|
|
73
89
|
}
|
|
74
90
|
async run(message, snapshot) {
|
|
91
|
+
if (!this.ctx.codeMode) {
|
|
92
|
+
return {
|
|
93
|
+
answer: '',
|
|
94
|
+
cancelled: false,
|
|
95
|
+
error: 'No Mode selected. Pick one with /mode — Frontend Only, Backend Only, ' +
|
|
96
|
+
'Full Stack, Plan/Ask Only, or Security Scan/Fix.',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
75
99
|
this.stopped = false;
|
|
100
|
+
this.turnActive = true;
|
|
76
101
|
this.startPolling();
|
|
77
102
|
const selection = selectRelevantTools(message, this.ctx.mcpTools ?? []);
|
|
78
103
|
if (selection.servers.length) {
|
|
@@ -99,6 +124,8 @@ export class TurnRunner {
|
|
|
99
124
|
return await this.consume(response);
|
|
100
125
|
}
|
|
101
126
|
finally {
|
|
127
|
+
this.turnActive = false;
|
|
128
|
+
this.following = false;
|
|
102
129
|
this.stopPolling();
|
|
103
130
|
await Promise.allSettled([...this.inFlight]);
|
|
104
131
|
}
|
|
@@ -106,15 +133,19 @@ export class TurnRunner {
|
|
|
106
133
|
async consume(response) {
|
|
107
134
|
let answer = '';
|
|
108
135
|
let pendingHitl = null;
|
|
136
|
+
let continuing = false;
|
|
109
137
|
for await (const event of readTurnStream(response)) {
|
|
110
138
|
switch (event.type) {
|
|
111
139
|
case 'token':
|
|
112
140
|
answer += event.content ?? '';
|
|
113
141
|
this.cb.onToken(event.content ?? '');
|
|
114
142
|
break;
|
|
115
|
-
case 'stage':
|
|
116
|
-
|
|
143
|
+
case 'stage': {
|
|
144
|
+
const label = displayStage(String(event.label ?? ''));
|
|
145
|
+
if (label)
|
|
146
|
+
this.cb.onStage(label);
|
|
117
147
|
break;
|
|
148
|
+
}
|
|
118
149
|
case 'tool_call_start': {
|
|
119
150
|
const e = event;
|
|
120
151
|
const detail = e.args?.command ?? e.args?.filename ?? '';
|
|
@@ -158,12 +189,16 @@ export class TurnRunner {
|
|
|
158
189
|
options: extractOptions(e),
|
|
159
190
|
multi: Boolean(e.ui_schema?.multi ?? e.args?.multi),
|
|
160
191
|
threadId: String(e.thread_id ?? `${this.ctx.workspaceId}_${this.ctx.chatSpaceId}`),
|
|
192
|
+
kind: hitlKind(e.hitl_type, e.args),
|
|
161
193
|
};
|
|
162
194
|
break;
|
|
163
195
|
}
|
|
164
196
|
case 'final_answer':
|
|
165
197
|
answer = String(event.content ?? answer);
|
|
166
198
|
break;
|
|
199
|
+
case 'turn_continuing':
|
|
200
|
+
continuing = true;
|
|
201
|
+
break;
|
|
167
202
|
case 'error': {
|
|
168
203
|
const e = event;
|
|
169
204
|
return { answer, cancelled: Boolean(e.cancelled), error: String(e.content ?? 'Unknown error') };
|
|
@@ -175,27 +210,107 @@ export class TurnRunner {
|
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
if (pendingHitl) {
|
|
178
|
-
|
|
213
|
+
const settled = await this.settleHitl(pendingHitl, answer);
|
|
214
|
+
if (isFollowSignal(settled))
|
|
215
|
+
return this.follow(settled.answer);
|
|
216
|
+
return settled;
|
|
217
|
+
}
|
|
218
|
+
if (continuing) {
|
|
219
|
+
return this.follow(answer);
|
|
179
220
|
}
|
|
180
221
|
return { answer, cancelled: false };
|
|
181
222
|
}
|
|
182
|
-
async
|
|
223
|
+
async follow(partial) {
|
|
224
|
+
this.following = true;
|
|
225
|
+
let lastSlice = -1;
|
|
226
|
+
this.cb.onStage('Still working — continuing on the server…');
|
|
227
|
+
try {
|
|
228
|
+
while (!this.stopped) {
|
|
229
|
+
await new Promise((r) => setTimeout(r, FOLLOW_POLL_MS));
|
|
230
|
+
let status;
|
|
231
|
+
try {
|
|
232
|
+
status = await getTurnStatus(this.ctx.workspaceId, this.ctx.chatSpaceId);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (typeof status.slice === 'number' && status.slice !== lastSlice) {
|
|
238
|
+
lastSlice = status.slice;
|
|
239
|
+
this.cb.onStage(`Still working — part ${status.slice + 1}`);
|
|
240
|
+
}
|
|
241
|
+
if (status.status === 'done') {
|
|
242
|
+
return { answer: String(status.content ?? partial), cancelled: false };
|
|
243
|
+
}
|
|
244
|
+
if (status.status === 'error') {
|
|
245
|
+
return { answer: partial, cancelled: false, error: String(status.error ?? 'The turn failed.') };
|
|
246
|
+
}
|
|
247
|
+
if (status.status === 'cancelled') {
|
|
248
|
+
return { answer: partial, cancelled: true };
|
|
249
|
+
}
|
|
250
|
+
if (status.status === 'paused') {
|
|
251
|
+
if (String(status.hitl_type ?? '') === 'time_slice') {
|
|
252
|
+
this.cb.onStage('Still working — continuing on the server…');
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const settled = await this.settleHitl({
|
|
256
|
+
message: String(status.hitl_prompt ?? 'Approval required.'),
|
|
257
|
+
tool: status.hitl_tool ? String(status.hitl_tool) : undefined,
|
|
258
|
+
options: extractOptions({ ui_schema: status.hitl_ui_schema, args: status.hitl_args }),
|
|
259
|
+
multi: Boolean(status.hitl_ui_schema?.multi),
|
|
260
|
+
threadId: `${this.ctx.workspaceId}_${this.ctx.chatSpaceId}`,
|
|
261
|
+
kind: hitlKind(status.hitl_type, status.hitl_args),
|
|
262
|
+
}, partial);
|
|
263
|
+
if (isFollowSignal(settled)) {
|
|
264
|
+
partial = settled.answer;
|
|
265
|
+
this.cb.onStage('Still working — continuing on the server…');
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
return settled;
|
|
269
|
+
}
|
|
270
|
+
if (status.status === 'idle') {
|
|
271
|
+
return { answer: partial, cancelled: false, error: 'The turn ended without a result.' };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { answer: partial, cancelled: true };
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
this.following = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async settleHitl(first, priorAnswer = '') {
|
|
183
281
|
let current = first;
|
|
184
|
-
let answer =
|
|
282
|
+
let answer = priorAnswer;
|
|
185
283
|
for (let round = 0; current && round < 25; round += 1) {
|
|
186
284
|
const decision = await this.enqueuePrompt(() => this.cb.requestDecision(current));
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
285
|
+
if (decision.codeMode)
|
|
286
|
+
this.setCodeMode(decision.codeMode);
|
|
287
|
+
let body;
|
|
288
|
+
try {
|
|
289
|
+
body = await resumeTurn({
|
|
290
|
+
workspaceId: this.ctx.workspaceId,
|
|
291
|
+
chatSpaceId: this.ctx.chatSpaceId,
|
|
292
|
+
threadId: current.threadId,
|
|
293
|
+
sessionId: this.ctx.sessionId,
|
|
294
|
+
sessionKey: this.key.toBase64(),
|
|
295
|
+
approved: decision.approved,
|
|
296
|
+
feedback: decision.feedback,
|
|
297
|
+
modifiedArgs: decision.modifiedArgs,
|
|
298
|
+
codeMode: decision.codeMode ?? null,
|
|
299
|
+
codeRate: decision.codeMode ? permissionModeToRate(this.ctx.mode) : null,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const detail = String(err.message ?? '');
|
|
304
|
+
if (/still running|background/i.test(detail)) {
|
|
305
|
+
return this.continueAfterHitl(answer);
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
197
309
|
answer = String(body.response ?? answer);
|
|
198
310
|
const meta = body.context_metadata ?? {};
|
|
311
|
+
if (meta.continuing) {
|
|
312
|
+
return this.continueAfterHitl(answer);
|
|
313
|
+
}
|
|
199
314
|
current = meta.paused
|
|
200
315
|
? {
|
|
201
316
|
message: String(meta.hitl_prompt ?? body.response ?? 'Approval required.'),
|
|
@@ -203,10 +318,43 @@ export class TurnRunner {
|
|
|
203
318
|
options: extractOptions({ ui_schema: meta.hitl_ui_schema, args: meta.hitl_args }),
|
|
204
319
|
multi: Boolean(meta.hitl_ui_schema?.multi),
|
|
205
320
|
threadId: String(meta.thread_id ?? current.threadId),
|
|
321
|
+
kind: hitlKind(meta.hitl_type, meta.hitl_args),
|
|
206
322
|
}
|
|
207
323
|
: null;
|
|
208
324
|
}
|
|
209
|
-
|
|
325
|
+
try {
|
|
326
|
+
const status = await getTurnStatus(this.ctx.workspaceId, this.ctx.chatSpaceId);
|
|
327
|
+
if (status.status === 'running' || status.continuing) {
|
|
328
|
+
return this.continueAfterHitl(answer);
|
|
329
|
+
}
|
|
330
|
+
if (status.status === 'paused' && String(status.hitl_type ?? '') !== 'time_slice') {
|
|
331
|
+
return this.settleHitl({
|
|
332
|
+
message: String(status.hitl_prompt ?? 'Approval required.'),
|
|
333
|
+
tool: status.hitl_tool ? String(status.hitl_tool) : undefined,
|
|
334
|
+
options: extractOptions({ ui_schema: status.hitl_ui_schema, args: status.hitl_args }),
|
|
335
|
+
multi: Boolean(status.hitl_ui_schema?.multi),
|
|
336
|
+
threadId: `${this.ctx.workspaceId}_${this.ctx.chatSpaceId}`,
|
|
337
|
+
kind: hitlKind(status.hitl_type, status.hitl_args),
|
|
338
|
+
}, answer);
|
|
339
|
+
}
|
|
340
|
+
if (status.status === 'error') {
|
|
341
|
+
return { answer, cancelled: false, error: String(status.error ?? 'The turn failed.') };
|
|
342
|
+
}
|
|
343
|
+
if (status.status === 'cancelled') {
|
|
344
|
+
return { answer, cancelled: true };
|
|
345
|
+
}
|
|
346
|
+
if (status.status === 'done' && status.content) {
|
|
347
|
+
return { answer: String(status.content), cancelled: false };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
}
|
|
352
|
+
return { answer, cancelled: false };
|
|
353
|
+
}
|
|
354
|
+
async continueAfterHitl(answer) {
|
|
355
|
+
if (this.following)
|
|
356
|
+
return { follow: true, answer };
|
|
357
|
+
return this.follow(answer);
|
|
210
358
|
}
|
|
211
359
|
dispatch(request) {
|
|
212
360
|
if (this.stopped || this.seen.has(request.request_id))
|
|
@@ -218,6 +366,10 @@ export class TurnRunner {
|
|
|
218
366
|
this.seen.add(id);
|
|
219
367
|
}
|
|
220
368
|
this.seen.add(request.request_id);
|
|
369
|
+
if (this.following && request.op !== 'ask') {
|
|
370
|
+
const args = (request.args ?? {});
|
|
371
|
+
this.cb.onTool(String(request.op), String(args.command ?? args.path ?? args.file_path ?? ''));
|
|
372
|
+
}
|
|
221
373
|
const task = (async () => {
|
|
222
374
|
let outcome;
|
|
223
375
|
try {
|
|
@@ -267,6 +419,10 @@ export class TurnRunner {
|
|
|
267
419
|
catch (err) {
|
|
268
420
|
outcome = { ok: false, error: err.message };
|
|
269
421
|
}
|
|
422
|
+
if (this.following && outcome.ok) {
|
|
423
|
+
for (const path of writtenPaths(request))
|
|
424
|
+
this.cb.onFileTouched(path, 'written');
|
|
425
|
+
}
|
|
270
426
|
try {
|
|
271
427
|
await postFsResult(this.ctx.sessionId, request.request_id, outcome, this.key);
|
|
272
428
|
}
|
|
@@ -334,14 +490,34 @@ export class TurnRunner {
|
|
|
334
490
|
}
|
|
335
491
|
}
|
|
336
492
|
async shutdown() {
|
|
493
|
+
const wasActive = this.turnActive;
|
|
337
494
|
this.stopped = true;
|
|
338
495
|
this.stopPolling();
|
|
496
|
+
if (wasActive)
|
|
497
|
+
await cancelTurn(this.ctx.workspaceId, this.ctx.chatSpaceId);
|
|
339
498
|
await closeSession(this.ctx.sessionId);
|
|
340
499
|
}
|
|
341
500
|
}
|
|
342
|
-
function
|
|
501
|
+
export function hitlKind(hitlType, args) {
|
|
502
|
+
const field = args && typeof args === 'object' ? String(args.field ?? '') : '';
|
|
503
|
+
return String(hitlType ?? '') === 'code_clarification' && field === 'mode'
|
|
504
|
+
? 'code_mode'
|
|
505
|
+
: 'other';
|
|
506
|
+
}
|
|
507
|
+
export function extractOptions(event) {
|
|
508
|
+
const schemaEnum = (() => {
|
|
509
|
+
const props = event?.ui_schema?.properties;
|
|
510
|
+
if (!props || typeof props !== 'object')
|
|
511
|
+
return undefined;
|
|
512
|
+
for (const value of Object.values(props)) {
|
|
513
|
+
if (Array.isArray(value?.enum) && value.enum.length)
|
|
514
|
+
return value.enum;
|
|
515
|
+
}
|
|
516
|
+
return undefined;
|
|
517
|
+
})();
|
|
343
518
|
const raw = event?.ui_schema?.options ??
|
|
344
519
|
event?.ui_schema?.choices ??
|
|
520
|
+
schemaEnum ??
|
|
345
521
|
event?.args?.options ??
|
|
346
522
|
event?.options;
|
|
347
523
|
if (!Array.isArray(raw) || raw.length === 0)
|
package/dist/protocol/shell.js
CHANGED
|
@@ -104,6 +104,9 @@ function shellInvocation(command) {
|
|
|
104
104
|
}
|
|
105
105
|
return { file: process.env.SHELL || '/bin/sh', args: ['-c', command] };
|
|
106
106
|
}
|
|
107
|
+
function agentEnv() {
|
|
108
|
+
return { ...process.env, GIT_PAGER: 'cat', PAGER: 'cat', GIT_TERMINAL_PROMPT: '0' };
|
|
109
|
+
}
|
|
107
110
|
function runWithPty(pty, command, options) {
|
|
108
111
|
const { file, args } = shellInvocation(command);
|
|
109
112
|
const tail = new Tail(MAX_OUTPUT_CHARS);
|
|
@@ -112,7 +115,7 @@ function runWithPty(pty, command, options) {
|
|
|
112
115
|
cols: options.cols ?? 120,
|
|
113
116
|
rows: options.rows ?? 30,
|
|
114
117
|
cwd: options.cwd,
|
|
115
|
-
env:
|
|
118
|
+
env: agentEnv(),
|
|
116
119
|
});
|
|
117
120
|
let settled = false;
|
|
118
121
|
let timedOut = false;
|
|
@@ -189,7 +192,7 @@ function runWithPipes(command, options) {
|
|
|
189
192
|
const tail = new Tail(MAX_OUTPUT_CHARS);
|
|
190
193
|
const child = spawn(file, args, {
|
|
191
194
|
cwd: options.cwd,
|
|
192
|
-
env:
|
|
195
|
+
env: agentEnv(),
|
|
193
196
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
194
197
|
});
|
|
195
198
|
let settled = false;
|
package/dist/runHeadless.js
CHANGED
|
@@ -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',
|
|
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) => {
|
|
@@ -102,6 +105,11 @@ export async function runHeadless(options) {
|
|
|
102
105
|
answer = result.answer ?? '';
|
|
103
106
|
if (result.error) {
|
|
104
107
|
out.line('error', result.error);
|
|
108
|
+
if (/credit|insufficient/i.test(result.error)) {
|
|
109
|
+
out.line('notice', `Top up at ${siteUrl()}/credits`);
|
|
110
|
+
emit(false, 2, result.error);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
105
113
|
emit(false, 1, result.error);
|
|
106
114
|
return 1;
|
|
107
115
|
}
|
|
@@ -111,7 +119,7 @@ export async function runHeadless(options) {
|
|
|
111
119
|
if (out.refusalCount > 0) {
|
|
112
120
|
const advice = options.mode === 'plan'
|
|
113
121
|
? 'plan mode makes no changes — re-run without --plan'
|
|
114
|
-
: `re-run with
|
|
122
|
+
: `re-run with --rate bypass (Rate was ${permissionModeToRate(options.mode)})`;
|
|
115
123
|
const message = `${out.refusalCount} operation(s) were skipped because there is no terminal ` +
|
|
116
124
|
`to approve them: ${advice}.`;
|
|
117
125
|
out.line('error', message);
|
package/dist/session.js
CHANGED
|
@@ -8,12 +8,25 @@
|
|
|
8
8
|
* accompanying it; it is not open source and is not licensed for
|
|
9
9
|
* redistribution or derivative works.
|
|
10
10
|
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
11
12
|
import { stat } from 'node:fs/promises';
|
|
12
|
-
import { basename, join } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import { basename, dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { bootstrap, fetchCredits, OnboardingRequired } from './api/workspace.js';
|
|
16
|
+
import { openBrowser } from './auth/pkce.js';
|
|
14
17
|
import { isSignedIn, login } from './auth/session.js';
|
|
15
18
|
import { McpManager } from './mcp/manager.js';
|
|
16
19
|
import { buildSnapshot } from './protocol/snapshot.js';
|
|
20
|
+
export function packageVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
24
|
+
return String(pkg.version ?? '0.0.0');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return '0.0.0';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
async function localMemoryPresent(cwd) {
|
|
18
31
|
try {
|
|
19
32
|
const info = await stat(join(cwd, '.rivine', 'local_memory.md'));
|
|
@@ -23,6 +36,30 @@ async function localMemoryPresent(cwd) {
|
|
|
23
36
|
return false;
|
|
24
37
|
}
|
|
25
38
|
}
|
|
39
|
+
const ONBOARDING_POLL_MS = 5_000;
|
|
40
|
+
const ONBOARDING_WAIT_MS = 20 * 60_000;
|
|
41
|
+
async function bootstrapAfterOnboarding(cwd, project, options) {
|
|
42
|
+
let waitingSince = 0;
|
|
43
|
+
for (;;) {
|
|
44
|
+
try {
|
|
45
|
+
return await bootstrap(cwd, project);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (!(err instanceof OnboardingRequired))
|
|
49
|
+
throw err;
|
|
50
|
+
if (!waitingSince) {
|
|
51
|
+
waitingSince = Date.now();
|
|
52
|
+
options.onNotice?.(`${err.message} Opening ${err.url} — choose your cloudhandle there, then come back. Rivine Code continues by itself.`);
|
|
53
|
+
options.onLoginUrl?.(err.url);
|
|
54
|
+
openBrowser(err.url);
|
|
55
|
+
}
|
|
56
|
+
else if (Date.now() - waitingSince > ONBOARDING_WAIT_MS) {
|
|
57
|
+
throw new Error(`Your Aselius account is not set up yet. Finish it at ${err.url}, then run \`rivine\` again.`);
|
|
58
|
+
}
|
|
59
|
+
await new Promise((r) => setTimeout(r, ONBOARDING_POLL_MS));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
26
63
|
export async function startSession(options) {
|
|
27
64
|
const { cwd } = options;
|
|
28
65
|
if (!(await isSignedIn())) {
|
|
@@ -33,7 +70,10 @@ export async function startSession(options) {
|
|
|
33
70
|
});
|
|
34
71
|
}
|
|
35
72
|
const project = basename(cwd) || 'project';
|
|
36
|
-
const result = await
|
|
73
|
+
const result = await bootstrapAfterOnboarding(cwd, project, options);
|
|
74
|
+
for (const notice of continuationNotices(result, packageVersion())) {
|
|
75
|
+
options.onNotice?.(notice);
|
|
76
|
+
}
|
|
37
77
|
const mcp = new McpManager();
|
|
38
78
|
const mcpReady = mcp.connectAll(cwd).catch(() => []);
|
|
39
79
|
return {
|
|
@@ -55,3 +95,29 @@ export function creditShortfall(balance, minimums) {
|
|
|
55
95
|
return null;
|
|
56
96
|
return balance < floor ? floor : null;
|
|
57
97
|
}
|
|
98
|
+
export function compareSemver(a, b) {
|
|
99
|
+
const parts = (v) => String(v || '0')
|
|
100
|
+
.replace(/^v/i, '')
|
|
101
|
+
.split(/[.+-]/)
|
|
102
|
+
.map((p) => parseInt(p, 10) || 0);
|
|
103
|
+
const aa = parts(a);
|
|
104
|
+
const bb = parts(b);
|
|
105
|
+
const n = Math.max(aa.length, bb.length);
|
|
106
|
+
for (let i = 0; i < n; i += 1) {
|
|
107
|
+
const d = (aa[i] ?? 0) - (bb[i] ?? 0);
|
|
108
|
+
if (d)
|
|
109
|
+
return d < 0 ? -1 : 1;
|
|
110
|
+
}
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
export function continuationNotices(bootstrap, localVersion) {
|
|
114
|
+
const out = [];
|
|
115
|
+
const min = bootstrap.min_cli_version;
|
|
116
|
+
if (min && compareSemver(localVersion, min) < 0) {
|
|
117
|
+
out.push(`This CLI is ${localVersion}; long turns need >= ${min}. Upgrade: npm install -g @beunec/rivine@latest`);
|
|
118
|
+
}
|
|
119
|
+
if (bootstrap.continuation_enabled === false) {
|
|
120
|
+
out.push('This Workspace has turn continuation off — long Rivine Code builds may stop at the server time limit.');
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|