@getmegabrain/cli 0.1.4 → 0.1.5
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/cloud-fork.js +104 -0
- package/dist/index.js +39 -1
- package/package.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `megabrain --session <id> --cloud-fork` — fork a MegaBrain cloud session
|
|
3
|
+
* into a local OpenCode session.
|
|
4
|
+
*
|
|
5
|
+
* The web UI hands users this command for any cloud session. Flow:
|
|
6
|
+
* 1. Download the session export from the session-ingest worker
|
|
7
|
+
* (`GET /api/session/:id/export`, authenticated with the same Kilo user
|
|
8
|
+
* JWT issued by `megabrain login`).
|
|
9
|
+
* 2. `opencode import <file>` to materialize the session locally.
|
|
10
|
+
* 3. Launch `opencode --session <id> --fork` so the user continues on a
|
|
11
|
+
* fork instead of mutating the imported original.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
export const SESSION_INGEST_URL = (process.env.MEGABRAIN_SESSION_INGEST_URL ?? 'https://session-ingest.vasilij-lukin.workers.dev').replace(/\/+$/, '');
|
|
17
|
+
const SESSION_ID_RE = /^ses_[a-z0-9]+$/i;
|
|
18
|
+
export function isValidSessionId(sessionId) {
|
|
19
|
+
return SESSION_ID_RE.test(sessionId);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Detects `--cloud-fork` and extracts the `--session <id>` / `--session=<id>`
|
|
23
|
+
* value from raw argv. `--session` stays in passthroughArgs — it is only
|
|
24
|
+
* removed together with `--cloud-fork` by the caller when forking.
|
|
25
|
+
*/
|
|
26
|
+
export function parseCloudForkArgs(argv) {
|
|
27
|
+
let cloudFork = false;
|
|
28
|
+
let sessionId = null;
|
|
29
|
+
const passthroughArgs = [];
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === '--cloud-fork') {
|
|
33
|
+
cloudFork = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === '--session' && i + 1 < argv.length) {
|
|
37
|
+
sessionId = argv[i + 1];
|
|
38
|
+
passthroughArgs.push(arg, argv[i + 1]);
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg.startsWith('--session=')) {
|
|
43
|
+
sessionId = arg.slice('--session='.length);
|
|
44
|
+
passthroughArgs.push(arg);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
passthroughArgs.push(arg);
|
|
48
|
+
}
|
|
49
|
+
return { cloudFork, sessionId, passthroughArgs };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Guards against error surfaces served as 200 (e.g. `{"detail":"..."}`):
|
|
53
|
+
* a valid OpenCode session export always carries `info.id`, and `opencode
|
|
54
|
+
* import` crashes with a cryptic error when it's missing.
|
|
55
|
+
*/
|
|
56
|
+
export function exportPayloadSessionId(payload) {
|
|
57
|
+
if (payload === null || typeof payload !== 'object')
|
|
58
|
+
return null;
|
|
59
|
+
const info = payload.info;
|
|
60
|
+
if (info === null || typeof info === 'undefined' || typeof info !== 'object')
|
|
61
|
+
return null;
|
|
62
|
+
const id = info.id;
|
|
63
|
+
return typeof id === 'string' && id.length > 0 ? id : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Downloads the cloud session export to a temp file and returns its path.
|
|
67
|
+
* The caller owns cleanup.
|
|
68
|
+
*/
|
|
69
|
+
export async function downloadSessionExport(sessionId, token) {
|
|
70
|
+
const url = `${SESSION_INGEST_URL}/api/session/${encodeURIComponent(sessionId)}/export`;
|
|
71
|
+
let res;
|
|
72
|
+
try {
|
|
73
|
+
res = await fetch(url, {
|
|
74
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
75
|
+
signal: AbortSignal.timeout(300_000),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
throw new Error(`Could not reach MegaBrain to download session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
+
}
|
|
81
|
+
if (res.status === 404) {
|
|
82
|
+
throw new Error(`Cloud session ${sessionId} was not found (or does not belong to your account).`);
|
|
83
|
+
}
|
|
84
|
+
if (res.status === 401 || res.status === 403) {
|
|
85
|
+
throw new Error('Your MegaBrain sign-in is no longer valid. Run `megabrain login` and retry.');
|
|
86
|
+
}
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(`Downloading session ${sessionId} failed (HTTP ${res.status}). Try again.`);
|
|
89
|
+
}
|
|
90
|
+
const body = await res.text();
|
|
91
|
+
let payload;
|
|
92
|
+
try {
|
|
93
|
+
payload = JSON.parse(body);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error(`Session ${sessionId} export is not valid JSON. Try again.`);
|
|
97
|
+
}
|
|
98
|
+
if (exportPayloadSessionId(payload) === null) {
|
|
99
|
+
throw new Error(`Session ${sessionId} export is missing session metadata. Try again.`);
|
|
100
|
+
}
|
|
101
|
+
const tmpPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'megabrain-cloud-fork-')), `${sessionId}.json`);
|
|
102
|
+
fs.writeFileSync(tmpPath, body, { mode: 0o600 });
|
|
103
|
+
return tmpPath;
|
|
104
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { login } from './device-auth.js';
|
|
4
6
|
import { readCredentials, writeCredentials, clearCredentials } from './credentials.js';
|
|
5
7
|
import { writeOpenCodeConfig } from './opencode-config.js';
|
|
6
8
|
import { fetchGatewayModelIds } from './models.js';
|
|
7
9
|
import { ensureOpencodeBinary } from './opencode.js';
|
|
10
|
+
import { parseCloudForkArgs, isValidSessionId, downloadSessionExport } from './cloud-fork.js';
|
|
8
11
|
async function signIn() {
|
|
9
12
|
console.log('Signing in to MegaBrain…');
|
|
10
13
|
const { token, userId, userEmail } = await login();
|
|
@@ -53,6 +56,31 @@ function runOpenCode(args) {
|
|
|
53
56
|
child.on('exit', code => resolve(code ?? 0));
|
|
54
57
|
});
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Forks a cloud session locally: downloads the session export from the
|
|
61
|
+
* MegaBrain session-ingest service, imports it into OpenCode, then launches
|
|
62
|
+
* OpenCode on a fork of the imported session.
|
|
63
|
+
*/
|
|
64
|
+
async function runCloudFork(sessionId) {
|
|
65
|
+
const credentials = readCredentials();
|
|
66
|
+
if (!credentials) {
|
|
67
|
+
throw new Error('Not signed in. Run `megabrain login` and retry.');
|
|
68
|
+
}
|
|
69
|
+
console.log(`Downloading cloud session ${sessionId}…`);
|
|
70
|
+
const exportPath = await downloadSessionExport(sessionId, credentials.token);
|
|
71
|
+
try {
|
|
72
|
+
console.log('Importing session into OpenCode…');
|
|
73
|
+
const importExitCode = await runOpenCode(['import', exportPath]);
|
|
74
|
+
if (importExitCode !== 0) {
|
|
75
|
+
throw new Error(`Importing session ${sessionId} failed (opencode exit ${importExitCode}).`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
fs.rmSync(path.dirname(exportPath), { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
console.log('Starting a fork of the imported session…\n');
|
|
82
|
+
return runOpenCode(['--session', sessionId, '--fork']);
|
|
83
|
+
}
|
|
56
84
|
async function main() {
|
|
57
85
|
const command = process.argv[2];
|
|
58
86
|
switch (command) {
|
|
@@ -66,8 +94,18 @@ async function main() {
|
|
|
66
94
|
runWhoami();
|
|
67
95
|
return;
|
|
68
96
|
default: {
|
|
97
|
+
const args = process.argv.slice(2);
|
|
98
|
+
const { cloudFork, sessionId } = parseCloudForkArgs(args);
|
|
99
|
+
if (cloudFork) {
|
|
100
|
+
if (!sessionId || !isValidSessionId(sessionId)) {
|
|
101
|
+
throw new Error('Usage: megabrain --session <ses_…> --cloud-fork (a valid cloud session id is required).');
|
|
102
|
+
}
|
|
103
|
+
await ensureSignedIn();
|
|
104
|
+
process.exitCode = await runCloudFork(sessionId);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
69
107
|
await ensureSignedIn();
|
|
70
|
-
const exitCode = await runOpenCode(
|
|
108
|
+
const exitCode = await runOpenCode(args);
|
|
71
109
|
process.exitCode = exitCode;
|
|
72
110
|
}
|
|
73
111
|
}
|
package/package.json
CHANGED