theclawbay 0.2.13 → 0.3.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
@@ -60,20 +60,29 @@ After setup, restart terminal/VS Code once only if your own shell workflows depe
60
60
  - Full ChatGPT account model-picker behavior is tied to ChatGPT auth mode.
61
61
  - `theclawbay setup` keeps your existing Codex login state unchanged on purpose (so local history context is preserved and setup stays non-destructive).
62
62
 
63
- ## Run Relay (Optional)
63
+ ## Link Only (Optional)
64
64
 
65
- Only needed as a fallback compatibility mode:
65
+ If you only want to save backend/API-key state without applying local client config yet:
66
66
 
67
67
  ```sh
68
68
  theclawbay link --api-key <apiKey>
69
- theclawbay proxy
70
69
  ```
71
70
 
72
- By default this starts a local relay on `http://127.0.0.1:2455` and forwards to:
71
+ ## Logout
73
72
 
74
- - `https://theclawbay.com/api/codex-auth/v1/proxy/...`
73
+ To remove local The Claw Bay API-key auth state from this machine:
75
74
 
76
- The command auto-detects whether `openclaw` and/or `opencode` are installed and configures them automatically.
75
+ ```sh
76
+ theclawbay logout
77
+ ```
78
+
79
+ ## Release Notes (Maintainers)
80
+
81
+ If a previous publish failed after version bump, publish the current local version without bumping again:
82
+
83
+ ```sh
84
+ npm run release:publish
85
+ ```
77
86
 
78
87
  ## Notes
79
88
 
@@ -20,7 +20,7 @@ class LinkCommand extends base_command_1.BaseCommand {
20
20
  });
21
21
  this.log(`Linked. Managed config written to ${paths_1.managedConfigPath}`);
22
22
  this.log(`Backend: ${backendUrl}`);
23
- this.log(`Run "theclawbay proxy" to start the local relay.`);
23
+ this.log(`Run "theclawbay setup" to configure Codex/OpenClaw/OpenCode on this machine.`);
24
24
  });
25
25
  }
26
26
  }
