@getmegabrain/cli 0.1.3 → 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/LICENSE.md +19 -0
- package/README.md +8 -3
- package/dist/cloud-fork.js +104 -0
- package/dist/index.js +42 -3
- package/dist/opencode.js +65 -0
- package/package.json +12 -12
package/LICENSE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2025-present Kilo Code Inc.
|
|
2
|
+
|
|
3
|
+
Open Core Ventures Source Available License (OCVSAL) version 1.0
|
|
4
|
+
|
|
5
|
+
Using software and associated documentation files (the "Software") in production requires a valid commercial agreement from the copyright holder.
|
|
6
|
+
|
|
7
|
+
You are free to modify the Software, test it, and publish modifications to the Software. You agree that the copyright holder retains all right, title and interest in and to all modifications.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
10
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
11
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
12
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
13
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
14
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
15
|
+
SOFTWARE.
|
|
16
|
+
|
|
17
|
+
For all third party components incorporated into this Software, those
|
|
18
|
+
components are licensed under the original license provided by the owner of the
|
|
19
|
+
applicable component.
|
package/README.md
CHANGED
|
@@ -32,9 +32,14 @@ e.g. `megabrain run "fix the failing test"`.
|
|
|
32
32
|
provider pointed at the MegaBrain Gateway — the same config shape BrainClaw
|
|
33
33
|
writes server-side for its own sandboxes
|
|
34
34
|
(`services/kiloclaw/controller/src/opencode-config.ts`).
|
|
35
|
-
- Every other command execs the real `opencode` binary
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
- Every other command execs the real `opencode` binary. That binary ships
|
|
36
|
+
bundled inside this package (the `opencode-ai` dependency), and the CLI runs
|
|
37
|
+
it by its absolute path in `node_modules` — it is never expected on your
|
|
38
|
+
`PATH`, and you never install `opencode` yourself. If a restrictive install
|
|
39
|
+
(e.g. `--ignore-scripts`, pnpm, or an allow-scripts policy) skipped
|
|
40
|
+
`opencode-ai`'s postinstall, the CLI fetches the native binary on first
|
|
41
|
+
launch. So it behaves exactly like OpenCode — this package only handles auth
|
|
42
|
+
and gateway wiring.
|
|
38
43
|
|
|
39
44
|
Override the target deployment for local development with
|
|
40
45
|
`MEGABRAIN_APP_URL` (defaults to `https://getmegabrain.com`).
|
|
@@ -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,9 +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';
|
|
9
|
+
import { ensureOpencodeBinary } from './opencode.js';
|
|
10
|
+
import { parseCloudForkArgs, isValidSessionId, downloadSessionExport } from './cloud-fork.js';
|
|
7
11
|
async function signIn() {
|
|
8
12
|
console.log('Signing in to MegaBrain…');
|
|
9
13
|
const { token, userId, userEmail } = await login();
|
|
@@ -40,10 +44,10 @@ function runWhoami() {
|
|
|
40
44
|
}
|
|
41
45
|
function runOpenCode(args) {
|
|
42
46
|
return new Promise((resolve, reject) => {
|
|
43
|
-
const child = spawn(
|
|
47
|
+
const child = spawn(ensureOpencodeBinary(), args, { stdio: 'inherit' });
|
|
44
48
|
child.on('error', err => {
|
|
45
49
|
if (err.code === 'ENOENT') {
|
|
46
|
-
reject(new Error('
|
|
50
|
+
reject(new Error('The bundled `opencode` binary is missing. Try reinstalling: npm install -g @getmegabrain/cli'));
|
|
47
51
|
}
|
|
48
52
|
else {
|
|
49
53
|
reject(err);
|
|
@@ -52,6 +56,31 @@ function runOpenCode(args) {
|
|
|
52
56
|
child.on('exit', code => resolve(code ?? 0));
|
|
53
57
|
});
|
|
54
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
|
+
}
|
|
55
84
|
async function main() {
|
|
56
85
|
const command = process.argv[2];
|
|
57
86
|
switch (command) {
|
|
@@ -65,8 +94,18 @@ async function main() {
|
|
|
65
94
|
runWhoami();
|
|
66
95
|
return;
|
|
67
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
|
+
}
|
|
68
107
|
await ensureSignedIn();
|
|
69
|
-
const exitCode = await runOpenCode(
|
|
108
|
+
const exitCode = await runOpenCode(args);
|
|
70
109
|
process.exitCode = exitCode;
|
|
71
110
|
}
|
|
72
111
|
}
|
package/dist/opencode.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the OpenCode binary that ships *inside* this package.
|
|
3
|
+
*
|
|
4
|
+
* `opencode` is a dependency (`opencode-ai`), not something the user installs
|
|
5
|
+
* or needs to know about. We must NOT invoke it by bare name off `PATH`:
|
|
6
|
+
* npm only links the top-level package's bin (`megabrain`) into the global bin
|
|
7
|
+
* directory on `npm install -g`, so a nested dependency's `opencode` bin never
|
|
8
|
+
* lands on `PATH`. Instead we resolve the bundled binary's absolute path from
|
|
9
|
+
* our own `node_modules` and exec that.
|
|
10
|
+
*
|
|
11
|
+
* `opencode-ai` materializes the platform-specific native binary in its
|
|
12
|
+
* `postinstall` (it copies the right `opencode-<platform>-<arch>` build into
|
|
13
|
+
* `bin/opencode.exe`). If install scripts were skipped (pnpm's default, npm
|
|
14
|
+
* `--ignore-scripts`, or an allow-scripts policy), that file is still the stub
|
|
15
|
+
* shell script shipped in the tarball. Detect that case and run the postinstall
|
|
16
|
+
* ourselves so the binary is fetched on first launch.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
/** Marker printed by the placeholder bin when opencode-ai's postinstall never ran. */
|
|
24
|
+
const STUB_MARKER = 'postinstall script was not run';
|
|
25
|
+
function opencodePackageDir() {
|
|
26
|
+
return path.dirname(require.resolve('opencode-ai/package.json'));
|
|
27
|
+
}
|
|
28
|
+
/** Absolute path to the bundled OpenCode binary. Pure: no side effects. */
|
|
29
|
+
export function opencodeBinaryPath() {
|
|
30
|
+
return path.join(opencodePackageDir(), 'bin', 'opencode.exe');
|
|
31
|
+
}
|
|
32
|
+
/** True when the binary is missing or is still the un-materialized stub. */
|
|
33
|
+
function needsPostinstall(binPath) {
|
|
34
|
+
let fd;
|
|
35
|
+
try {
|
|
36
|
+
fd = fs.openSync(binPath, 'r');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const buf = Buffer.alloc(256);
|
|
43
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
44
|
+
return buf.toString('utf8', 0, bytesRead).includes(STUB_MARKER);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
fs.closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns the bundled OpenCode binary path, running opencode-ai's postinstall
|
|
52
|
+
* first if the native binary hasn't been materialized yet.
|
|
53
|
+
*/
|
|
54
|
+
export function ensureOpencodeBinary() {
|
|
55
|
+
const binPath = opencodeBinaryPath();
|
|
56
|
+
if (needsPostinstall(binPath)) {
|
|
57
|
+
const postinstall = path.join(opencodePackageDir(), 'postinstall.mjs');
|
|
58
|
+
console.log('Fetching OpenCode binary…');
|
|
59
|
+
spawnSync(process.execPath, [postinstall], {
|
|
60
|
+
cwd: opencodePackageDir(),
|
|
61
|
+
stdio: 'inherit',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return binPath;
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmegabrain/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "MegaBrain CLI — sign in once, then code from your terminal via OpenCode wired to the MegaBrain Gateway.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"opencode-ai": "^1.17.11"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "24.12.4",
|
|
24
|
+
"@typescript/native-preview": "7.0.0-dev.20260514.1",
|
|
25
|
+
"tsx": "4.21.0",
|
|
26
|
+
"typescript": "5.9.3",
|
|
27
|
+
"vitest": "4.1.6"
|
|
28
|
+
},
|
|
19
29
|
"scripts": {
|
|
20
30
|
"build": "tsc -p tsconfig.build.json",
|
|
21
31
|
"postbuild": "chmod +x dist/index.js",
|
|
@@ -24,15 +34,5 @@
|
|
|
24
34
|
"test": "vitest run --passWithNoTests",
|
|
25
35
|
"test:watch": "vitest",
|
|
26
36
|
"lint": "pnpm -w exec oxlint --config .oxlintrc.json packages/megabrain-cli/src"
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"opencode-ai": "^1.17.11"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/node": "catalog:",
|
|
33
|
-
"@typescript/native-preview": "catalog:",
|
|
34
|
-
"tsx": "catalog:",
|
|
35
|
-
"typescript": "catalog:",
|
|
36
|
-
"vitest": "catalog:"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|