@bitkyc08/opencodex 0.1.0 → 0.2.1
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 +11 -1
- package/package.json +1 -1
- package/src/cli.ts +14 -2
- package/src/config.ts +7 -1
- package/src/oauth/anthropic.ts +5 -1
- package/src/oauth/local-token-detect.ts +5 -12
- package/src/oauth/login-cli.ts +3 -9
- package/src/open-url.ts +9 -0
- package/src/server.ts +2 -2
- package/src/service.ts +89 -11
package/README.md
CHANGED
|
@@ -30,6 +30,16 @@ Codex CLI / App / SDK ──/v1/responses──▶ opencodex ──▶ Any provi
|
|
|
30
30
|
OpenRouter · Azure · DeepSeek · GLM · …and OpenAI itself
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Supported platforms
|
|
34
|
+
|
|
35
|
+
| OS | Status | Service manager |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| macOS (arm64 / x64) | Fully supported | launchd |
|
|
38
|
+
| Linux (x64 / arm64) | Fully supported | systemd (user unit) |
|
|
39
|
+
| Windows (x64) | Fully supported | Task Scheduler |
|
|
40
|
+
|
|
41
|
+
Requires [Bun](https://bun.sh) 1.1+. All three platforms work natively (no WSL needed on Windows).
|
|
42
|
+
|
|
33
43
|
## Quick start
|
|
34
44
|
|
|
35
45
|
```bash
|
|
@@ -110,7 +120,7 @@ ocx status # is the proxy running?
|
|
|
110
120
|
ocx login <xai|anthropic|kimi> # OAuth login
|
|
111
121
|
ocx logout <provider> # remove a stored login
|
|
112
122
|
ocx gui # open the web dashboard
|
|
113
|
-
ocx service <install|start|stop|status|uninstall> #
|
|
123
|
+
ocx service <install|start|stop|status|uninstall> # background service (launchd/systemd/schtasks)
|
|
114
124
|
ocx update # update opencodex to the latest published version
|
|
115
125
|
```
|
|
116
126
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -84,11 +84,22 @@ function handleStart() {
|
|
|
84
84
|
process.on("SIGTERM", shutdown);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function killProxy(pid: number): void {
|
|
88
|
+
if (process.platform === "win32") {
|
|
89
|
+
try {
|
|
90
|
+
(require("node:child_process") as typeof import("node:child_process"))
|
|
91
|
+
.execSync(`taskkill /PID ${pid} /T /F`, { stdio: "pipe" });
|
|
92
|
+
} catch { /* process already gone */ }
|
|
93
|
+
} else {
|
|
94
|
+
process.kill(pid, "SIGTERM");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
function handleStop() {
|
|
88
99
|
const pid = readPid();
|
|
89
100
|
if (pid) {
|
|
90
101
|
try {
|
|
91
|
-
|
|
102
|
+
killProxy(pid);
|
|
92
103
|
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
93
104
|
} catch {
|
|
94
105
|
console.log("Proxy process not found.");
|
|
@@ -159,7 +170,8 @@ switch (command) {
|
|
|
159
170
|
await new Promise(r => setTimeout(r, 1000));
|
|
160
171
|
}
|
|
161
172
|
console.log(`Opening ${guiUrl}`);
|
|
162
|
-
|
|
173
|
+
const { openUrl } = await import("./open-url");
|
|
174
|
+
openUrl(guiUrl);
|
|
163
175
|
break;
|
|
164
176
|
}
|
|
165
177
|
case "service":
|
package/src/config.ts
CHANGED
|
@@ -94,7 +94,13 @@ export function readPid(): number | null {
|
|
|
94
94
|
const raw = readFileSync(PID_PATH, "utf-8").trim();
|
|
95
95
|
const pid = parseInt(raw, 10);
|
|
96
96
|
if (isNaN(pid)) return null;
|
|
97
|
-
try {
|
|
97
|
+
try {
|
|
98
|
+
process.kill(pid, 0);
|
|
99
|
+
return pid;
|
|
100
|
+
} catch (e: unknown) {
|
|
101
|
+
if ((e as NodeJS.ErrnoException).code === "EPERM") return pid;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
98
104
|
} catch {
|
|
99
105
|
return null;
|
|
100
106
|
}
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -135,7 +135,11 @@ export async function loginAnthropic(
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
} else if (importLocal === "only") {
|
|
138
|
-
throw new Error(
|
|
138
|
+
throw new Error(
|
|
139
|
+
process.platform === "darwin"
|
|
140
|
+
? "No Claude Code token found in the keychain. Run 'ocx login anthropic' for browser OAuth."
|
|
141
|
+
: "Claude Code auto-import is macOS-only. Run 'ocx login anthropic' for browser OAuth.",
|
|
142
|
+
);
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
return new AnthropicOAuthFlow(ctrl).login();
|
|
@@ -38,20 +38,13 @@ export function detectGrokCliToken(): OAuthCredentials | null {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Read the Claude Code OAuth credential from
|
|
41
|
+
/** Read the Claude Code OAuth credential from macOS Keychain. Windows/Linux: use `ocx login`. */
|
|
42
42
|
function readClaudeSecureStorage(): string | null {
|
|
43
|
+
if (process.platform !== "darwin") return null;
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}).trim();
|
|
48
|
-
}
|
|
49
|
-
if (process.platform === "linux") {
|
|
50
|
-
return execSync(`secret-tool lookup service "${CLAUDE_KEYCHAIN_SERVICE}"`, {
|
|
51
|
-
encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
-
}).trim();
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
45
|
+
return execSync(`security find-generic-password -s "${CLAUDE_KEYCHAIN_SERVICE}" -w`, {
|
|
46
|
+
encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
47
|
+
}).trim();
|
|
55
48
|
} catch {
|
|
56
49
|
return null;
|
|
57
50
|
}
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
|
-
import {
|
|
2
|
+
import { openUrl } from "../open-url";
|
|
3
3
|
import { loadConfig, readPid, saveConfig } from "../config";
|
|
4
4
|
import { OAUTH_PROVIDERS, runLogin } from "./index";
|
|
5
5
|
import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey } from "./key-providers";
|
|
6
6
|
|
|
7
|
-
function openBrowser(url: string): void {
|
|
8
|
-
const cmd =
|
|
9
|
-
process.platform === "darwin" ? "open" : process.platform === "win32" ? 'start ""' : "xdg-open";
|
|
10
|
-
exec(`${cmd} "${url}"`, () => {});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
/** Push the new provider into a running proxy's live config so it routes without a restart. */
|
|
14
8
|
async function notifyRunningProxy(name: string, provider: unknown): Promise<void> {
|
|
15
9
|
if (!readPid()) return;
|
|
@@ -44,7 +38,7 @@ async function handleOAuthLogin(name: string): Promise<void> {
|
|
|
44
38
|
onAuth: ({ url, instructions }) => {
|
|
45
39
|
console.log(`\n🔐 Opening browser for ${name} login...\n${url}\n`);
|
|
46
40
|
if (instructions) console.log(instructions);
|
|
47
|
-
|
|
41
|
+
openUrl(url);
|
|
48
42
|
},
|
|
49
43
|
onProgress: (m) => console.log(` ${m}`),
|
|
50
44
|
onManualCodeInput: () =>
|
|
@@ -60,7 +54,7 @@ async function handleOAuthLogin(name: string): Promise<void> {
|
|
|
60
54
|
async function handleKeyLogin(name: string): Promise<void> {
|
|
61
55
|
const def = KEY_LOGIN_PROVIDERS[name];
|
|
62
56
|
console.log(`\n🔑 ${def.label} — opening ${def.dashboardUrl} so you can create/copy an API key...`);
|
|
63
|
-
|
|
57
|
+
openUrl(def.dashboardUrl);
|
|
64
58
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
65
59
|
const key = (await new Promise<string>((res) => rl.question(`Paste your ${def.label} API key: `, res))).trim();
|
|
66
60
|
rl.close();
|
package/src/open-url.ts
ADDED
package/src/server.ts
CHANGED
|
@@ -403,8 +403,8 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
|
|
|
403
403
|
if (authUrl) {
|
|
404
404
|
// Open the browser server-side (the proxy runs on the user's machine) — the GUI's
|
|
405
405
|
// window.open is popup-blocked because it runs after an await, not a direct click.
|
|
406
|
-
const
|
|
407
|
-
(
|
|
406
|
+
const { openUrl } = await import("./open-url");
|
|
407
|
+
openUrl(authUrl);
|
|
408
408
|
}
|
|
409
409
|
return jsonResponse({ url: authUrl, instructions });
|
|
410
410
|
} catch (err) {
|
package/src/service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ocx service` — run the proxy as a background service that auto-starts on login and
|
|
3
|
-
* auto-restarts on crash. macOS → launchd
|
|
4
|
-
* The
|
|
3
|
+
* auto-restarts on crash. macOS → launchd; Windows → Task Scheduler; Linux → systemd user unit.
|
|
4
|
+
* The service sets OCX_SERVICE=1 so the proxy's shutdown handler does NOT restore native
|
|
5
5
|
* Codex on a service-managed restart (the restarted instance re-injects); explicit stop/uninstall
|
|
6
6
|
* restore it via the command.
|
|
7
7
|
*/
|
|
@@ -91,35 +91,113 @@ function stopWindows(): void { try { sh(`schtasks /end /tn ${TASK}`); } catch {
|
|
|
91
91
|
function statusWindows(): string { try { return sh(`schtasks /query /tn ${TASK}`); } catch { return ""; } }
|
|
92
92
|
function uninstallWindows(): void { try { sh(`schtasks /delete /tn ${TASK} /f`); } catch { /* absent */ } }
|
|
93
93
|
|
|
94
|
+
// ── Linux (systemd user unit) ──
|
|
95
|
+
function unitDir(): string {
|
|
96
|
+
return join(homedir(), ".config", "systemd", "user");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function unitPath(): string {
|
|
100
|
+
return join(unitDir(), `${TASK}.service`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildUnit(): string {
|
|
104
|
+
const { bun, cli } = cliEntry();
|
|
105
|
+
const log = logPath();
|
|
106
|
+
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
107
|
+
return `[Unit]
|
|
108
|
+
Description=OpenCodex Proxy Server
|
|
109
|
+
After=network-online.target
|
|
110
|
+
Wants=network-online.target
|
|
111
|
+
|
|
112
|
+
[Service]
|
|
113
|
+
Type=simple
|
|
114
|
+
ExecStart=${bun} ${cli} start
|
|
115
|
+
Restart=on-failure
|
|
116
|
+
RestartSec=5
|
|
117
|
+
Environment=OCX_SERVICE=1
|
|
118
|
+
Environment=PATH=${path}
|
|
119
|
+
StandardOutput=append:${log}
|
|
120
|
+
StandardError=append:${log}
|
|
121
|
+
|
|
122
|
+
[Install]
|
|
123
|
+
WantedBy=default.target
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isSystemd(): boolean {
|
|
128
|
+
try { execSync("systemctl --version", { stdio: "pipe" }); return true; } catch { return false; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function installSystemd(): void {
|
|
132
|
+
const dir = unitDir();
|
|
133
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
134
|
+
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
135
|
+
writeFileSync(unitPath(), buildUnit(), "utf8");
|
|
136
|
+
sh("systemctl --user daemon-reload");
|
|
137
|
+
sh(`systemctl --user enable --now ${TASK}`);
|
|
138
|
+
}
|
|
139
|
+
function startSystemd(): void { sh(`systemctl --user start ${TASK}`); }
|
|
140
|
+
function stopSystemd(): void { try { sh(`systemctl --user stop ${TASK}`); } catch { /* not running */ } }
|
|
141
|
+
function statusSystemd(): string { try { return sh(`systemctl --user status ${TASK}`); } catch { return ""; } }
|
|
142
|
+
function uninstallSystemd(): void {
|
|
143
|
+
try { sh(`systemctl --user disable --now ${TASK}`); } catch { /* absent */ }
|
|
144
|
+
if (existsSync(unitPath())) unlinkSync(unitPath());
|
|
145
|
+
try { sh("systemctl --user daemon-reload"); } catch { /* best-effort */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type ServiceOps = {
|
|
149
|
+
install: () => void; start: () => void; stop: () => void;
|
|
150
|
+
status: () => string; uninstall: () => void;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function platformOps(): ServiceOps | null {
|
|
154
|
+
if (process.platform === "darwin")
|
|
155
|
+
return { install: installLaunchd, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
|
|
156
|
+
if (process.platform === "win32")
|
|
157
|
+
return { install: installWindows, start: startWindows, stop: stopWindows, status: statusWindows, uninstall: uninstallWindows };
|
|
158
|
+
if (process.platform === "linux") {
|
|
159
|
+
if (existsSync("/.dockerenv")) {
|
|
160
|
+
console.error("Docker detected. Run 'ocx start' directly instead of using the service manager.");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
if (!isSystemd()) {
|
|
164
|
+
console.error("systemd not found. Run 'ocx start' under your process supervisor.");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
return { install: installSystemd, start: startSystemd, stop: stopSystemd, status: statusSystemd, uninstall: uninstallSystemd };
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
94
172
|
export function serviceCommand(sub?: string): void {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.error("ocx service supports macOS (launchd) and Windows (Task Scheduler). On Linux, run 'ocx start' under systemd or your process supervisor.");
|
|
173
|
+
const ops = platformOps();
|
|
174
|
+
if (!ops) {
|
|
175
|
+
console.error("ocx service supports macOS (launchd), Windows (Task Scheduler), and Linux (systemd).");
|
|
99
176
|
process.exit(1);
|
|
100
177
|
}
|
|
101
178
|
switch (sub) {
|
|
102
179
|
case "install":
|
|
103
|
-
|
|
180
|
+
ops.install();
|
|
104
181
|
console.log("✅ opencodex service installed + started (auto-starts on login, auto-restarts on crash).");
|
|
182
|
+
if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
|
|
105
183
|
break;
|
|
106
184
|
case "start":
|
|
107
|
-
|
|
185
|
+
ops.start();
|
|
108
186
|
console.log("✅ service started.");
|
|
109
187
|
break;
|
|
110
188
|
case "stop":
|
|
111
|
-
|
|
189
|
+
ops.stop();
|
|
112
190
|
restoreNativeCodex();
|
|
113
191
|
console.log("✅ service stopped + native Codex restored.");
|
|
114
192
|
break;
|
|
115
193
|
case "status": {
|
|
116
|
-
const s =
|
|
194
|
+
const s = ops.status();
|
|
117
195
|
console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running.");
|
|
118
196
|
break;
|
|
119
197
|
}
|
|
120
198
|
case "uninstall":
|
|
121
199
|
case "remove":
|
|
122
|
-
|
|
200
|
+
ops.uninstall();
|
|
123
201
|
restoreNativeCodex();
|
|
124
202
|
console.log("✅ service uninstalled + native Codex restored.");
|
|
125
203
|
break;
|