theclawbay 0.2.1 → 0.2.4

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
@@ -2,11 +2,6 @@
2
2
 
3
3
  `theclawbay` is a customer CLI for The Claw Bay.
4
4
 
5
- It now does one job:
6
-
7
- - link your purchased API key
8
- - run a local relay that forwards Codex/OpenAI requests to your The Claw Bay backend
9
-
10
5
  This package no longer runs local account login automation or local account switching flows.
11
6
 
12
7
  > Not affiliated with OpenAI or Codex. Not an official tool.
@@ -14,7 +9,7 @@ This package no longer runs local account login automation or local account swit
14
9
  ## Requirements
15
10
 
16
11
  - Node.js 18+
17
- - Codex CLI installed (`@openai/codex`)
12
+ - Codex CLI (`@openai/codex`) and/or OpenClaw CLI
18
13
 
19
14
  ## Install
20
15
 
@@ -22,25 +17,55 @@ This package no longer runs local account login automation or local account swit
22
17
  npm i -g theclawbay
23
18
  ```
24
19
 
25
- ## Link
20
+ If you use `nvm`, keep a single global prefix to avoid duplicate binaries:
21
+
22
+ ```sh
23
+ npm config delete prefix
24
+ ```
25
+
26
+ ## One-Time Setup (Recommended)
26
27
 
27
28
  Use your purchased API key from your dashboard:
28
29
 
29
30
  ```sh
30
- theclawbay link --api-key <apiKey>
31
+ theclawbay setup --api-key <apiKey>
32
+ ```
33
+
34
+ This auto-detects installed clients and writes direct WAN API-key config so users can run directly.
35
+ By default, Codex setup preserves local conversation history (no ChatGPT history sync mode).
36
+
37
+ Explicit client targeting:
38
+
39
+ ```sh
40
+ theclawbay setup --api-key <apiKey> --client codex
41
+ theclawbay setup --api-key <apiKey> --client openclaw
42
+ theclawbay setup --api-key <apiKey> --client both
31
43
  ```
32
44
 
33
- This defaults to `https://theclawbay.com`.
45
+ If needed, skip the automatic Codex login step:
46
+
47
+ ```sh
48
+ theclawbay setup --api-key <apiKey> --skip-login
49
+ ```
50
+
51
+ If you explicitly want the prior ChatGPT-linked history mode:
52
+
53
+ ```sh
54
+ theclawbay setup --api-key <apiKey> --openai-sync
55
+ ```
34
56
 
35
57
  If you operate a custom backend, pass it explicitly:
36
58
 
37
59
  ```sh
38
- theclawbay link --api-key <apiKey> --backend https://your-domain.com
60
+ theclawbay setup --api-key <apiKey> --backend https://your-domain.com
39
61
  ```
40
62
 
41
- ## Run Relay
63
+ ## Run Relay (Optional)
64
+
65
+ Only needed as a fallback compatibility mode:
42
66
 
43
67
  ```sh
68
+ theclawbay link --api-key <apiKey>
44
69
  theclawbay proxy
45
70
  ```
46
71
 
@@ -51,17 +76,6 @@ By default this starts a local relay on `http://127.0.0.1:2455` and forwards to:
51
76
  The command now auto-detects whether `codex`, `openclaw`, or both are installed and prints setup steps for the detected client(s).
52
77
  If both are installed and you're in an interactive terminal, it asks which one you want to configure.
53
78
 
54
- ## Common Flags
55
-
56
- `theclawbay proxy` supports:
57
-
58
- - `--host` local bind host (default `127.0.0.1`)
59
- - `--port` local bind port (default `2455`)
60
- - `--backend` override backend URL from linked config
61
- - `--api-key` override API key from linked config
62
- - `--base-path` override server proxy path (default `/api/codex-auth/v1/proxy`)
63
- - `--client` setup target: `auto` | `codex` | `openclaw` | `both` (default `auto`)
64
-
65
79
  ## Notes
66
80
 
