@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 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> # run as a background service
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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
- process.kill(pid, "SIGTERM");
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
- (await import("node:child_process")).exec(`open ${guiUrl}`);
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 { process.kill(pid, 0); return pid; } catch { return null; }
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
  }
@@ -135,7 +135,11 @@ export async function loginAnthropic(
135
135
  }
136
136
  }
137
137
  } else if (importLocal === "only") {
138
- throw new Error("No Claude Code token found in the keychain. Run 'ocx login anthropic' for browser OAuth.");
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 the OS secure store (macOS keychain / linux secret-tool). */
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
- if (process.platform === "darwin") {
45
- return execSync(`security find-generic-password -s "${CLAUDE_KEYCHAIN_SERVICE}" -w`, {
46
- encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
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
  }
@@ -1,15 +1,9 @@
1
1
  import * as readline from "node:readline";
2
- import { exec } from "node:child_process";
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
- openBrowser(url);
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
- openBrowser(def.dashboardUrl);
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();
@@ -0,0 +1,9 @@
1
+ import { exec } from "node:child_process";
2
+
3
+ export function openUrl(url: string): void {
4
+ const cmd =
5
+ process.platform === "darwin" ? "open"
6
+ : process.platform === "win32" ? 'start ""'
7
+ : "xdg-open";
8
+ exec(`${cmd} ${JSON.stringify(url)}`);
9
+ }
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 cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? 'start ""' : "xdg-open";
407
- (await import("node:child_process")).exec(`${cmd} "${authUrl}"`, () => {});
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 LaunchAgent; Windows → Task Scheduler.
4
- * The plist/task sets OCX_SERVICE=1 so the proxy's shutdown handler does NOT restore native
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 mac = process.platform === "darwin";
96
- const win = process.platform === "win32";
97
- if (!mac && !win) {
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
- mac ? installLaunchd() : installWindows();
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
- mac ? startLaunchd() : startWindows();
185
+ ops.start();
108
186
  console.log("✅ service started.");
109
187
  break;
110
188
  case "stop":
111
- mac ? stopLaunchd() : stopWindows();
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 = mac ? statusLaunchd() : statusWindows();
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
- mac ? uninstallLaunchd() : uninstallWindows();
200
+ ops.uninstall();
123
201
  restoreNativeCodex();
124
202
  console.log("✅ service uninstalled + native Codex restored.");
125
203
  break;