@bman654/clodex 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 +183 -0
- package/dist/chunk-C7N2JI7Z.js +430 -0
- package/dist/chunk-C7N2JI7Z.js.map +1 -0
- package/dist/claude-wrapper.js +95 -0
- package/dist/claude-wrapper.js.map +1 -0
- package/dist/cli.js +12358 -0
- package/dist/cli.js.map +1 -0
- package/docs/background-agents.md +54 -0
- package/package.json +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jacob Ben David
|
|
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,183 @@
|
|
|
1
|
+
# clodex
|
|
2
|
+
|
|
3
|
+
**clodex** bridges [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to OpenAI models — with an OpenAI API key or a ChatGPT/Codex-plan OAuth login. It translates Claude Code's Anthropic wire format to the OpenAI API through the Vercel AI SDK, with working prompt caching, accurate context windows and auto-compaction, live mid-session model switching, and an optional Claude Code binary patcher that makes your OpenAI models first-class citizens inside Claude Code (validated names, `/model` entries, correct context reporting).
|
|
4
|
+
|
|
5
|
+
> clodex is derived from the original [relay-ai](https://github.com/jacob-bd/relay-ai) project, heavily modified and streamlined for this one use case, with the full commit history preserved.
|
|
6
|
+
|
|
7
|
+
## Get started (ChatGPT/Codex plan)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @bman654/clodex # 1. install the CLI (Node 22+)
|
|
11
|
+
clodex providers auth openai # 2. sign in with your ChatGPT/Codex plan (device-code OAuth)
|
|
12
|
+
clodex models # 3. pick favorite models and short aliases
|
|
13
|
+
clodex patch # 4. (optional) patch Claude Code so those models are first-class
|
|
14
|
+
clodex claude # 5. launch Claude Code on an OpenAI model
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
1. **Install** — puts the `clodex` command on your PATH.
|
|
18
|
+
2. **Sign in** — opens a device-code OAuth flow for your ChatGPT/Codex plan; the token is stored in your OS credential store. (API-key users: `clodex providers add` instead.)
|
|
19
|
+
3. **Pick models** — an interactive manager for favorites (max 20) and short aliases like `sol`. Favorites drive the `/model` switch menu, proxy-mode routing, and the patcher.
|
|
20
|
+
4. **Patch** *(optional but recommended for proxy mode)* — bakes your favorites and aliases into the Claude Code binary so they pass model validation, appear in `/model`, and report their real context windows. Re-run after each `claude` update; `clodex patch --restore` undoes it.
|
|
21
|
+
5. **Launch** — starts Claude Code bridged to the model you choose.
|
|
22
|
+
|
|
23
|
+
## Bridge modes
|
|
24
|
+
|
|
25
|
+
Both `clodex claude` and `clodex server` support two bridge modes. A mode flag applies to **that run only**; to change a command's default, add `--save-mode` (e.g. `clodex claude --endpoint --save-mode`). With no flag and nothing saved, both commands default to **proxy** mode, which works with your existing Claude auth.
|
|
26
|
+
|
|
27
|
+
- **`--proxy`** (the default): a selective man-in-the-middle proxy for `api.anthropic.com`. Claude Code keeps its normal Anthropic login — Anthropic models work untouched — while models named `clodex:<provider-id>:<model-id>` (or their saved aliases) route to OpenAI. Switch with `/model clodex:openai-oauth:gpt-5.6-sol` or `/model sol` after patching.
|
|
28
|
+
- **`--endpoint`**: clodex runs a local Anthropic-format gateway and launches Claude Code with `ANTHROPIC_BASE_URL` pointed at it. All traffic goes through the gateway. With favorites saved, the gateway is multi-route and Claude Code's `/model` menu lists your starting model plus favorites for live switching.
|
|
29
|
+
|
|
30
|
+
In proxy mode, Claude Code keeps its own Anthropic credentials and only requests naming a `clodex:` model or alias are rerouted:
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
flowchart LR
|
|
34
|
+
CC["Claude Code<br/>(own Anthropic login)"] -->|"HTTPS via HTTPS_PROXY,<br/>trusts the clodex CA"| MITM["clodex MITM proxy"]
|
|
35
|
+
MITM --> DEC{"model is clodex:...<br/>or a saved alias?"}
|
|
36
|
+
DEC -->|"yes — translated request,<br/>clodex-managed OpenAI credentials"| OAI["OpenAI<br/>(OAuth: Responses WebSocket /<br/>API key: HTTPS)"]
|
|
37
|
+
DEC -->|"no — passed through untouched,<br/>Claude Code's Anthropic credentials ride along"| ANT["api.anthropic.com"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
In endpoint mode, no Anthropic account is involved — Claude Code is launched pointing at the local gateway with a local API key, and its startup `/v1/models` fetch powers the `/model` menu:
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
flowchart LR
|
|
44
|
+
CC["Claude Code<br/>(ANTHROPIC_BASE_URL + local API key,<br/>no Anthropic account credentials)"] -->|"Anthropic-format /v1/messages<br/>+ local API key"| GW["clodex gateway<br/>(:17645/anthropic)"]
|
|
45
|
+
CC -->|"GET /v1/models at startup"| GW
|
|
46
|
+
GW -->|"model catalog with context windows<br/>(feeds the /model menu)"| CC
|
|
47
|
+
GW -->|"translated request,<br/>clodex-managed OpenAI credentials"| OAI["OpenAI"]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Using Claude Code's agents view or background agents? Ask your Claude Code agent to read [docs/background-agents.md](docs/background-agents.md) and set it up for you — one global `clodex server --proxy` plus the `clodex-claude` wrapper bin bridges every claude process automatically.
|
|
51
|
+
|
|
52
|
+
## CLI reference
|
|
53
|
+
|
|
54
|
+
### `clodex claude [options] [claude-flags]`
|
|
55
|
+
|
|
56
|
+
Launch Claude Code bridged to OpenAI models. Unrecognized flags (and everything after `--`) pass through to Claude Code (`-c`, `--resume`, `--print`, …).
|
|
57
|
+
|
|
58
|
+
| Flag | Effect |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `--endpoint` | Endpoint bridge mode for this run: local gateway + `ANTHROPIC_BASE_URL` |
|
|
61
|
+
| `--proxy` | Proxy bridge mode for this run: keep Claude Code's Anthropic auth; `clodex:` models route to OpenAI (default when nothing is saved) |
|
|
62
|
+
| `--save-mode` | With `--endpoint`/`--proxy`: save that mode as the `claude` default |
|
|
63
|
+
| `--dry-run` | Run the wizard but print a launch preview instead of launching (never persists anything) |
|
|
64
|
+
| `--trace` | Write debug logs to `~/.clodex/logs/` and show errors on exit |
|
|
65
|
+
| `--provider <id>` | Boot provider id (`openai` or `openai-oauth`); with `--model`, skips the wizard |
|
|
66
|
+
| `--model <id>` | Boot model id; with `--provider`, skips the wizard |
|
|
67
|
+
| `--help`, `--version` | Help / version |
|
|
68
|
+
|
|
69
|
+
Notes:
|
|
70
|
+
|
|
71
|
+
- Claude Code may save the launched model to `~/.claude/settings.json`, so bare `claude` later can still show a clodex model name. Reset with `claude --model sonnet`.
|
|
72
|
+
- Non-interactive stdin reuses your last provider/model instead of showing the wizard.
|
|
73
|
+
|
|
74
|
+
### `clodex server [options]`
|
|
75
|
+
|
|
76
|
+
Foreground gateway, same two bridge modes, no Claude Code launch — point any Anthropic-format (or OpenAI-format) client at it.
|
|
77
|
+
|
|
78
|
+
Common options (both modes):
|
|
79
|
+
|
|
80
|
+
| Flag | Effect |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `--endpoint` | Endpoint mode for this run: Anthropic-format HTTP gateway |
|
|
83
|
+
| `--proxy` | Proxy mode for this run: selective `api.anthropic.com` MITM proxy (default when nothing is saved; local only) |
|
|
84
|
+
| `--save-mode` | With `--endpoint`/`--proxy`: save that mode as the `server` default |
|
|
85
|
+
| `--port <1-65535>` | Listen port (default 17645) |
|
|
86
|
+
| `--ws-diagnostics` | Log sanitized request envelopes and WebSocket head decisions |
|
|
87
|
+
| `--help`, `--version` | Help / version |
|
|
88
|
+
|
|
89
|
+
Endpoint mode only (an error if combined with `--proxy`):
|
|
90
|
+
|
|
91
|
+
| Flag | Effect |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `--quick`, `--saved` | Start immediately from saved/default settings, skipping the wizard |
|
|
94
|
+
| `--listen local\|network` | One-run listen mode override |
|
|
95
|
+
| `--providers all\|favorites\|id1,id2` | One-run provider catalog override |
|
|
96
|
+
| `--mask-gateway-ids` / `--no-mask-gateway-ids` | Mask or expose vendor names in discovery model ids (see below) |
|
|
97
|
+
| `--password <value>` | One-run network-mode server password |
|
|
98
|
+
|
|
99
|
+
Proxy mode has no extra options — it takes only the common options.
|
|
100
|
+
|
|
101
|
+
Bare `clodex server` uses the saved default mode (proxy if none saved). Proxy mode starts immediately. Endpoint mode on a TTY opens a short wizard — start from saved settings, or configure: favorites-only catalog?, which providers to expose, discovery-id masking, and listen local/network (network asks for a password). Without a TTY (or with `--quick`/any endpoint-mode option) it skips all prompts and starts from saved settings; network mode then needs a saved password or `--password`.
|
|
102
|
+
|
|
103
|
+
**`--mask-gateway-ids`:** endpoint-mode discovery ids look like `anthropic-openai-oauth__gpt-5.6`. Some Claude clients validate model names (Claude Desktop / Cowork pickers, Claude Code skill/agent `model:` frontmatter) and reject or filter ids containing non-Anthropic vendor names. Masking reverses the provider and model segments (`anthropic-htuao-ianepo__6.5-tpg`) so vendor strings never appear literally; display names stay readable (`GPT 5.6 (OpenAI)`), and the gateway accepts both masked and unmasked ids in chat requests. Tradeoff: masked ids are unreadable — copy them exactly from the printed catalog. Masking is on by default; use `--no-mask-gateway-ids` for clients that don't need it.
|
|
104
|
+
|
|
105
|
+
Endpoint-mode endpoints (default port 17645):
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
|
|
109
|
+
OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use any API key locally; network mode requires the server password. Proxy mode prints `HTTPS_PROXY`, `HTTP_PROXY`, and `NODE_EXTRA_CA_CERTS` values to export — do **not** set `ANTHROPIC_BASE_URL` in that mode.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Endpoint gateway serving only your favorites, no prompts, for a local client
|
|
118
|
+
clodex server --endpoint --quick --providers favorites
|
|
119
|
+
|
|
120
|
+
# Proxy mode for an existing-auth Claude Code (export the env it prints)
|
|
121
|
+
clodex server --proxy
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `clodex patch [--restore]`
|
|
125
|
+
|
|
126
|
+
Patch the installed Claude Code binary so clodex favorites and aliases are first-class: accepted by the Agent tool's model field, listed in `/model`, resolved to their real ids, and reporting the correct context window.
|
|
127
|
+
|
|
128
|
+
| Flag | Effect |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `--restore` | Restore the pristine (unpatched) Claude Code binary |
|
|
131
|
+
| `--trace` | Show the underlying tweakcc output |
|
|
132
|
+
| `--help` | Help |
|
|
133
|
+
|
|
134
|
+
The patch map is built from your favorites and aliases; context windows come from provider metadata. A pristine per-version backup is kept, and a manifest (`~/.clodex/patch-state.json`) makes re-runs no-ops until your config or Claude Code version changes — then the binary is restored first and re-patched fresh. `clodex claude` checks patch freshness at launch and offers to re-patch (a non-blocking notice when not interactive). Re-run `clodex patch` after every `claude` update.
|
|
135
|
+
|
|
136
|
+
### `clodex models` / `clodex favorites`
|
|
137
|
+
|
|
138
|
+
Manage favorite models (max 20) and short aliases. Favorites feed the endpoint-mode `/model` switch menu, proxy-mode routing, and the patcher. Saved to `~/.clodex/config.json`.
|
|
139
|
+
|
|
140
|
+
| Flag | Effect |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| *(none)* | Interactive manager: search all providers or browse one at a time |
|
|
143
|
+
| `--list` | Print the exact `clodex:<provider-id>:<model-id>` names (and aliases) without opening the manager |
|
|
144
|
+
| `--alias <name=target>` | Save a short name for a favorite, e.g. `--alias sol=clodex:openai-oauth:gpt-5.6-sol` (the `clodex:` prefix is optional in the target) |
|
|
145
|
+
| `--unalias <name>` | Remove a saved short name |
|
|
146
|
+
| `--help`, `--version` | Help / version |
|
|
147
|
+
|
|
148
|
+
### `clodex providers [subcommand]`
|
|
149
|
+
|
|
150
|
+
| Subcommand | Effect |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| *(none)* | Provider hub wizard |
|
|
153
|
+
| `add` | Add OpenAI with an API key (choose OAuth or API key) |
|
|
154
|
+
| `auth openai` | Sign in with ChatGPT/Codex-plan OAuth (device code) |
|
|
155
|
+
| `list` | Show configured providers |
|
|
156
|
+
| `remove <id>` | Remove a provider by id |
|
|
157
|
+
| `refresh-models [id]` | Update cached model lists |
|
|
158
|
+
|
|
159
|
+
Providers supported: `openai` (API key, platform.openai.com) and `openai-oauth` (ChatGPT/Codex plan).
|
|
160
|
+
|
|
161
|
+
### Root
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
clodex --help # overview of all commands
|
|
165
|
+
clodex --version # version
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
- Config home: `~/.clodex` (override with `CLODEX_HOME`). On first run, config is migrated automatically from a legacy `~/.relay-ai` directory if present; the legacy directory is never modified.
|
|
171
|
+
- Credentials live in the OS credential store (Keychain / Windows Credential Manager / Secret Service) under the `clodex` service.
|
|
172
|
+
- `CLODEX_CLAUDE_PATH` overrides Claude Code binary discovery.
|
|
173
|
+
- **Outbound proxy:** when `HTTP_PROXY`/`HTTPS_PROXY` (and optionally `NO_PROXY`) are set in clodex's environment, all clodex-originated network calls honor them — OAuth sign-in and token refresh, model-list and models.dev refreshes, upstream OpenAI API calls, and the ChatGPT/Codex OAuth WebSocket transport (tunneled via HTTP CONNECT).
|
|
174
|
+
|
|
175
|
+
## Known limitations
|
|
176
|
+
|
|
177
|
+
- Cost display inside Claude Code is inaccurate for OpenAI models (Claude Code applies its own pricing table).
|
|
178
|
+
- In the endpoint-mode switch menu, the displayed context window reflects the launch model and does not update on live `/model` switches (Claude Code fetches window metadata once at startup). Proxy mode with `clodex patch` reports correct per-model windows.
|
|
179
|
+
- ChatGPT/Codex OAuth requires `store:false` upstream; some OpenAI cache controls are intentionally omitted on OAuth routes because they returned empty responses during compatibility testing.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server-runtime.ts
|
|
4
|
+
import { mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, join as join2 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/paths.ts
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { cpSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
11
|
+
var APP_DIR_NAME = "clodex";
|
|
12
|
+
var LEGACY_APP_DIR_NAME = "relay-ai";
|
|
13
|
+
function userHome(env = process.env) {
|
|
14
|
+
return env.HOME ?? env.USERPROFILE ?? homedir();
|
|
15
|
+
}
|
|
16
|
+
function resolveAppHomeOverride(env = process.env) {
|
|
17
|
+
const override = env.CLODEX_HOME;
|
|
18
|
+
return override?.trim() || void 0;
|
|
19
|
+
}
|
|
20
|
+
function getAppHome(env = process.env) {
|
|
21
|
+
const override = resolveAppHomeOverride(env);
|
|
22
|
+
if (override) return override;
|
|
23
|
+
return join(userHome(env), `.${APP_DIR_NAME}`);
|
|
24
|
+
}
|
|
25
|
+
function getLegacyAppHome(env = process.env) {
|
|
26
|
+
return join(userHome(env), `.${LEGACY_APP_DIR_NAME}`);
|
|
27
|
+
}
|
|
28
|
+
var legacyMigrationDone = false;
|
|
29
|
+
function ensureLegacyAppHomeMigrated(env = process.env) {
|
|
30
|
+
if (legacyMigrationDone) return;
|
|
31
|
+
legacyMigrationDone = true;
|
|
32
|
+
try {
|
|
33
|
+
const appHome = getAppHome(env);
|
|
34
|
+
if (existsSync(appHome)) return;
|
|
35
|
+
const legacyHome = getLegacyAppHome(env);
|
|
36
|
+
if (!existsSync(legacyHome)) return;
|
|
37
|
+
mkdirSync(appHome, { recursive: true, mode: 448 });
|
|
38
|
+
for (const entry of readdirSync(legacyHome)) {
|
|
39
|
+
if (entry === "logs") continue;
|
|
40
|
+
cpSync(join(legacyHome, entry), join(appHome, entry), { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getConfigPath(env = process.env) {
|
|
46
|
+
return join(getAppHome(env), "config.json");
|
|
47
|
+
}
|
|
48
|
+
function getProvidersPath(env = process.env) {
|
|
49
|
+
return join(getAppHome(env), "providers.json");
|
|
50
|
+
}
|
|
51
|
+
function getLogsPath(env = process.env) {
|
|
52
|
+
return join(getAppHome(env), "logs");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/server-runtime.ts
|
|
56
|
+
function getServerRuntimePath(env = process.env) {
|
|
57
|
+
return join2(getAppHome(env), "server-runtime.json");
|
|
58
|
+
}
|
|
59
|
+
function isPort(value) {
|
|
60
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
61
|
+
}
|
|
62
|
+
function parseServerRuntimeState(raw) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(raw);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
70
|
+
const record = parsed;
|
|
71
|
+
const mode = record["mode"];
|
|
72
|
+
if (mode !== "endpoint" && mode !== "proxy") return null;
|
|
73
|
+
if (!isPort(record["port"])) return null;
|
|
74
|
+
const pid = record["pid"];
|
|
75
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return null;
|
|
76
|
+
const startedAt = typeof record["startedAt"] === "string" ? record["startedAt"] : "";
|
|
77
|
+
const caPath = record["caPath"];
|
|
78
|
+
if (mode === "proxy") {
|
|
79
|
+
if (typeof caPath !== "string" || !caPath.trim()) return null;
|
|
80
|
+
return { mode, port: record["port"], pid, caPath, startedAt };
|
|
81
|
+
}
|
|
82
|
+
return { mode, port: record["port"], pid, startedAt };
|
|
83
|
+
}
|
|
84
|
+
function isPidAlive(pid, kill = process.kill.bind(process)) {
|
|
85
|
+
try {
|
|
86
|
+
kill(pid, 0);
|
|
87
|
+
return true;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return err?.code === "EPERM";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function writeServerRuntimeState(state, env = process.env) {
|
|
93
|
+
try {
|
|
94
|
+
const path = getServerRuntimePath(env);
|
|
95
|
+
mkdirSync2(dirname(path), { recursive: true, mode: 448 });
|
|
96
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}
|
|
97
|
+
`, { encoding: "utf8", mode: 384 });
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function removeServerRuntimeState(env = process.env) {
|
|
102
|
+
try {
|
|
103
|
+
rmSync(getServerRuntimePath(env), { force: true });
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function readLiveServerRuntimeState(env = process.env, options = {}) {
|
|
108
|
+
let raw;
|
|
109
|
+
try {
|
|
110
|
+
raw = readFileSync(getServerRuntimePath(env), "utf8");
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const state = parseServerRuntimeState(raw);
|
|
115
|
+
if (!state) return null;
|
|
116
|
+
const alive = options.isAlive ?? isPidAlive;
|
|
117
|
+
return alive(state.pid) ? state : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/config.ts
|
|
121
|
+
import { dirname as dirname2 } from "path";
|
|
122
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
123
|
+
function readJsonFile(path) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
126
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function readConfig() {
|
|
132
|
+
ensureLegacyAppHomeMigrated();
|
|
133
|
+
return readJsonFile(getConfigPath()) ?? {};
|
|
134
|
+
}
|
|
135
|
+
function writeConfig(config) {
|
|
136
|
+
const configPath = getConfigPath();
|
|
137
|
+
mkdirSync3(dirname2(configPath), { recursive: true, mode: 448 });
|
|
138
|
+
writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
|
|
139
|
+
`, { encoding: "utf8", mode: 384 });
|
|
140
|
+
}
|
|
141
|
+
function loadPreferences() {
|
|
142
|
+
const config = readConfig();
|
|
143
|
+
return {
|
|
144
|
+
lastModel: config.lastModel,
|
|
145
|
+
lastProvider: config.lastProvider,
|
|
146
|
+
recentModelsByProvider: config.recentModelsByProvider,
|
|
147
|
+
favoriteModels: config.favoriteModels,
|
|
148
|
+
modelAliases: config.modelAliases,
|
|
149
|
+
claudeBridgeMode: config.claudeBridgeMode,
|
|
150
|
+
serverBridgeMode: config.serverBridgeMode,
|
|
151
|
+
appPathOverrides: config.appPathOverrides,
|
|
152
|
+
recentLaunchFolders: config.recentLaunchFolders,
|
|
153
|
+
server: config.server
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function savePreferences(prefs) {
|
|
157
|
+
const config = readConfig();
|
|
158
|
+
if (prefs.lastModel !== void 0) config.lastModel = prefs.lastModel;
|
|
159
|
+
if (prefs.lastProvider !== void 0) config.lastProvider = prefs.lastProvider;
|
|
160
|
+
if (prefs.recentModelsByProvider !== void 0) config.recentModelsByProvider = prefs.recentModelsByProvider;
|
|
161
|
+
if (prefs.favoriteModels !== void 0) config.favoriteModels = prefs.favoriteModels;
|
|
162
|
+
if (prefs.modelAliases !== void 0) config.modelAliases = prefs.modelAliases;
|
|
163
|
+
if (prefs.claudeBridgeMode !== void 0) config.claudeBridgeMode = prefs.claudeBridgeMode;
|
|
164
|
+
if (prefs.serverBridgeMode !== void 0) config.serverBridgeMode = prefs.serverBridgeMode;
|
|
165
|
+
if (prefs.appPathOverrides !== void 0) config.appPathOverrides = prefs.appPathOverrides;
|
|
166
|
+
if (prefs.recentLaunchFolders !== void 0) config.recentLaunchFolders = prefs.recentLaunchFolders;
|
|
167
|
+
writeConfig(config);
|
|
168
|
+
}
|
|
169
|
+
function getAppPathOverride(appId) {
|
|
170
|
+
const value = loadPreferences().appPathOverrides?.[appId];
|
|
171
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
172
|
+
}
|
|
173
|
+
function resolveBridgeMode(command, explicit, opts = {}) {
|
|
174
|
+
const key = command === "claude" ? "claudeBridgeMode" : "serverBridgeMode";
|
|
175
|
+
if (explicit) {
|
|
176
|
+
if (opts.persist === true) savePreferences({ [key]: explicit });
|
|
177
|
+
return explicit;
|
|
178
|
+
}
|
|
179
|
+
return loadPreferences()[key] ?? "proxy";
|
|
180
|
+
}
|
|
181
|
+
var MAX_RECENT_MODELS = 3;
|
|
182
|
+
function recordLaunchSelection(_agent, providerId, modelId, prefs) {
|
|
183
|
+
const prevRecent = prefs.recentModelsByProvider?.[providerId] ?? [];
|
|
184
|
+
const updatedRecent = [modelId, ...prevRecent.filter((id) => id !== modelId)].slice(0, MAX_RECENT_MODELS);
|
|
185
|
+
savePreferences({
|
|
186
|
+
lastProvider: providerId,
|
|
187
|
+
lastModel: modelId,
|
|
188
|
+
recentModelsByProvider: { ...prefs.recentModelsByProvider, [providerId]: updatedRecent }
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
var SERVER_PASSWORD_SERVICE = "clodex-server-password";
|
|
192
|
+
var SERVER_PASSWORD_ACCOUNT = "server-password";
|
|
193
|
+
async function getServerPasswordKeyring() {
|
|
194
|
+
try {
|
|
195
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
196
|
+
return new Entry(SERVER_PASSWORD_SERVICE, SERVER_PASSWORD_ACCOUNT);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function getSavedServerPassword() {
|
|
202
|
+
const config = readConfig();
|
|
203
|
+
if (config.server?.savedPassword) {
|
|
204
|
+
const pwd = config.server.savedPassword;
|
|
205
|
+
const keyring2 = await getServerPasswordKeyring();
|
|
206
|
+
if (keyring2) {
|
|
207
|
+
try {
|
|
208
|
+
await keyring2.setPassword(pwd);
|
|
209
|
+
delete config.server.savedPassword;
|
|
210
|
+
if (Object.keys(config.server).length === 0) delete config.server;
|
|
211
|
+
writeConfig(config);
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return pwd;
|
|
216
|
+
}
|
|
217
|
+
const keyring = await getServerPasswordKeyring();
|
|
218
|
+
if (keyring) {
|
|
219
|
+
try {
|
|
220
|
+
return await keyring.getPassword();
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
async function setSavedServerPassword(password) {
|
|
228
|
+
const keyring = await getServerPasswordKeyring();
|
|
229
|
+
if (keyring) {
|
|
230
|
+
try {
|
|
231
|
+
await keyring.setPassword(password);
|
|
232
|
+
return;
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const config = readConfig();
|
|
237
|
+
config.server = {
|
|
238
|
+
...config.server ?? {},
|
|
239
|
+
savedPassword: password
|
|
240
|
+
};
|
|
241
|
+
writeConfig(config);
|
|
242
|
+
}
|
|
243
|
+
function getServerExposedProviders() {
|
|
244
|
+
const list = readConfig().server?.exposedProviders;
|
|
245
|
+
return list && list.length > 0 ? list : null;
|
|
246
|
+
}
|
|
247
|
+
function setServerExposedProviders(providerIds) {
|
|
248
|
+
const config = readConfig();
|
|
249
|
+
config.server = {
|
|
250
|
+
...config.server ?? {},
|
|
251
|
+
exposedProviders: providerIds
|
|
252
|
+
};
|
|
253
|
+
writeConfig(config);
|
|
254
|
+
}
|
|
255
|
+
function getServerMaskGatewayIds() {
|
|
256
|
+
return readConfig().server?.maskGatewayIds ?? true;
|
|
257
|
+
}
|
|
258
|
+
function setServerMaskGatewayIds(mask) {
|
|
259
|
+
const config = readConfig();
|
|
260
|
+
config.server = {
|
|
261
|
+
...config.server ?? {},
|
|
262
|
+
maskGatewayIds: mask
|
|
263
|
+
};
|
|
264
|
+
writeConfig(config);
|
|
265
|
+
}
|
|
266
|
+
function getServerFavoritesOnly() {
|
|
267
|
+
return readConfig().server?.favoritesOnly ?? false;
|
|
268
|
+
}
|
|
269
|
+
function setServerFavoritesOnly(favoritesOnly) {
|
|
270
|
+
const config = readConfig();
|
|
271
|
+
config.server = {
|
|
272
|
+
...config.server ?? {},
|
|
273
|
+
favoritesOnly
|
|
274
|
+
};
|
|
275
|
+
writeConfig(config);
|
|
276
|
+
}
|
|
277
|
+
function getServerListenMode() {
|
|
278
|
+
return readConfig().server?.listenMode === "network" ? "network" : "local";
|
|
279
|
+
}
|
|
280
|
+
function setServerListenMode(listenMode) {
|
|
281
|
+
const config = readConfig();
|
|
282
|
+
config.server = {
|
|
283
|
+
...config.server ?? {},
|
|
284
|
+
listenMode
|
|
285
|
+
};
|
|
286
|
+
writeConfig(config);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/launch.ts
|
|
290
|
+
import { execSync, spawn } from "child_process";
|
|
291
|
+
import { existsSync as existsSync3, appendFileSync } from "fs";
|
|
292
|
+
import { homedir as homedir2 } from "os";
|
|
293
|
+
import { join as join3 } from "path";
|
|
294
|
+
|
|
295
|
+
// src/binary-lookup.ts
|
|
296
|
+
import { execFileSync } from "child_process";
|
|
297
|
+
import { existsSync as existsSync2 } from "fs";
|
|
298
|
+
function findBinaryOnPath(name, fallbackPaths, options = {}) {
|
|
299
|
+
const isWindows2 = options.isWindows ?? process.platform === "win32";
|
|
300
|
+
const exists = options.exists ?? existsSync2;
|
|
301
|
+
const runWhich = options.runWhich ?? ((binary, win) => execFileSync(win ? "where.exe" : "which", [binary], {
|
|
302
|
+
encoding: "utf8",
|
|
303
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
304
|
+
}));
|
|
305
|
+
try {
|
|
306
|
+
const lines = runWhich(name, isWindows2).trim().split("\n").map((line) => line.trim()).filter(Boolean);
|
|
307
|
+
const path = (isWindows2 ? lines.find((line) => line.toLowerCase().endsWith(".cmd")) : null) ?? lines[0];
|
|
308
|
+
if (path && (!options.verifyWhichResult || exists(path))) return path;
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
for (const path of fallbackPaths) {
|
|
312
|
+
if (exists(path)) return path;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/launch.ts
|
|
318
|
+
var isWindows = process.platform === "win32";
|
|
319
|
+
var FALLBACK_PATHS = isWindows ? [
|
|
320
|
+
join3(process.env["APPDATA"] ?? homedir2(), "npm", "claude.cmd"),
|
|
321
|
+
join3(process.env["APPDATA"] ?? homedir2(), "npm", "claude"),
|
|
322
|
+
join3(homedir2(), "AppData", "Roaming", "npm", "claude.cmd")
|
|
323
|
+
] : [
|
|
324
|
+
join3(homedir2(), ".local", "bin", "claude"),
|
|
325
|
+
join3(homedir2(), ".npm", "bin", "claude"),
|
|
326
|
+
"/usr/local/bin/claude",
|
|
327
|
+
"/opt/homebrew/bin/claude"
|
|
328
|
+
];
|
|
329
|
+
function findClaudeBinary() {
|
|
330
|
+
const environmentOverride = process.env["CLODEX_CLAUDE_PATH"];
|
|
331
|
+
if (environmentOverride?.trim()) {
|
|
332
|
+
return existsSync3(environmentOverride) ? environmentOverride : null;
|
|
333
|
+
}
|
|
334
|
+
const override = getAppPathOverride("claude");
|
|
335
|
+
if (override) return existsSync3(override) ? override : null;
|
|
336
|
+
return findBinaryOnPath("claude", FALLBACK_PATHS);
|
|
337
|
+
}
|
|
338
|
+
function getInstalledClaudeVersion() {
|
|
339
|
+
try {
|
|
340
|
+
const claudePath = findClaudeBinary();
|
|
341
|
+
if (!claudePath) return "2.1.183";
|
|
342
|
+
const result = execSync(`${isWindows ? `"${claudePath}"` : claudePath} --version`, {
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
345
|
+
});
|
|
346
|
+
const match = result.match(/(\d+\.\d+\.\d+)/);
|
|
347
|
+
if (match) return match[1];
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
return "2.1.183";
|
|
351
|
+
}
|
|
352
|
+
function buildClaudeArgs(model, extraArgs) {
|
|
353
|
+
return model ? ["--model", model, ...extraArgs] : [...extraArgs];
|
|
354
|
+
}
|
|
355
|
+
function launchClaude(env, model, extraArgs) {
|
|
356
|
+
return new Promise((resolve) => {
|
|
357
|
+
const claudePath = findClaudeBinary();
|
|
358
|
+
const args = buildClaudeArgs(model, extraArgs);
|
|
359
|
+
const debugFileIdx = extraArgs.indexOf("--debug-file");
|
|
360
|
+
const debugLogPath = debugFileIdx !== -1 && extraArgs[debugFileIdx + 1] ? extraArgs[debugFileIdx + 1] : void 0;
|
|
361
|
+
const originalStdoutWrite = process.stdout.write;
|
|
362
|
+
const originalStderrWrite = process.stderr.write;
|
|
363
|
+
const muteWrite = (chunk, encoding, callback) => {
|
|
364
|
+
if (typeof encoding === "function") {
|
|
365
|
+
callback = encoding;
|
|
366
|
+
}
|
|
367
|
+
if (debugLogPath) {
|
|
368
|
+
try {
|
|
369
|
+
const str = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
370
|
+
appendFileSync(debugLogPath, `[parent] ${str}`);
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (callback) callback();
|
|
375
|
+
return true;
|
|
376
|
+
};
|
|
377
|
+
process.stdout.write = muteWrite;
|
|
378
|
+
process.stderr.write = muteWrite;
|
|
379
|
+
const restore = () => {
|
|
380
|
+
process.stdout.write = originalStdoutWrite;
|
|
381
|
+
process.stderr.write = originalStderrWrite;
|
|
382
|
+
};
|
|
383
|
+
const child = spawn(claudePath, args, {
|
|
384
|
+
stdio: "inherit",
|
|
385
|
+
env,
|
|
386
|
+
shell: isWindows
|
|
387
|
+
});
|
|
388
|
+
const forward = (signal) => {
|
|
389
|
+
child.kill(signal);
|
|
390
|
+
};
|
|
391
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
392
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
393
|
+
child.on("exit", (code) => {
|
|
394
|
+
restore();
|
|
395
|
+
resolve(code ?? 0);
|
|
396
|
+
});
|
|
397
|
+
child.on("error", (err) => {
|
|
398
|
+
restore();
|
|
399
|
+
resolve(1);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export {
|
|
405
|
+
getAppHome,
|
|
406
|
+
ensureLegacyAppHomeMigrated,
|
|
407
|
+
getProvidersPath,
|
|
408
|
+
getLogsPath,
|
|
409
|
+
writeServerRuntimeState,
|
|
410
|
+
removeServerRuntimeState,
|
|
411
|
+
readLiveServerRuntimeState,
|
|
412
|
+
loadPreferences,
|
|
413
|
+
savePreferences,
|
|
414
|
+
resolveBridgeMode,
|
|
415
|
+
recordLaunchSelection,
|
|
416
|
+
getSavedServerPassword,
|
|
417
|
+
setSavedServerPassword,
|
|
418
|
+
getServerExposedProviders,
|
|
419
|
+
setServerExposedProviders,
|
|
420
|
+
getServerMaskGatewayIds,
|
|
421
|
+
setServerMaskGatewayIds,
|
|
422
|
+
getServerFavoritesOnly,
|
|
423
|
+
setServerFavoritesOnly,
|
|
424
|
+
getServerListenMode,
|
|
425
|
+
setServerListenMode,
|
|
426
|
+
findClaudeBinary,
|
|
427
|
+
getInstalledClaudeVersion,
|
|
428
|
+
launchClaude
|
|
429
|
+
};
|
|
430
|
+
//# sourceMappingURL=chunk-C7N2JI7Z.js.map
|