@@ -0,0 +1,5 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class LogoutCommand extends BaseCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const promises_1 = __importDefault(require("node:fs/promises"));
7
+ const node_os_1 = __importDefault(require("node:os"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const base_command_1 = require("../lib/base-command");
10
+ const paths_1 = require("../lib/config/paths");
11
+ const DEFAULT_PROVIDER_ID = "theclawbay";
12
+ const LEGACY_PROVIDER_ID = "codex-lb";
13
+ const MANAGED_START = "# theclawbay-managed:start";
14
+ const MANAGED_END = "# theclawbay-managed:end";
15
+ const SHELL_START = "# theclawbay-shell-managed:start";
16
+ const SHELL_END = "# theclawbay-shell-managed:end";
17
+ const ENV_FILE = node_path_1.default.join(node_os_1.default.homedir(), ".config", "theclawbay", "env");
18
+ const ENV_KEY_NAME = "CODEX_LB_API_KEY";
19
+ function removeManagedBlock(source, start, end) {
20
+ const markerStart = source.indexOf(start);
21
+ if (markerStart < 0)
22
+ return source;
23
+ const markerEnd = source.indexOf(end, markerStart);
24
+ if (markerEnd < 0)
25
+ return `${source.slice(0, markerStart).trimEnd()}\n`;
26
+ return `${(source.slice(0, markerStart) + source.slice(markerEnd + end.length)).trimEnd()}\n`;
27
+ }
28
+ function removeProviderTable(source, providerId) {
29
+ const header = `[model_providers.${providerId}]`;
30
+ const lines = source.split(/\r?\n/);
31
+ const output = [];
32
+ for (let i = 0; i < lines.length; i++) {
33
+ if ((lines[i] ?? "").trim() !== header) {
34
+ output.push(lines[i] ?? "");
35
+ continue;
36
+ }
37
+ i++;
38
+ while (i < lines.length && !/^\s*\[[^\]]+\]\s*$/.test(lines[i] ?? ""))
39
+ i++;
40
+ i--;
41
+ }
42
+ return `${output.join("\n").trimEnd()}\n`;
43
+ }
44
+ function removeTopLevelProviderSelection(source) {
45
+ const lines = source.split(/\r?\n/);
46
+ const filtered = [];
47
+ for (let i = 0; i < lines.length; i++) {
48
+ const line = lines[i] ?? "";
49
+ if (/^\s*\[/.test(line)) {
50
+ filtered.push(...lines.slice(i));
51
+ break;
52
+ }
53
+ if (/^\s*#/.test(line) || !line.trim()) {
54
+ filtered.push(line);
55
+ continue;
56
+ }
57
+ const match = line.match(/^\s*model_provider\s*=\s*"([^"]+)"\s*$/);
58
+ if (!match) {
59
+ filtered.push(line);
60
+ continue;
61
+ }
62
+ if (match[1] === DEFAULT_PROVIDER_ID || match[1] === LEGACY_PROVIDER_ID)
63
+ continue;
64
+ filtered.push(line);
65
+ }
66
+ return `${filtered.join("\n").trimEnd()}\n`;
67
+ }
68
+ async function readFileIfExists(filePath) {
69
+ try {
70
+ return await promises_1.default.readFile(filePath, "utf8");
71
+ }
72
+ catch (error) {
73
+ const err = error;
74
+ if (err.code === "ENOENT")
75
+ return null;
76
+ throw error;
77
+ }
78
+ }
79
+ async function removeFileIfExists(filePath) {
80
+ try {
81
+ await promises_1.default.unlink(filePath);
82
+ return true;
83
+ }
84
+ catch (error) {
85
+ const err = error;
86
+ if (err.code === "ENOENT")
87
+ return false;
88
+ throw error;
89
+ }
90
+ }
91
+ async function writeIfChanged(filePath, next, previous) {
92
+ if (next === previous)
93
+ return false;
94
+ await promises_1.default.writeFile(filePath, next, "utf8");
95
+ return true;
96
+ }
97
+ function objectRecordOr(value, fallback) {
98
+ if (typeof value === "object" && value !== null && !Array.isArray(value))
99
+ return { ...value };
100
+ return fallback;
101
+ }
102
+ async function cleanupCodexConfig() {
103
+ const configPath = node_path_1.default.join(paths_1.codexDir, "config.toml");
104
+ const existing = await readFileIfExists(configPath);
105
+ if (existing === null)
106
+ return false;
107
+ let next = existing;
108
+ next = removeManagedBlock(next, MANAGED_START, MANAGED_END);
109
+ next = removeProviderTable(next, DEFAULT_PROVIDER_ID);
110
+ next = removeProviderTable(next, LEGACY_PROVIDER_ID);
111
+ next = removeTopLevelProviderSelection(next);
112
+ return writeIfChanged(configPath, next, existing);
113
+ }
114
+ async function cleanupShellFiles() {
115
+ const updated = [];
116
+ const shellRcPaths = [".bashrc", ".zshrc", ".profile"].map((name) => node_path_1.default.join(node_os_1.default.homedir(), name));
117
+ for (const rcPath of shellRcPaths) {
118
+ const existing = await readFileIfExists(rcPath);
119
+ if (existing === null)
120
+ continue;
121
+ const next = removeManagedBlock(existing, SHELL_START, SHELL_END);
122
+ if (await writeIfChanged(rcPath, next, existing))
123
+ updated.push(rcPath);
124
+ }
125
+ return updated;
126
+ }
127
+ async function cleanupVsCodeHooks() {
128
+ const updated = [];
129
+ const homes = [".vscode-server", ".vscode-server-insiders"];
130
+ for (const home of homes) {
131
+ const setupPath = node_path_1.default.join(node_os_1.default.homedir(), home, "server-env-setup");
132
+ const existing = await readFileIfExists(setupPath);
133
+ if (existing === null)
134
+ continue;
135
+ const next = removeManagedBlock(existing, SHELL_START, SHELL_END);
136
+ if (await writeIfChanged(setupPath, next, existing))
137
+ updated.push(setupPath);
138
+ }
139
+ return updated;
140
+ }
141
+ async function cleanupOpenClawConfig() {
142
+ const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".openclaw", "openclaw.json");
143
+ const existingRaw = await readFileIfExists(configPath);
144
+ if (existingRaw === null || !existingRaw.trim())
145
+ return false;
146
+ let doc;
147
+ try {
148
+ doc = objectRecordOr(JSON.parse(existingRaw), {});
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ let changed = false;
154
+ const modelsRoot = objectRecordOr(doc.models, {});
155
+ const providersRoot = objectRecordOr(modelsRoot.providers, {});
156
+ for (const id of [DEFAULT_PROVIDER_ID, LEGACY_PROVIDER_ID]) {
157
+ if (id in providersRoot) {
158
+ delete providersRoot[id];
159
+ changed = true;
160
+ }
161
+ }
162
+ modelsRoot.providers = providersRoot;
163
+ doc.models = modelsRoot;
164
+ const agentsRoot = objectRecordOr(doc.agents, {});
165
+ const defaultsRoot = objectRecordOr(agentsRoot.defaults, {});
166
+ const modelRoot = objectRecordOr(defaultsRoot.model, {});
167
+ const primary = modelRoot.primary;
168
+ if (typeof primary === "string" &&
169
+ (primary.startsWith(`${DEFAULT_PROVIDER_ID}/`) || primary.startsWith(`${LEGACY_PROVIDER_ID}/`))) {
170
+ delete modelRoot.primary;
171
+ changed = true;
172
+ }
173
+ defaultsRoot.model = modelRoot;
174
+ agentsRoot.defaults = defaultsRoot;
175
+ doc.agents = agentsRoot;
176
+ if (!changed)
177
+ return false;
178
+ await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
179
+ return true;
180
+ }
181
+ async function cleanupOpenCodeConfig() {
182
+ const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json");
183
+ const existingRaw = await readFileIfExists(configPath);
184
+ if (existingRaw === null || !existingRaw.trim())
185
+ return false;
186
+ let doc;
187
+ try {
188
+ doc = objectRecordOr(JSON.parse(existingRaw), {});
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ let changed = false;
194
+ const providerRoot = objectRecordOr(doc.provider, {});
195
+ for (const id of [DEFAULT_PROVIDER_ID, LEGACY_PROVIDER_ID]) {
196
+ if (id in providerRoot) {
197
+ delete providerRoot[id];
198
+ changed = true;
199
+ }
200
+ }
201
+ doc.provider = providerRoot;
202
+ const model = doc.model;
203
+ if (typeof model === "string" &&
204
+ (model.startsWith(`${DEFAULT_PROVIDER_ID}/`) || model.startsWith(`${LEGACY_PROVIDER_ID}/`))) {
205
+ delete doc.model;
206
+ changed = true;
207
+ }
208
+ if (!changed)
209
+ return false;
210
+ await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
211
+ return true;
212
+ }
213
+ class LogoutCommand extends base_command_1.BaseCommand {
214
+ async run() {
215
+ await this.runSafe(async () => {
216
+ const deletedManagedPaths = (await Promise.all([
217
+ removeFileIfExists(paths_1.managedConfigPath),
218
+ removeFileIfExists(paths_1.legacyManagedConfigPathClayBay),
219
+ removeFileIfExists(paths_1.legacyManagedConfigPathCodexAuth),
220
+ ])).filter(Boolean).length;
221
+ const removedEnvFile = await removeFileIfExists(ENV_FILE);
222
+ const updatedShellFiles = await cleanupShellFiles();
223
+ const updatedVsCodeHooks = await cleanupVsCodeHooks();
224
+ const updatedCodexConfig = await cleanupCodexConfig();
225
+ const updatedOpenClawConfig = await cleanupOpenClawConfig();
226
+ const updatedOpenCodeConfig = await cleanupOpenCodeConfig();
227
+ delete process.env[ENV_KEY_NAME];
228
+ this.log("Logout complete");
229
+ this.log(`- Managed configs removed: ${deletedManagedPaths}`);
230
+ this.log(`- API key env file removed: ${removedEnvFile ? "yes" : "no"}`);
231
+ this.log(`- Shell profiles updated: ${updatedShellFiles.length ? updatedShellFiles.join(", ") : "none"}`);
232
+ this.log(`- VS Code env hooks updated: ${updatedVsCodeHooks.length ? updatedVsCodeHooks.join(", ") : "none"}`);
233
+ this.log(`- Codex config cleaned: ${updatedCodexConfig ? "yes" : "no"}`);
234
+ this.log(`- OpenClaw config cleaned: ${updatedOpenClawConfig ? "yes" : "no"}`);
235
+ this.log(`- OpenCode config cleaned: ${updatedOpenCodeConfig ? "yes" : "no"}`);
236
+ this.log("Note: restart terminals/VS Code windows to clear already-loaded shell environment.");
237
+ });
238
+ }
239
+ }
240
+ LogoutCommand.description = "Log out from local The Claw Bay API-key auth on this machine";
241
+ exports.default = LogoutCommand;
package/dist/index.js CHANGED
@@ -1,7 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = __importDefault(require("node:path"));
4
9
  const core_1 = require("@oclif/core");
5
- void (0, core_1.run)()
6
- .then(() => (0, core_1.flush)())
7
- .catch(core_1.Errors.handle);
10
+ const update_check_1 = require("./lib/update-check");
11
+ function detectPackageVersion() {
12
+ try {
13
+ const packageJsonPath = node_path_1.default.join(__dirname, "..", "package.json");
14
+ const raw = (0, node_fs_1.readFileSync)(packageJsonPath, "utf8");
15
+ const parsed = JSON.parse(raw);
16
+ return typeof parsed.version === "string" ? parsed.version : "0.0.0";
17
+ }
18
+ catch {
19
+ return "0.0.0";
20
+ }
21
+ }
22
+ async function maybePrintBareInvocationHint() {
23
+ if (process.argv.slice(2).length > 0)
24
+ return;
25
+ const packageName = "theclawbay";
26
+ const currentVersion = detectPackageVersion();
27
+ await (0, update_check_1.maybeNotifyUpdate)({
28
+ packageName,
29
+ currentVersion,
30
+ log: (message) => console.log(message),
31
+ }).catch(() => { });
32
+ console.log("Get your API key at: https://theclawbay.com/keys");
33
+ console.log("Then run: theclawbay setup --api-key <apiKey>");
34
+ console.log("");
35
+ }
36
+ async function main() {
37
+ await maybePrintBareInvocationHint();
38
+ await (0, core_1.run)().then(() => (0, core_1.flush)());
39
+ }
40
+ void main().catch(core_1.Errors.handle);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.2.13",
4
- "description": "The Claw Bay CLI: one-time API-key setup for direct Codex access, with optional relay fallback.",
3
+ "version": "0.3.1",
4
+ "description": "The Claw Bay CLI: one-time API-key setup for direct Codex access.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "theclawbay": "dist/index.js"
@@ -14,6 +14,7 @@
14
14
  "release:patch": "./scripts/release-npm.sh patch",
15
15
  "release:minor": "./scripts/release-npm.sh minor",
16
16
  "release:major": "./scripts/release-npm.sh major",
17
+ "release:publish": "./scripts/release-npm.sh none",
17
18
  "release:dry-run": "./scripts/release-npm.sh patch --dry-run"
18
19
  },
19
20
  "engines": {
@@ -30,9 +31,7 @@
30
31
  "cli",
31
32
  "setup",
32
33
  "wan",
33
- "proxy",
34
- "api-key",
35
- "relay"
34
+ "api-key"
36
35
  ],
37
36
  "preferGlobal": true,
38
37
  "dependencies": {
@@ -1,13 +0,0 @@
1
- import { BaseCommand } from "../lib/base-command";
2
- export default class ProxyCommand extends BaseCommand {
3
- static description: string;
4
- static flags: {
5
- host: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
6
- port: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
7
- backend: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
- "api-key": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
- "base-path": import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
- client: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
- };
12
- run(): Promise<void>;
13
- }
@@ -1,306 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const node_http_1 = __importDefault(require("node:http"));
7
- const node_stream_1 = require("node:stream");
8
- const node_child_process_1 = require("node:child_process");
9
- const promises_1 = require("node:readline/promises");
10
- const core_1 = require("@oclif/core");
11
- const base_command_1 = require("../lib/base-command");
12
- const config_1 = require("../lib/managed/config");
13
- function trimTrailingSlash(value) {
14
- return value.replace(/\/+$/g, "");
15
- }
16
- function ensureLeadingSlash(value) {
17
- return value.startsWith("/") ? value : `/${value}`;
18
- }
19
- function joinUrl(base, pathname) {
20
- const url = new URL(base);
21
- url.pathname = `${url.pathname.replace(/\/+$/g, "")}${pathname.startsWith("/") ? "" : "/"}${pathname}`;
22
- return url.toString();
23
- }
24
- function normalizeUrl(raw, label) {
25
- try {
26
- const parsed = new URL(raw.trim());
27
- return trimTrailingSlash(parsed.toString());
28
- }
29
- catch {
30
- throw new Error(`invalid ${label} URL: ${raw}`);
31
- }
32
- }
33
- function headersFromIncoming(source) {
34
- const headers = new Headers();
35
- for (const [key, value] of Object.entries(source)) {
36
- if (!value)
37
- continue;
38
- const lower = key.toLowerCase();
39
- if (lower === "host" || lower === "content-length" || lower === "authorization" || lower === "connection") {
40
- continue;
41
- }
42
- if (Array.isArray(value)) {
43
- headers.set(key, value.join(", "));
44
- }
45
- else {
46
- headers.set(key, value);
47
- }
48
- }
49
- return headers;
50
- }
51
- async function readIncomingBody(req) {
52
- if (!req.method || req.method === "GET" || req.method === "HEAD")
53
- return null;
54
- const chunks = [];
55
- for await (const chunk of req) {
56
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
57
- }
58
- if (!chunks.length)
59
- return null;
60
- return Buffer.concat(chunks);
61
- }
62
- function bufferToArrayBuffer(buffer) {
63
- const bytes = new Uint8Array(buffer.length);
64
- bytes.set(buffer);
65
- return bytes.buffer;
66
- }
67
- function writeUpstreamHeaders(res, source) {
68
- source.forEach((value, key) => {
69
- const lower = key.toLowerCase();
70
- if (lower === "connection" || lower === "transfer-encoding" || lower === "keep-alive")
71
- return;
72
- res.setHeader(key, value);
73
- });
74
- }
75
- function hasCommand(name) {
76
- const result = (0, node_child_process_1.spawnSync)("which", [name], { stdio: "ignore" });
77
- return result.status === 0;
78
- }
79
- async function askClientChoice() {
80
- const rl = (0, promises_1.createInterface)({
81
- input: process.stdin,
82
- output: process.stdout,
83
- });
84
- try {
85
- const answer = (await rl.question("Which clients do you want to configure? [1] All detected [2] Codex [3] OpenClaw [4] OpenCode [5] Codex+OpenClaw: "))
86
- .trim()
87
- .toLowerCase();
88
- if (answer === "2" || answer === "codex")
89
- return "codex";
90
- if (answer === "3" || answer === "openclaw")
91
- return "openclaw";
92
- if (answer === "4" || answer === "opencode")
93
- return "opencode";
94
- if (answer === "5" || answer === "both")
95
- return "both";
96
- return "all";
97
- }
98
- finally {
99
- rl.close();
100
- }
101
- }
102
- function clientTargetsForChoice(choice) {
103
- if (choice === "codex")
104
- return { codex: true, openclaw: false, opencode: false };
105
- if (choice === "openclaw")
106
- return { codex: false, openclaw: true, opencode: false };
107
- if (choice === "opencode")
108
- return { codex: false, openclaw: false, opencode: true };
109
- if (choice === "both")
110
- return { codex: true, openclaw: true, opencode: false };
111
- return { codex: true, openclaw: true, opencode: true };
112
- }
113
- function detectedClientTargets() {
114
- return {
115
- codex: hasCommand("codex"),
116
- openclaw: hasCommand("openclaw"),
117
- opencode: hasCommand("opencode"),
118
- };
119
- }
120
- function countEnabledTargets(targets) {
121
- return Number(targets.codex) + Number(targets.openclaw) + Number(targets.opencode);
122
- }
123
- async function resolveClientChoice(mode) {
124
- if (mode === "all") {
125
- const detected = detectedClientTargets();
126
- return countEnabledTargets(detected) > 0 ? detected : { codex: true, openclaw: false, opencode: false };
127
- }
128
- if (mode === "codex" || mode === "openclaw" || mode === "opencode" || mode === "both") {
129
- return clientTargetsForChoice(mode);
130
- }
131
- const detected = detectedClientTargets();
132
- const enabledCount = countEnabledTargets(detected);
133
- if (enabledCount === 0)
134
- return { codex: true, openclaw: false, opencode: false };
135
- if (enabledCount === 1)
136
- return detected;
137
- if (process.stdin.isTTY && process.stdout.isTTY) {
138
- const choice = await askClientChoice();
139
- if (choice === "all")
140
- return detected;
141
- const requested = clientTargetsForChoice(choice);
142
- return {
143
- codex: requested.codex && detected.codex,
144
- openclaw: requested.openclaw && detected.openclaw,
145
- opencode: requested.opencode && detected.opencode,
146
- };
147
- }
148
- return detected;
149
- }
150
- class ProxyCommand extends base_command_1.BaseCommand {
151
- async run() {
152
- await this.runSafe(async () => {
153
- const { flags } = await this.parse(ProxyCommand);
154
- const managed = await (0, config_1.readManagedConfig)();
155
- if (!managed.apiKey?.trim()) {
156
- throw new Error('API key auth is required. Re-link using "theclawbay link --api-key <key>".');
157
- }
158
- const backendUrl = normalizeUrl(flags.backend ?? managed.backendUrl, "--backend");
159
- const apiKey = (flags["api-key"] ?? managed.apiKey ?? "").trim();
160
- if (!apiKey) {
161
- throw new Error("API key is required for WAN proxy mode.");
162
- }
163
- const basePath = ensureLeadingSlash(flags["base-path"].trim()).replace(/\/+$/g, "");
164
- const proxyBase = `${trimTrailingSlash(backendUrl)}${basePath}`;
165
- const targets = await resolveClientChoice(flags.client);
166
- const server = node_http_1.default.createServer(async (req, res) => {
167
- const method = req.method?.toUpperCase() ?? "GET";
168
- const incomingUrl = new URL(req.url || "/", "http://local-proxy.invalid");
169
- const routePath = incomingUrl.pathname.replace(/^\/+/, "");
170
- const upstreamUrl = joinUrl(proxyBase, routePath);
171
- const urlWithQuery = `${upstreamUrl}${incomingUrl.search || ""}`;
172
- try {
173
- const headers = headersFromIncoming(req.headers);
174
- headers.set("Authorization", `Bearer ${apiKey}`);
175
- const bodyBuffer = await readIncomingBody(req);
176
- const body = bodyBuffer ? bufferToArrayBuffer(bodyBuffer) : undefined;
177
- const upstream = await fetch(urlWithQuery, {
178
- method,
179
- headers,
180
- body,
181
- redirect: "manual",
182
- });
183
- res.statusCode = upstream.status;
184
- res.statusMessage = upstream.statusText;
185
- writeUpstreamHeaders(res, upstream.headers);
186
- if (!upstream.body || method === "HEAD") {
187
- res.end();
188
- return;
189
- }
190
- const stream = node_stream_1.Readable.fromWeb(upstream.body);
191
- stream.on("error", () => {
192
- res.destroy();
193
- });
194
- stream.pipe(res);
195
- }
196
- catch (error) {
197
- const message = error instanceof Error ? error.message : String(error);
198
- res.statusCode = 502;
199
- res.setHeader("Content-Type", "application/json");
200
- res.end(JSON.stringify({ error: `proxy relay failed: ${message}` }));
201
- }
202
- });
203
- await new Promise((resolve, reject) => {
204
- server.once("error", reject);
205
- server.listen(flags.port, flags.host, () => {
206
- server.off("error", reject);
207
- resolve();
208
- });
209
- });
210
- const localBase = `http://${flags.host}:${flags.port}`;
211
- this.log(`theclawbay WAN relay listening at ${localBase}`);
212
- this.log(`Forwarding to ${proxyBase}`);
213
- this.log("");
214
- if (targets.codex) {
215
- this.log("Codex CLI config snippet:");
216
- this.log('model_provider = "theclawbay-wan"');
217
- this.log("");
218
- this.log("[model_providers.theclawbay-wan]");
219
- this.log('name = "OpenAI"');
220
- this.log(`base_url = "${localBase}/backend-api/codex"`);
221
- this.log('wire_api = "responses"');
222
- this.log(`chatgpt_base_url = "${localBase}"`);
223
- this.log("requires_openai_auth = true");
224
- this.log("");
225
- }
226
- if (targets.openclaw) {
227
- const openClawProviderJson = JSON.stringify({
228
- baseUrl: `${localBase}/v1`,
229
- apiKey: "theclawbay-local",
230
- api: "openai-responses",
231
- models: [
232
- { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
233
- { id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
234
- { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
235
- { id: "gpt-5.1-codex", name: "GPT-5.1 Codex" },
236
- ],
237
- });
238
- this.log("OpenClaw setup:");
239
- this.log("Run these once:");
240
- this.log('openclaw config set agents.defaults.model.primary "theclawbay/gpt-5.3-codex"');
241
- this.log('openclaw config set models.mode "merge"');
242
- this.log(`openclaw config set models.providers.theclawbay '${openClawProviderJson}' --json`);
243
- this.log("");
244
- }
245
- if (targets.opencode) {
246
- this.log("OpenCode setup:");
247
- this.log("Set in ~/.config/opencode/opencode.json:");
248
- this.log(JSON.stringify({
249
- provider: {
250
- theclawbay: {
251
- npm: "@ai-sdk/openai-compatible",
252
- name: "theclawbay",
253
- options: {
254
- baseURL: `${localBase}/v1`,
255
- apiKey: "theclawbay-local",
256
- },
257
- models: {
258
- "gpt-5.3-codex": { name: "GPT-5.3 Codex" },
259
- },
260
- },
261
- },
262
- model: "theclawbay/gpt-5.3-codex",
263
- }, null, 2));
264
- this.log("");
265
- }
266
- await new Promise((resolve) => {
267
- const stop = () => {
268
- server.close(() => resolve());
269
- };
270
- process.once("SIGINT", stop);
271
- process.once("SIGTERM", stop);
272
- });
273
- });
274
- }
275
- }
276
- ProxyCommand.description = "Run a local relay that forwards Codex/OpenAI requests to your The Claw Bay backend using API-key auth";
277
- ProxyCommand.flags = {
278
- host: core_1.Flags.string({
279
- default: "127.0.0.1",
280
- description: "Local bind host",
281
- }),
282
- port: core_1.Flags.integer({
283
- default: 2455,
284
- description: "Local bind port",
285
- }),
286
- backend: core_1.Flags.string({
287
- required: false,
288
- description: "Override backend URL (defaults to linked managed backend)",
289
- }),
290
- "api-key": core_1.Flags.string({
291
- required: false,
292
- aliases: ["apiKey"],
293
- description: "Override API key (defaults to linked managed API key)",
294
- }),
295
- "base-path": core_1.Flags.string({
296
- default: "/api/codex-auth/v1/proxy",
297
- description: "Server proxy base path",
298
- }),
299
- client: core_1.Flags.string({
300
- required: false,
301
- default: "auto",
302
- options: ["auto", "codex", "openclaw", "opencode", "both", "all"],
303
- description: "Client target to guide setup for",
304
- }),
305
- };
306
- exports.default = ProxyCommand;