67
81
  - End users only need API keys. They do not need upstream account credentials.
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class SetupCommand extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ backend: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
6
+ "api-key": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ provider: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ client: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ "openclaw-model": import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ "skip-login": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ "openai-sync": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,308 @@
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_path_1 = __importDefault(require("node:path"));
8
+ const node_child_process_1 = require("node:child_process");
9
+ const promises_2 = require("node:readline/promises");
10
+ const core_1 = require("@oclif/core");
11
+ const base_command_1 = require("../lib/base-command");
12
+ const paths_1 = require("../lib/config/paths");
13
+ const api_key_1 = require("../lib/managed/api-key");
14
+ const config_1 = require("../lib/managed/config");
15
+ const errors_1 = require("../lib/managed/errors");
16
+ const DEFAULT_BACKEND_URL = "https://theclawbay.com";
17
+ const DEFAULT_PROVIDER_ID = "theclawbay-wan";
18
+ const MANAGED_START = "# theclawbay-managed:start";
19
+ const MANAGED_END = "# theclawbay-managed:end";
20
+ const DEFAULT_OPENCLAW_MODEL = "gpt-5.3-codex";
21
+ function trimTrailingSlash(value) {
22
+ return value.replace(/\/+$/g, "");
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 hasCommand(name) {
34
+ const result = (0, node_child_process_1.spawnSync)("which", [name], { stdio: "ignore" });
35
+ return result.status === 0;
36
+ }
37
+ async function askClientChoice() {
38
+ const rl = (0, promises_2.createInterface)({
39
+ input: process.stdin,
40
+ output: process.stdout,
41
+ });
42
+ try {
43
+ const answer = (await rl.question("Which client do you want to configure? [1] Codex [2] OpenClaw [3] Both: "))
44
+ .trim()
45
+ .toLowerCase();
46
+ if (answer === "2" || answer === "openclaw")
47
+ return "openclaw";
48
+ if (answer === "3" || answer === "both")
49
+ return "both";
50
+ return "codex";
51
+ }
52
+ finally {
53
+ rl.close();
54
+ }
55
+ }
56
+ async function resolveClientChoice(mode) {
57
+ if (mode === "codex" || mode === "openclaw" || mode === "both") {
58
+ return mode;
59
+ }
60
+ const codexInstalled = hasCommand("codex");
61
+ const openclawInstalled = hasCommand("openclaw");
62
+ if (codexInstalled && !openclawInstalled)
63
+ return "codex";
64
+ if (!codexInstalled && openclawInstalled)
65
+ return "openclaw";
66
+ if (codexInstalled && openclawInstalled) {
67
+ if (process.stdin.isTTY && process.stdout.isTTY) {
68
+ return askClientChoice();
69
+ }
70
+ return "both";
71
+ }
72
+ return "codex";
73
+ }
74
+ function removeManagedBlock(source) {
75
+ const start = source.indexOf(MANAGED_START);
76
+ if (start < 0)
77
+ return source;
78
+ const end = source.indexOf(MANAGED_END, start);
79
+ if (end < 0)
80
+ return source.slice(0, start).trimEnd() + "\n";
81
+ return (source.slice(0, start) + source.slice(end + MANAGED_END.length)).trimEnd() + "\n";
82
+ }
83
+ function removeProviderTable(source, providerId) {
84
+ const header = `[model_providers.${providerId}]`;
85
+ const lines = source.split(/\r?\n/);
86
+ const output = [];
87
+ for (let i = 0; i < lines.length; i++) {
88
+ if (lines[i]?.trim() !== header) {
89
+ output.push(lines[i] ?? "");
90
+ continue;
91
+ }
92
+ i++;
93
+ while (i < lines.length && !/^\s*\[[^\]]+\]\s*$/.test(lines[i] ?? "")) {
94
+ i++;
95
+ }
96
+ i--;
97
+ }
98
+ return `${output.join("\n").trimEnd()}\n`;
99
+ }
100
+ function upsertFirstKeyLine(source, key, tomlValue) {
101
+ const lines = source.split(/\r?\n/);
102
+ for (let i = 0; i < lines.length; i++) {
103
+ const line = lines[i] ?? "";
104
+ if (/^\s*#/.test(line))
105
+ continue;
106
+ if (/^\s*\[/.test(line))
107
+ break;
108
+ if (new RegExp(`^\\s*${key}\\s*=`).test(line)) {
109
+ lines[i] = `${key} = ${tomlValue}`;
110
+ return `${lines.join("\n").trimEnd()}\n`;
111
+ }
112
+ }
113
+ return `${`${key} = ${tomlValue}\n${source}`.trimEnd()}\n`;
114
+ }
115
+ async function writeCodexConfig(params) {
116
+ const configPath = node_path_1.default.join(paths_1.codexDir, "config.toml");
117
+ await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
118
+ let existing = "";
119
+ try {
120
+ existing = await promises_1.default.readFile(configPath, "utf8");
121
+ }
122
+ catch (error) {
123
+ const err = error;
124
+ if (err.code !== "ENOENT")
125
+ throw error;
126
+ }
127
+ const proxyRoot = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy`;
128
+ let next = existing;
129
+ next = removeManagedBlock(next);
130
+ next = removeProviderTable(next, params.providerId);
131
+ next = upsertFirstKeyLine(next, "model_provider", `"${params.providerId}"`);
132
+ const managedBlock = [
133
+ MANAGED_START,
134
+ `[model_providers.${params.providerId}]`,
135
+ 'name = "OpenAI"',
136
+ `base_url = "${proxyRoot}/backend-api/codex"`,
137
+ 'wire_api = "responses"',
138
+ ...(params.openaiSync
139
+ ? [`chatgpt_base_url = "${proxyRoot}"`, "requires_openai_auth = true"]
140
+ : []),
141
+ MANAGED_END,
142
+ "",
143
+ ].join("\n");
144
+ if (next.trim().length) {
145
+ next = `${next.trimEnd()}\n\n`;
146
+ }
147
+ next += managedBlock;
148
+ await promises_1.default.writeFile(configPath, next, "utf8");
149
+ return configPath;
150
+ }
151
+ function codexLoginWithApiKey(apiKey) {
152
+ const run = (0, node_child_process_1.spawnSync)("codex", ["login", "--with-api-key"], {
153
+ input: `${apiKey}\n`,
154
+ encoding: "utf8",
155
+ stdio: ["pipe", "pipe", "pipe"],
156
+ });
157
+ if (run.status === 0)
158
+ return;
159
+ const stderr = (run.stderr ?? "").trim();
160
+ const stdout = (run.stdout ?? "").trim();
161
+ const details = stderr || stdout || "codex login returned a non-zero exit status";
162
+ throw new Error(`failed to store API key in Codex CLI: ${details}`);
163
+ }
164
+ function runOpenClawConfigCommand(args) {
165
+ const run = (0, node_child_process_1.spawnSync)("openclaw", args, {
166
+ encoding: "utf8",
167
+ stdio: ["ignore", "pipe", "pipe"],
168
+ });
169
+ if (run.status === 0)
170
+ return;
171
+ const stderr = (run.stderr ?? "").trim();
172
+ const stdout = (run.stdout ?? "").trim();
173
+ const details = stderr || stdout || "openclaw command returned a non-zero exit status";
174
+ throw new Error(`failed to update OpenClaw config: ${details}`);
175
+ }
176
+ function setupOpenClaw(params) {
177
+ const base = trimTrailingSlash(params.backendUrl);
178
+ const model = params.model.trim() || DEFAULT_OPENCLAW_MODEL;
179
+ const provider = {
180
+ baseUrl: `${base}/api/codex-auth/v1/proxy/v1`,
181
+ apiKey: params.apiKey,
182
+ api: "openai-responses",
183
+ models: [
184
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
185
+ { id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
186
+ { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
187
+ { id: "gpt-5.1-codex", name: "GPT-5.1 Codex" },
188
+ ],
189
+ };
190
+ runOpenClawConfigCommand(["config", "set", "agents.defaults.model.primary", `openai/${model}`]);
191
+ runOpenClawConfigCommand(["config", "set", "models.mode", "merge"]);
192
+ runOpenClawConfigCommand([
193
+ "config",
194
+ "set",
195
+ "models.providers.openai",
196
+ JSON.stringify(provider),
197
+ "--json",
198
+ ]);
199
+ }
200
+ class SetupCommand extends base_command_1.BaseCommand {
201
+ async run() {
202
+ await this.runSafe(async () => {
203
+ const { flags } = await this.parse(SetupCommand);
204
+ let managed = null;
205
+ try {
206
+ managed = await (0, config_1.readManagedConfig)();
207
+ }
208
+ catch (error) {
209
+ if (!(error instanceof errors_1.ManagedConfigMissingError))
210
+ throw error;
211
+ }
212
+ const apiKey = (flags["api-key"] ?? managed?.apiKey ?? "").trim();
213
+ if (!apiKey) {
214
+ throw new Error('API key is required. Run "theclawbay setup --api-key <key>".');
215
+ }
216
+ const backendRaw = flags.backend ?? (0, api_key_1.tryInferBackendUrlFromApiKey)(apiKey) ?? managed?.backendUrl ?? DEFAULT_BACKEND_URL;
217
+ const backendUrl = normalizeUrl(backendRaw, "--backend");
218
+ const clientChoice = await resolveClientChoice(flags.client);
219
+ const codexWanted = clientChoice === "codex" || clientChoice === "both";
220
+ const openClawWanted = clientChoice === "openclaw" || clientChoice === "both";
221
+ await (0, config_1.writeManagedConfig)({ backendUrl, apiKey });
222
+ let codexConfigPath = null;
223
+ if (codexWanted) {
224
+ codexConfigPath = await writeCodexConfig({
225
+ backendUrl,
226
+ providerId: flags.provider.trim() || DEFAULT_PROVIDER_ID,
227
+ openaiSync: flags["openai-sync"],
228
+ });
229
+ }
230
+ if (codexWanted && !flags["skip-login"]) {
231
+ if (!hasCommand("codex")) {
232
+ throw new Error('codex CLI not found. Install @openai/codex, or run setup with --client openclaw.');
233
+ }
234
+ codexLoginWithApiKey(apiKey);
235
+ }
236
+ if (openClawWanted) {
237
+ if (!hasCommand("openclaw")) {
238
+ throw new Error('openclaw CLI not found. Install OpenClaw, or run setup with --client codex.');
239
+ }
240
+ setupOpenClaw({
241
+ backendUrl,
242
+ apiKey,
243
+ model: flags["openclaw-model"],
244
+ });
245
+ }
246
+ this.log(`Linked. Managed config written to ${paths_1.managedConfigPath}`);
247
+ this.log(`Backend: ${backendUrl}`);
248
+ if (codexConfigPath) {
249
+ this.log(`Codex config updated at ${codexConfigPath}`);
250
+ if (flags["openai-sync"]) {
251
+ this.log("Codex configured in OpenAI-sync mode (ChatGPT-linked history view).");
252
+ }
253
+ else {
254
+ this.log("Codex configured in local-history mode (default).");
255
+ }
256
+ }
257
+ if (openClawWanted) {
258
+ this.log("OpenClaw model provider updated for direct WAN API-key routing.");
259
+ }
260
+ if (codexWanted && flags["skip-login"]) {
261
+ this.log("Skipped Codex login. Run: printenv OPENAI_API_KEY | codex login --with-api-key");
262
+ }
263
+ else if (codexWanted) {
264
+ this.log("Codex API-key login updated.");
265
+ }
266
+ this.log('Done. Users can run directly (no "theclawbay proxy" required in recommended setup).');
267
+ });
268
+ }
269
+ }
270
+ SetupCommand.description = "One-time direct setup for Codex/OpenClaw: link key and route directly to The Claw Bay backend";
271
+ SetupCommand.flags = {
272
+ backend: core_1.Flags.string({
273
+ required: false,
274
+ description: "Backend base URL override (default: https://theclawbay.com)",
275
+ }),
276
+ "api-key": core_1.Flags.string({
277
+ required: false,
278
+ aliases: ["apiKey"],
279
+ description: "API key issued by your The Claw Bay dashboard",
280
+ }),
281
+ provider: core_1.Flags.string({
282
+ required: false,
283
+ default: DEFAULT_PROVIDER_ID,
284
+ description: "Codex model provider id to write into ~/.codex/config.toml",
285
+ }),
286
+ client: core_1.Flags.string({
287
+ required: false,
288
+ default: "auto",
289
+ options: ["auto", "codex", "openclaw", "both"],
290
+ description: "Client target to configure",
291
+ }),
292
+ "openclaw-model": core_1.Flags.string({
293
+ required: false,
294
+ default: DEFAULT_OPENCLAW_MODEL,
295
+ description: "OpenClaw model id to set as default (without provider prefix)",
296
+ }),
297
+ "skip-login": core_1.Flags.boolean({
298
+ required: false,
299
+ default: false,
300
+ description: "Skip `codex login --with-api-key`",
301
+ }),
302
+ "openai-sync": core_1.Flags.boolean({
303
+ required: false,
304
+ default: false,
305
+ description: "Enable OpenAI-synced conversation mode (writes chatgpt_base_url/requires_openai_auth); may prefer remote ChatGPT history over local-only history",
306
+ }),
307
+ };
308
+ exports.default = SetupCommand;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.2.1",
4
- "description": "The Claw Bay CLI: link an API key and run a local relay to The Claw Bay backend.",
3
+ "version": "0.2.4",
4
+ "description": "The Claw Bay CLI: one-time API-key setup for direct Codex access, with optional relay fallback.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "theclawbay": "dist/index.js"
@@ -10,7 +10,11 @@
10
10
  "types": "dist/index.d.ts",
11
11
  "scripts": {
12
12
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/index.js',0o755)\"",
13
- "prepublishOnly": "npm run build"
13
+ "prepublishOnly": "npm run build",
14
+ "release:patch": "./scripts/release-npm.sh patch",
15
+ "release:minor": "./scripts/release-npm.sh minor",
16
+ "release:major": "./scripts/release-npm.sh major",
17
+ "release:dry-run": "./scripts/release-npm.sh patch --dry-run"
14
18
  },
15
19
  "engines": {
16
20
  "node": ">=18"
@@ -24,6 +28,8 @@
24
28
  "codex",
25
29
  "claw-bay",
26
30
  "cli",
31
+ "setup",
32
+ "wan",
27
33
  "proxy",
28
34
  "api-key",
29
35
  "relay"