@getmegabrain/cli 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 +40 -0
- package/dist/constants.js +9 -0
- package/dist/credentials.js +26 -0
- package/dist/device-auth.js +65 -0
- package/dist/index.js +70 -0
- package/dist/opencode-config.js +34 -0
- package/dist/paths.js +11 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @getmegabrain/cli
|
|
2
|
+
|
|
3
|
+
A thin wrapper around [OpenCode](https://opencode.ai) that signs you into
|
|
4
|
+
MegaBrain and points OpenCode at the MegaBrain Gateway automatically — no
|
|
5
|
+
provider API keys to configure.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @getmegabrain/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
megabrain # signs in on first run, then starts OpenCode
|
|
17
|
+
megabrain login # sign in (or re-sign-in) explicitly
|
|
18
|
+
megabrain whoami # print the signed-in account's email
|
|
19
|
+
megabrain logout # forget local credentials
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Any arguments after `megabrain` are passed straight through to `opencode`,
|
|
23
|
+
e.g. `megabrain run "fix the failing test"`.
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
- `megabrain login` starts a device-authorization flow: it requests a short
|
|
28
|
+
code from `getmegabrain.com`, opens your browser to confirm it, and polls
|
|
29
|
+
until you approve. On approval it receives a MegaBrain Gateway token.
|
|
30
|
+
- That token is written to `~/.config/megabrain/credentials.json` (mode
|
|
31
|
+
`0600`) and to `~/.config/opencode/opencode.json` as an OpenAI-compatible
|
|
32
|
+
provider pointed at the MegaBrain Gateway — the same config shape BrainClaw
|
|
33
|
+
writes server-side for its own sandboxes
|
|
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.
|
|
38
|
+
|
|
39
|
+
Override the target deployment for local development with
|
|
40
|
+
`MEGABRAIN_APP_URL` (defaults to `https://getmegabrain.com`).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base URL of the MegaBrain web app (device-auth + gateway both live here).
|
|
3
|
+
* Override for local development against a non-production deployment.
|
|
4
|
+
*/
|
|
5
|
+
export const APP_BASE_URL = (process.env.MEGABRAIN_APP_URL ?? 'https://getmegabrain.com').replace(/\/+$/, '');
|
|
6
|
+
/** OpenAI-compatible gateway root; OpenCode's provider appends `/chat/completions`. */
|
|
7
|
+
export const GATEWAY_BASE_URL = `${APP_BASE_URL}/api/gateway/`;
|
|
8
|
+
/** Gateway model id used when the user hasn't picked one. */
|
|
9
|
+
export const DEFAULT_MODEL_ID = 'kilo-auto/balanced';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { CREDENTIALS_FILE, MEGABRAIN_CONFIG_DIR } from './paths.js';
|
|
3
|
+
export function readCredentials() {
|
|
4
|
+
try {
|
|
5
|
+
const raw = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
if (!parsed.token || !parsed.userId || !parsed.userEmail)
|
|
8
|
+
return null;
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function writeCredentials(credentials) {
|
|
16
|
+
fs.mkdirSync(MEGABRAIN_CONFIG_DIR, { recursive: true });
|
|
17
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
18
|
+
}
|
|
19
|
+
export function clearCredentials() {
|
|
20
|
+
try {
|
|
21
|
+
fs.rmSync(CREDENTIALS_FILE, { force: true });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Nothing to remove.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { APP_BASE_URL } from './constants.js';
|
|
3
|
+
async function createDeviceAuthCode() {
|
|
4
|
+
const res = await fetch(`${APP_BASE_URL}/api/device-auth/codes`, { method: 'POST' });
|
|
5
|
+
if (!res.ok) {
|
|
6
|
+
throw new Error(`Could not start sign-in (HTTP ${res.status}). Please try again.`);
|
|
7
|
+
}
|
|
8
|
+
return (await res.json());
|
|
9
|
+
}
|
|
10
|
+
async function pollDeviceAuthCode(code) {
|
|
11
|
+
const res = await fetch(`${APP_BASE_URL}/api/device-auth/codes/${encodeURIComponent(code)}`);
|
|
12
|
+
return (await res.json());
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Best-effort browser launch; the printed URL is always the fallback.
|
|
16
|
+
*
|
|
17
|
+
* `spawn` reports a missing binary via an async `error` event, not a thrown
|
|
18
|
+
* exception, so a listener is required even though we ignore it — otherwise
|
|
19
|
+
* an unhandled `error` event crashes the process.
|
|
20
|
+
*/
|
|
21
|
+
function tryOpenBrowser(url) {
|
|
22
|
+
const platform = process.platform;
|
|
23
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
|
|
24
|
+
const args = platform === 'win32' ? ['', url] : [url];
|
|
25
|
+
try {
|
|
26
|
+
const child = spawn(command, args, {
|
|
27
|
+
shell: platform === 'win32',
|
|
28
|
+
stdio: 'ignore',
|
|
29
|
+
detached: true,
|
|
30
|
+
});
|
|
31
|
+
child.on('error', () => {
|
|
32
|
+
// Ignore — user can open the URL manually.
|
|
33
|
+
});
|
|
34
|
+
child.unref();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Ignore — user can open the URL manually.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const POLL_INTERVAL_MS = 2000;
|
|
41
|
+
export async function login(log = console.log) {
|
|
42
|
+
const { code, verificationUrl, expiresIn } = await createDeviceAuthCode();
|
|
43
|
+
log('');
|
|
44
|
+
log(` Confirm this code: ${code}`);
|
|
45
|
+
log(` Sign in at: ${verificationUrl}`);
|
|
46
|
+
log('');
|
|
47
|
+
log(' Opening your browser…');
|
|
48
|
+
tryOpenBrowser(verificationUrl);
|
|
49
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
52
|
+
const result = await pollDeviceAuthCode(code);
|
|
53
|
+
if (result.status === 'approved') {
|
|
54
|
+
return { token: result.token, userId: result.userId, userEmail: result.userEmail };
|
|
55
|
+
}
|
|
56
|
+
if (result.status === 'denied') {
|
|
57
|
+
throw new Error('Sign-in was denied.');
|
|
58
|
+
}
|
|
59
|
+
if (result.status === 'expired') {
|
|
60
|
+
throw new Error('Sign-in code expired. Run `megabrain login` again.');
|
|
61
|
+
}
|
|
62
|
+
// 'pending' — keep polling.
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Sign-in timed out. Run `megabrain login` again.');
|
|
65
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { login } from './device-auth.js';
|
|
4
|
+
import { readCredentials, writeCredentials, clearCredentials } from './credentials.js';
|
|
5
|
+
import { writeOpenCodeConfig } from './opencode-config.js';
|
|
6
|
+
async function ensureSignedIn() {
|
|
7
|
+
if (readCredentials())
|
|
8
|
+
return;
|
|
9
|
+
console.log('Signing in to MegaBrain…');
|
|
10
|
+
const { token, userId, userEmail } = await login();
|
|
11
|
+
writeCredentials({ token, userId, userEmail });
|
|
12
|
+
writeOpenCodeConfig(token);
|
|
13
|
+
console.log(`\nSigned in as ${userEmail}.\n`);
|
|
14
|
+
}
|
|
15
|
+
async function runLogin() {
|
|
16
|
+
console.log('Signing in to MegaBrain…');
|
|
17
|
+
const { token, userId, userEmail } = await login();
|
|
18
|
+
writeCredentials({ token, userId, userEmail });
|
|
19
|
+
writeOpenCodeConfig(token);
|
|
20
|
+
console.log(`\nSigned in as ${userEmail}. Run \`megabrain\` to start coding.\n`);
|
|
21
|
+
}
|
|
22
|
+
function runLogout() {
|
|
23
|
+
clearCredentials();
|
|
24
|
+
console.log('Signed out.');
|
|
25
|
+
}
|
|
26
|
+
function runWhoami() {
|
|
27
|
+
const credentials = readCredentials();
|
|
28
|
+
if (!credentials) {
|
|
29
|
+
console.log('Not signed in. Run `megabrain login`.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log(credentials.userEmail);
|
|
33
|
+
}
|
|
34
|
+
function runOpenCode(args) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn('opencode', args, { stdio: 'inherit' });
|
|
37
|
+
child.on('error', err => {
|
|
38
|
+
if (err.code === 'ENOENT') {
|
|
39
|
+
reject(new Error('Could not find `opencode` on your PATH. Try reinstalling: npm install -g @getmegabrain/cli'));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
reject(err);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
child.on('exit', code => resolve(code ?? 0));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function main() {
|
|
49
|
+
const command = process.argv[2];
|
|
50
|
+
switch (command) {
|
|
51
|
+
case 'login':
|
|
52
|
+
await runLogin();
|
|
53
|
+
return;
|
|
54
|
+
case 'logout':
|
|
55
|
+
runLogout();
|
|
56
|
+
return;
|
|
57
|
+
case 'whoami':
|
|
58
|
+
runWhoami();
|
|
59
|
+
return;
|
|
60
|
+
default: {
|
|
61
|
+
await ensureSignedIn();
|
|
62
|
+
const exitCode = await runOpenCode(process.argv.slice(2));
|
|
63
|
+
process.exitCode = exitCode;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
main().catch(err => {
|
|
68
|
+
console.error(`\nError: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes OpenCode's config so it authenticates against the MegaBrain Gateway
|
|
3
|
+
* with the token issued during `megabrain login`, instead of requiring the
|
|
4
|
+
* user to configure a provider by hand.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors services/kiloclaw/controller/src/opencode-config.ts (which does the
|
|
7
|
+
* same thing server-side for BrainClaw sandboxes) — same config shape, same
|
|
8
|
+
* gateway, adapted to write to the local user's OpenCode config directory
|
|
9
|
+
* instead of a pinned in-container path.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { GATEWAY_BASE_URL, DEFAULT_MODEL_ID } from './constants.js';
|
|
13
|
+
import { OPENCODE_CONFIG_DIR, OPENCODE_CONFIG_FILE } from './paths.js';
|
|
14
|
+
const PROVIDER_ID = 'megabrain';
|
|
15
|
+
/** OpenCode's `@ai-sdk/openai-compatible` provider appends `/chat/completions` to `baseURL`. */
|
|
16
|
+
function toOpenCodeBaseUrl(gatewayBaseUrl) {
|
|
17
|
+
return gatewayBaseUrl.replace(/\/+$/, '') + '/v1';
|
|
18
|
+
}
|
|
19
|
+
export function writeOpenCodeConfig(token, modelId = DEFAULT_MODEL_ID) {
|
|
20
|
+
const config = {
|
|
21
|
+
$schema: 'https://opencode.ai/config.json',
|
|
22
|
+
provider: {
|
|
23
|
+
[PROVIDER_ID]: {
|
|
24
|
+
npm: '@ai-sdk/openai-compatible',
|
|
25
|
+
name: 'MegaBrain Gateway',
|
|
26
|
+
options: { baseURL: toOpenCodeBaseUrl(GATEWAY_BASE_URL), apiKey: token },
|
|
27
|
+
models: { [modelId]: { name: modelId } },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
model: `${PROVIDER_ID}/${modelId}`,
|
|
31
|
+
};
|
|
32
|
+
fs.mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true });
|
|
33
|
+
fs.writeFileSync(OPENCODE_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** XDG_CONFIG_HOME, falling back to the platform default (`~/.config` on macOS/Linux). */
|
|
4
|
+
function configHome() {
|
|
5
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
6
|
+
}
|
|
7
|
+
export const MEGABRAIN_CONFIG_DIR = path.join(configHome(), 'megabrain');
|
|
8
|
+
export const CREDENTIALS_FILE = path.join(MEGABRAIN_CONFIG_DIR, 'credentials.json');
|
|
9
|
+
/** Matches OpenCode's own config resolution (`$XDG_CONFIG_HOME/opencode` / `~/.config/opencode`). */
|
|
10
|
+
export const OPENCODE_CONFIG_DIR = path.join(configHome(), 'opencode');
|
|
11
|
+
export const OPENCODE_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getmegabrain/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MegaBrain CLI — sign in once, then code from your terminal via OpenCode wired to the MegaBrain Gateway.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"megabrain": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"postbuild": "chmod +x dist/index.js",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"typecheck": "tsgo --noEmit",
|
|
21
|
+
"test": "vitest run --passWithNoTests",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"lint": "pnpm -w exec oxlint --config .oxlintrc.json packages/megabrain-cli/src"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"opencode-ai": "^1.17.11"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "catalog:",
|
|
30
|
+
"@typescript/native-preview": "catalog:",
|
|
31
|
+
"tsx": "catalog:",
|
|
32
|
+
"typescript": "catalog:",
|
|
33
|
+
"vitest": "catalog:"
|
|
34
|
+
}
|
|
35
|
+
}
|