@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 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 (installed as a
36
- dependency of this package), so it behaves exactly like OpenCode — this
37
- package only handles auth and gateway wiring.
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('opencode', args, { stdio: 'inherit' });
47
+ const child = spawn(ensureOpencodeBinary(), args, { stdio: 'inherit' });
44
48
  child.on('error', err => {
45
49
  if (err.code === 'ENOENT') {
46
- reject(new Error('Could not find `opencode` on your PATH. Try reinstalling: npm install -g @getmegabrain/cli'));
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(process.argv.slice(2));
108
+ const exitCode = await runOpenCode(args);
70
109
  process.exitCode = exitCode;
71
110
  }
72
111
  }
@@ -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",
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
+ }