superlean-codex 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/LICENSE +21 -0
- package/README.md +139 -0
- package/bin/superlean.js +48 -0
- package/docs/service-login.md +21 -0
- package/lib/check-codex-auth.js +9 -0
- package/lib/configure-codex.js +25 -0
- package/lib/install.js +68 -0
- package/lib/service-login.js +85 -0
- package/lib/shell-quote.js +3 -0
- package/package.json +44 -0
- package/shell/env.sh +3 -0
- package/shell/launcher.sh +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SuperleanAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Superlean Codex
|
|
2
|
+
|
|
3
|
+
Run Codex through SuperleanAI while keeping your existing ChatGPT login.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install the launcher globally, then run it from any project:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g superlean-codex
|
|
11
|
+
superlean-codex
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or install and launch it in one command:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx --yes superlean-codex@latest
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js 22+ with npm on macOS, Linux, or WSL.
|
|
21
|
+
|
|
22
|
+
## First run
|
|
23
|
+
|
|
24
|
+
The launcher:
|
|
25
|
+
|
|
26
|
+
1. Checks that Codex is already signed in with ChatGPT.
|
|
27
|
+
2. Opens the Superlean dashboard in your browser.
|
|
28
|
+
3. Asks you to approve **Connect Codex** using the code shown in the terminal.
|
|
29
|
+
4. Saves your Superlean identity key and starts Codex in the current project.
|
|
30
|
+
|
|
31
|
+
If the first check fails, run `codex login` directly and choose **Sign in with
|
|
32
|
+
ChatGPT**, then run `superlean-codex` again. Superlean does not read, copy, or
|
|
33
|
+
store the ChatGPT tokens in Codex's credential store.
|
|
34
|
+
|
|
35
|
+
After setup, use the launcher just like Codex:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
superlean-codex
|
|
39
|
+
superlean-codex --resume
|
|
40
|
+
superlean-codex exec "Explain this repository"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Reconnect to Superlean after a key expires or is revoked:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
superlean-codex login
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Reconnects reuse the existing usable Codex key entry. A new entry is created
|
|
50
|
+
only when none can be reused.
|
|
51
|
+
|
|
52
|
+
## What it installs
|
|
53
|
+
|
|
54
|
+
The first run writes the shell launcher to `~/.superlean/bin`, adds that
|
|
55
|
+
directory to Bash or Zsh startup files, and stores the Superlean identity key
|
|
56
|
+
in `~/.superlean/codex.env.sh` with owner-only permissions. Personal overrides
|
|
57
|
+
in `~/.superlean/codex-user.env.sh` are preserved across reinstalls.
|
|
58
|
+
|
|
59
|
+
An existing Codex installation is used when available. Otherwise, npm runs the
|
|
60
|
+
official `@openai/codex` package from its cache. The launcher preserves your
|
|
61
|
+
current directory and forwards all Codex arguments.
|
|
62
|
+
|
|
63
|
+
The launcher applies these settings only to the Codex process it starts; it
|
|
64
|
+
does not rewrite your normal Codex configuration:
|
|
65
|
+
|
|
66
|
+
```toml
|
|
67
|
+
model_provider = "superlean"
|
|
68
|
+
|
|
69
|
+
[model_providers.superlean]
|
|
70
|
+
name = "SuperleanAI"
|
|
71
|
+
base_url = "https://api.superleanai.com/codex/v1"
|
|
72
|
+
wire_api = "responses"
|
|
73
|
+
requires_openai_auth = true
|
|
74
|
+
env_http_headers = { "X-SuperleanAI-Key" = "SUPERLEANAI_CODEX_KEY" }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`requires_openai_auth` keeps your ChatGPT authentication in the Authorization
|
|
78
|
+
header. The separate `X-SuperleanAI-Key` header identifies your Superlean
|
|
79
|
+
account and is removed by the gateway before the request is forwarded.
|
|
80
|
+
|
|
81
|
+
## Other install options
|
|
82
|
+
|
|
83
|
+
Set up the launcher without starting Codex:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
npx --yes superlean-codex@latest install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `--no-profile` to leave shell startup files unchanged. If the installer
|
|
90
|
+
does not edit a profile, it prints the PATH command to add manually.
|
|
91
|
+
|
|
92
|
+
Pass Codex arguments through the npm entry point after `--`:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
npx --yes superlean-codex@latest -- --help
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npm test
|
|
102
|
+
npm run check
|
|
103
|
+
npm pack --dry-run
|
|
104
|
+
node bin/superlean.js --help
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The browser login protocol is documented in
|
|
108
|
+
[docs/service-login.md](docs/service-login.md).
|
|
109
|
+
|
|
110
|
+
## Releasing
|
|
111
|
+
|
|
112
|
+
CI (`.github/workflows/ci.yml`) runs tests and static checks on Node 22 and 24
|
|
113
|
+
for every push to `main` and every pull request. CD
|
|
114
|
+
(`.github/workflows/publish.yml`) publishes to npm when a GitHub Release is
|
|
115
|
+
published, using [npm trusted publishing](https://docs.npmjs.com/trusted-publishers)
|
|
116
|
+
(OIDC) with provenance. No npm token is stored in GitHub.
|
|
117
|
+
|
|
118
|
+
One-time setup:
|
|
119
|
+
|
|
120
|
+
1. If the package does not exist on npm yet, publish the first version manually
|
|
121
|
+
by running `npm publish` from a local checkout.
|
|
122
|
+
2. On npmjs.com, open the package page → **Settings** → **Trusted Publisher**
|
|
123
|
+
and add a GitHub Actions publisher with organization `superleanai`,
|
|
124
|
+
repository `superlean-codex`, and workflow filename `publish.yml`.
|
|
125
|
+
|
|
126
|
+
To cut a release:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npm version patch # or minor / major
|
|
130
|
+
git push --follow-tags
|
|
131
|
+
gh release create v0.1.1 --generate-notes
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The release tag must match the version in `package.json`, and `prepublishOnly`
|
|
135
|
+
runs the test suite and static checks before the package is uploaded.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT © SuperleanAI. See [LICENSE](LICENSE).
|
package/bin/superlean.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { install, shellQuote } from '../lib/install.js';
|
|
5
|
+
|
|
6
|
+
const help = `Usage:
|
|
7
|
+
npx superlean-codex@latest Set up and launch Codex
|
|
8
|
+
npx superlean-codex@latest install Set up without launching
|
|
9
|
+
npx superlean-codex@latest login Sign in to Superlean again
|
|
10
|
+
npx superlean-codex@latest --no-profile Leave shell profiles unchanged
|
|
11
|
+
npx superlean-codex@latest -- [args] Pass arguments to Codex
|
|
12
|
+
|
|
13
|
+
After setup, use superlean-codex in a new terminal.
|
|
14
|
+
Requires macOS, Linux, or WSL and Node.js 22+ with npm.`;
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
let launch = true;
|
|
19
|
+
let profile = true;
|
|
20
|
+
while (args.length) {
|
|
21
|
+
const arg = args[0];
|
|
22
|
+
if (arg === '--') { args.shift(); break; }
|
|
23
|
+
if (arg === '--help' || arg === '-h') { console.log(help); return; }
|
|
24
|
+
if (arg === '--no-profile') { profile = false; args.shift(); continue; }
|
|
25
|
+
if (arg === 'install') { launch = false; args.shift(); continue; }
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === 'win32') throw new Error('Use WSL for the Superlean shell launcher.');
|
|
29
|
+
const result = await install({ home: homedir(), shell: process.env.SHELL, profile });
|
|
30
|
+
console.log(`Superlean installed. Environment: ${result.envPath}`);
|
|
31
|
+
if (result.profiles.length) console.log('Next time, open a new terminal and run superlean-codex.');
|
|
32
|
+
else console.log(`Add the launcher to this shell: export PATH=${shellQuote(result.binDir)}:"$PATH"`);
|
|
33
|
+
if (!launch) return;
|
|
34
|
+
console.log('Checking your Codex login before applying Superlean settings.');
|
|
35
|
+
const child = spawn(result.launcherPath, args, { stdio: 'inherit' });
|
|
36
|
+
const onInterrupt = () => {};
|
|
37
|
+
const onTerminate = () => child.kill('SIGTERM');
|
|
38
|
+
process.on('SIGINT', onInterrupt);
|
|
39
|
+
process.on('SIGTERM', onTerminate);
|
|
40
|
+
const { code, signal } = await new Promise((resolve, reject) => {
|
|
41
|
+
child.once('error', reject);
|
|
42
|
+
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
43
|
+
});
|
|
44
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
45
|
+
process.removeListener('SIGTERM', onTerminate);
|
|
46
|
+
process.exitCode = code ?? (signal === 'SIGINT' ? 130 : 143);
|
|
47
|
+
}
|
|
48
|
+
main().catch((error) => { console.error(`Superlean: ${error.message}`); process.exitCode = 1; });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Browser-approved CLI login
|
|
2
|
+
|
|
3
|
+
The launcher creates a random verifier and sends its SHA-256 challenge to
|
|
4
|
+
`POST /api/cli/login` with `provider: "codex"`. The dashboard returns a
|
|
5
|
+
short-lived flow ID and browser approval URL. After the signed-in user approves
|
|
6
|
+
the matching code, only the terminal holding the verifier can redeem the flow.
|
|
7
|
+
|
|
8
|
+
The redemption response contains a Codex-scoped Superlean JWT. The launcher
|
|
9
|
+
writes it to `~/.superlean/codex.env.sh` and exposes it to Codex as
|
|
10
|
+
`SUPERLEANAI_CODEX_KEY`. Codex reads that value through `env_http_headers` and
|
|
11
|
+
sends it as `X-SuperleanAI-Key`; its own ChatGPT bearer token remains in the
|
|
12
|
+
Authorization header.
|
|
13
|
+
|
|
14
|
+
The gateway accepts this combination only on the `/codex` profile, verifies
|
|
15
|
+
that the Superlean key has a live `proxy:codex` database row, strips the
|
|
16
|
+
identity header, and forwards the user's ChatGPT credential. Reconnecting
|
|
17
|
+
reuses the most recent usable Codex key entry.
|
|
18
|
+
|
|
19
|
+
The flow stores neither the verifier nor either bearer credential. Flow IDs
|
|
20
|
+
are hashed in the database, approvals expire after ten minutes, and successful
|
|
21
|
+
redemption deletes the flow.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
let input = '';
|
|
2
|
+
process.stdin.setEncoding('utf8');
|
|
3
|
+
process.stdin.on('data', (chunk) => {
|
|
4
|
+
input += chunk;
|
|
5
|
+
if (input.length > 64 * 1024) process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
process.stdin.on('end', () => {
|
|
8
|
+
process.exit(/^Logged in using ChatGPT\s*$/m.test(input) ? 0 : 1);
|
|
9
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { chmod, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { shellQuote } from './shell-quote.js';
|
|
4
|
+
|
|
5
|
+
function safe(value, name, { nonempty = true } = {}) {
|
|
6
|
+
if (typeof value !== 'string' || (nonempty && !value) || /[\r\n\0]/.test(value)) {
|
|
7
|
+
throw new Error(`${name} must be ${nonempty ? 'a nonempty string' : 'a string'} without newlines.`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function configureCodex({ directory, baseUrl = 'https://api.superleanai.com/codex/v1', userKey }) {
|
|
13
|
+
safe(directory, 'directory');
|
|
14
|
+
safe(userKey, 'user key');
|
|
15
|
+
const parsed = new URL(safe(baseUrl, 'base URL'));
|
|
16
|
+
if (!['https:', 'http:'].includes(parsed.protocol) || parsed.username || parsed.password) {
|
|
17
|
+
throw new Error('base URL must be an HTTP(S) URL without embedded credentials.');
|
|
18
|
+
}
|
|
19
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
20
|
+
const path = join(directory, 'codex.env.sh');
|
|
21
|
+
const contents = `# Generated by Superlean. Reconnect with: superlean-codex login\nunset OPENAI_API_KEY\nexport SUPERLEANAI_CODEX_KEY=${shellQuote(userKey)}\n`;
|
|
22
|
+
await writeFile(path, contents, { mode: 0o600 });
|
|
23
|
+
await chmod(path, 0o600);
|
|
24
|
+
return path;
|
|
25
|
+
}
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { shellQuote } from './shell-quote.js';
|
|
4
|
+
export { shellQuote } from './shell-quote.js';
|
|
5
|
+
|
|
6
|
+
const marker = '# >>> Superlean >>>';
|
|
7
|
+
const endMarker = '# <<< Superlean <<<';
|
|
8
|
+
|
|
9
|
+
async function readOptional(path) {
|
|
10
|
+
try { return await readFile(path, 'utf8'); }
|
|
11
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function updateProfile(path, binDir) {
|
|
15
|
+
const current = (await readOptional(path)) ?? '';
|
|
16
|
+
const block = `${marker}\ncase ":$PATH:" in\n *:${shellQuote(binDir)}:*) ;;\n *) export PATH=${shellQuote(binDir)}:"$PATH" ;;\nesac\n${endMarker}`;
|
|
17
|
+
const start = current.indexOf(marker);
|
|
18
|
+
let updated;
|
|
19
|
+
if (start === -1) updated = `${current}${current.endsWith('\n') || !current ? '' : '\n'}\n${block}\n`;
|
|
20
|
+
else {
|
|
21
|
+
const end = current.indexOf(endMarker, start);
|
|
22
|
+
if (end === -1) throw new Error(`Incomplete Superlean block in ${path}; repair it and retry.`);
|
|
23
|
+
updated = current.slice(0, start) + block + current.slice(end + endMarker.length);
|
|
24
|
+
}
|
|
25
|
+
if (updated !== current) await writeFile(path, updated, { mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function install({ home, shell = '', profile = true }) {
|
|
29
|
+
if (!home || /[\r\n]/.test(home)) throw new Error('A home directory without newlines is required.');
|
|
30
|
+
const directory = join(home, '.superlean');
|
|
31
|
+
const binDir = join(directory, 'bin');
|
|
32
|
+
const libDir = join(directory, 'lib-codex');
|
|
33
|
+
await mkdir(binDir, { recursive: true, mode: 0o700 });
|
|
34
|
+
await chmod(directory, 0o700);
|
|
35
|
+
await mkdir(libDir, { recursive: true, mode: 0o700 });
|
|
36
|
+
await writeFile(join(libDir, 'package.json'), '{"type":"module"}\n', { mode: 0o600 });
|
|
37
|
+
for (const name of ['service-login.js', 'configure-codex.js', 'shell-quote.js', 'check-codex-auth.js']) {
|
|
38
|
+
await writeFile(join(libDir, name), await readFile(new URL(name, import.meta.url), 'utf8'), { mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
await writeFile(join(directory, 'codex-defaults.sh'), await readFile(new URL('../shell/env.sh', import.meta.url), 'utf8'), { mode: 0o600 });
|
|
41
|
+
const envPath = join(directory, 'codex-user.env.sh');
|
|
42
|
+
try { await writeFile(envPath, '# Personal Codex overrides. Preserved when Superlean is reinstalled.\n', { flag: 'wx', mode: 0o600 }); }
|
|
43
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
44
|
+
const launcherPath = join(binDir, 'superlean-codex');
|
|
45
|
+
const script = (await readFile(new URL('../shell/launcher.sh', import.meta.url), 'utf8'))
|
|
46
|
+
.replaceAll('__SUPERLEAN_DIR__', shellQuote(directory))
|
|
47
|
+
.replaceAll('__SUPERLEAN_LIB_DIR__', shellQuote(libDir));
|
|
48
|
+
await writeFile(launcherPath, script, { mode: 0o700 });
|
|
49
|
+
await chmod(launcherPath, 0o700);
|
|
50
|
+
const profiles = [];
|
|
51
|
+
if (profile) {
|
|
52
|
+
switch (basename(shell)) {
|
|
53
|
+
case 'zsh': profiles.push(join(home, '.zshrc')); break;
|
|
54
|
+
case 'bash': {
|
|
55
|
+
profiles.push(join(home, '.bashrc'));
|
|
56
|
+
const loginFiles = ['.bash_profile', '.bash_login', '.profile'];
|
|
57
|
+
let loginFile = '.profile';
|
|
58
|
+
for (const file of loginFiles) if (await readOptional(join(home, file)) !== null) { loginFile = file; break; }
|
|
59
|
+
profiles.push(join(home, loginFile));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'sh': case 'dash': case '': profiles.push(join(home, '.profile')); break;
|
|
63
|
+
default: break;
|
|
64
|
+
}
|
|
65
|
+
for (const path of profiles) await updateProfile(path, binDir);
|
|
66
|
+
}
|
|
67
|
+
return { directory, binDir, envPath, launcherPath, profiles };
|
|
68
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { configureCodex } from './configure-codex.js';
|
|
6
|
+
|
|
7
|
+
export async function openBrowser(url) {
|
|
8
|
+
const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
9
|
+
await new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawn(command, [url], { stdio: 'ignore', detached: true });
|
|
11
|
+
child.unref();
|
|
12
|
+
child.once('error', reject);
|
|
13
|
+
child.once('exit', (code) => code === 0 ? resolve() : reject(new Error('Browser could not open.')));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function serviceLogin({
|
|
18
|
+
dashboard = 'https://dashboard.superleanai.com', fetchFn = fetch,
|
|
19
|
+
openUrl = openBrowser, wait = delay, output = console.log, signal,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const origin = new URL(dashboard);
|
|
22
|
+
if (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname))) {
|
|
23
|
+
throw new Error('Superlean sign-in requires HTTPS, except on localhost.');
|
|
24
|
+
}
|
|
25
|
+
async function post(path, body) {
|
|
26
|
+
const response = await fetchFn(new URL(path, origin), {
|
|
27
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify(body), redirect: 'error',
|
|
29
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15000)]) : AbortSignal.timeout(15000),
|
|
30
|
+
});
|
|
31
|
+
let data;
|
|
32
|
+
try { data = await response.json(); } catch {
|
|
33
|
+
throw new Error('Superlean CLI sign-in is unavailable. The dashboard must include the CLI login endpoints.');
|
|
34
|
+
}
|
|
35
|
+
return { response, data };
|
|
36
|
+
}
|
|
37
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
38
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
39
|
+
const { response: started, data: flow } = await post('/api/cli/login', { code_challenge: challenge, provider: 'codex' });
|
|
40
|
+
if (started.status !== 201 || !/^[A-Za-z0-9_-]{43}$/.test(flow.flow_id ?? '')
|
|
41
|
+
|| typeof flow.verification_uri !== 'string' || !Number.isFinite(flow.expires_in)
|
|
42
|
+
|| flow.expires_in <= 0 || flow.expires_in > 600 || !Number.isFinite(flow.interval)
|
|
43
|
+
|| flow.interval < 1 || flow.interval > 30) {
|
|
44
|
+
throw new Error('Superlean could not start CLI sign-in. Check that the updated dashboard is deployed.');
|
|
45
|
+
}
|
|
46
|
+
const verificationUrl = new URL(flow.verification_uri, origin);
|
|
47
|
+
if (verificationUrl.origin !== origin.origin || verificationUrl.pathname !== '/cli/authorize'
|
|
48
|
+
|| verificationUrl.searchParams.get('flow') !== flow.flow_id) {
|
|
49
|
+
throw new Error('The dashboard returned an invalid sign-in URL.');
|
|
50
|
+
}
|
|
51
|
+
const userCode = `${flow.flow_id.slice(0, 4)}-${flow.flow_id.slice(4, 8)}`;
|
|
52
|
+
const displayCode = process.stdout.isTTY ? `\u001b[1m${userCode}\u001b[22m` : userCode;
|
|
53
|
+
output(`\n\nSign in to Superlean and approve code ${displayCode}:\n\n`);
|
|
54
|
+
output(verificationUrl.href);
|
|
55
|
+
const deadline = Date.now() + flow.expires_in * 1000;
|
|
56
|
+
// A missing browser (for example over SSH) leaves the printed URL usable.
|
|
57
|
+
Promise.resolve().then(() => openUrl(verificationUrl.href)).catch(() => {});
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
await wait(Math.min(flow.interval * 1000, Math.max(0, deadline - Date.now())), undefined, { signal });
|
|
60
|
+
if (Date.now() >= deadline) break;
|
|
61
|
+
const { response, data } = await post('/api/cli/login/token', { flow_id: flow.flow_id, code_verifier: verifier });
|
|
62
|
+
if (response.status === 202 && data.error === 'authorization_pending') continue;
|
|
63
|
+
if (response.status === 200 && typeof data.user_key === 'string' && data.user_key.trim()) {
|
|
64
|
+
return data.user_key;
|
|
65
|
+
}
|
|
66
|
+
if (response.status === 402) throw new Error('An active Superlean subscription is required. Visit https://dashboard.superleanai.com/subscription, then retry.');
|
|
67
|
+
if (response.status === 410) throw new Error('Superlean sign-in expired or was already used. Run the launcher again.');
|
|
68
|
+
throw new Error('Superlean could not complete sign-in. Run the launcher again.');
|
|
69
|
+
}
|
|
70
|
+
throw new Error('Superlean sign-in expired. Run the launcher again.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
process.once('SIGINT', () => controller.abort());
|
|
76
|
+
process.once('SIGTERM', () => controller.abort());
|
|
77
|
+
try {
|
|
78
|
+
const userKey = await serviceLogin({ signal: controller.signal });
|
|
79
|
+
await configureCodex({ directory: process.argv[2], userKey });
|
|
80
|
+
console.log('Superlean connected. Starting Codex.');
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(controller.signal.aborted ? 'Superlean sign-in cancelled.' : `Superlean: ${error.message}`);
|
|
83
|
+
process.exitCode = controller.signal.aborted ? 130 : 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "superlean-codex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command setup and shell launcher for Codex through SuperleanAI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SuperleanAI",
|
|
7
|
+
"homepage": "https://github.com/superleanai/superlean-codex#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/superleanai/superlean-codex/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/superleanai/superlean-codex.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"codex",
|
|
17
|
+
"openai",
|
|
18
|
+
"superleanai",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"superlean-codex": "bin/superlean.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin/",
|
|
27
|
+
"lib/",
|
|
28
|
+
"shell/",
|
|
29
|
+
"docs/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "node --test",
|
|
38
|
+
"check": "node --check bin/superlean.js && node --check lib/install.js && node --check lib/configure-codex.js && node --check lib/service-login.js && node --check lib/check-codex-auth.js && sh -n shell/launcher.sh && sh -n shell/env.sh",
|
|
39
|
+
"prepublishOnly": "npm test && npm run check"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/shell/env.sh
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
superlean_dir=__SUPERLEAN_DIR__
|
|
5
|
+
superlean_lib_dir=__SUPERLEAN_LIB_DIR__
|
|
6
|
+
|
|
7
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
8
|
+
echo 'Superlean: install Node.js 22+ (with npm) and try again.' >&2
|
|
9
|
+
exit 127
|
|
10
|
+
fi
|
|
11
|
+
if (
|
|
12
|
+
unset OPENAI_API_KEY SUPERLEANAI_CODEX_KEY
|
|
13
|
+
if command -v codex >/dev/null 2>&1; then
|
|
14
|
+
superlean_auth=$(codex login status 2>&1) || exit "$?"
|
|
15
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
16
|
+
superlean_auth=$(npx --yes --package=@openai/codex@latest -- codex login status 2>&1) || exit "$?"
|
|
17
|
+
else
|
|
18
|
+
echo 'Superlean: install Node.js 22+ (with npm), or install Codex and try again.' >&2
|
|
19
|
+
exit 127
|
|
20
|
+
fi
|
|
21
|
+
printf '%s' "$superlean_auth" | node "$superlean_lib_dir/check-codex-auth.js"
|
|
22
|
+
); then
|
|
23
|
+
:
|
|
24
|
+
else
|
|
25
|
+
superlean_status=$?
|
|
26
|
+
echo 'Superlean: could not confirm your ChatGPT login. Run codex login without Superlean settings, then retry.' >&2
|
|
27
|
+
exit "$superlean_status"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
. "$superlean_dir/codex-defaults.sh"
|
|
31
|
+
. "$superlean_dir/codex-user.env.sh"
|
|
32
|
+
|
|
33
|
+
superlean_login=false
|
|
34
|
+
if [ "${1-}" = login ]; then superlean_login=true; shift; fi
|
|
35
|
+
if [ "$superlean_login" = true ] || [ ! -f "$superlean_dir/codex.env.sh" ]; then
|
|
36
|
+
node "$superlean_lib_dir/service-login.js" "$superlean_dir"
|
|
37
|
+
fi
|
|
38
|
+
. "$superlean_dir/codex.env.sh"
|
|
39
|
+
|
|
40
|
+
set -- \
|
|
41
|
+
-c 'model_provider="superlean"' \
|
|
42
|
+
-c 'model_providers.superlean.name="SuperleanAI"' \
|
|
43
|
+
-c 'model_providers.superlean.base_url="https://api.superleanai.com/codex/v1"' \
|
|
44
|
+
-c 'model_providers.superlean.wire_api="responses"' \
|
|
45
|
+
-c 'model_providers.superlean.requires_openai_auth=true' \
|
|
46
|
+
-c 'model_providers.superlean.env_http_headers={"X-SuperleanAI-Key"="SUPERLEANAI_CODEX_KEY"}' \
|
|
47
|
+
"$@"
|
|
48
|
+
|
|
49
|
+
if command -v codex >/dev/null 2>&1; then exec codex "$@"; fi
|
|
50
|
+
if ! command -v npx >/dev/null 2>&1; then
|
|
51
|
+
echo 'Superlean: install Node.js 22+ (with npm), or install Codex and try again.' >&2
|
|
52
|
+
exit 127
|
|
53
|
+
fi
|
|
54
|
+
exec npx --yes --package=@openai/codex@latest -- codex "$@"
|