theclawbay 0.2.5 → 0.2.8
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 +16 -26
- package/dist/commands/proxy.js +80 -25
- package/dist/commands/setup.d.ts +0 -6
- package/dist/commands/setup.js +468 -194
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,40 +32,31 @@ theclawbay setup --api-key <apiKey>
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
This auto-detects installed clients and writes direct WAN API-key config so users can run directly.
|
|
35
|
-
|
|
36
|
-
It also auto-selects the highest backend-advertised Codex model so the IDE doesn't fall back to `Custom`.
|
|
35
|
+
It configures Codex with `model_provider = "codex-lb"` and a managed bearer token, auto-selects the highest backend-advertised model, and preserves conversation visibility by neutralizing legacy local session provider tags so chats remain visible across provider modes.
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
It also configures OpenClaw/OpenCode automatically when those CLIs are installed.
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
theclawbay setup --api-key <apiKey> --client codex
|
|
42
|
-
theclawbay setup --api-key <apiKey> --client openclaw
|
|
43
|
-
theclawbay setup --api-key <apiKey> --client both
|
|
44
|
-
```
|
|
39
|
+
`CODEX_LB_API_KEY` is persisted for restarts in:
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
- `~/.config/theclawbay/env`
|
|
42
|
+
- `~/.bashrc`
|
|
43
|
+
- `~/.zshrc`
|
|
44
|
+
- `~/.profile`
|
|
45
|
+
- `~/.vscode-server/server-env-setup` (and `~/.vscode-server-insiders/server-env-setup` when present)
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
theclawbay setup --api-key <apiKey> --skip-login
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Pin a specific Codex model (otherwise setup uses automatic model selection):
|
|
47
|
+
If you operate a custom backend, pass it explicitly:
|
|
53
48
|
|
|
54
49
|
```sh
|
|
55
|
-
theclawbay setup --api-key <apiKey> --
|
|
50
|
+
theclawbay setup --api-key <apiKey> --backend https://your-domain.com
|
|
56
51
|
```
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
After setup, restart terminal/VS Code once so env-backed clients (OpenClaw/OpenCode) pick up `CODEX_LB_API_KEY`.
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
theclawbay setup --api-key <apiKey> --openai-sync
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
If you operate a custom backend, pass it explicitly:
|
|
55
|
+
## Codex Extension Behavior
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
- In API-key provider mode, Codex may label the model source as `Custom`.
|
|
58
|
+
- Full ChatGPT account model-picker behavior is tied to ChatGPT auth mode.
|
|
59
|
+
- `theclawbay setup` keeps your existing Codex login state unchanged on purpose (so local history context is preserved and setup stays non-destructive).
|
|
69
60
|
|
|
70
61
|
## Run Relay (Optional)
|
|
71
62
|
|
|
@@ -80,8 +71,7 @@ By default this starts a local relay on `http://127.0.0.1:2455` and forwards to:
|
|
|
80
71
|
|
|
81
72
|
- `https://theclawbay.com/api/codex-auth/v1/proxy/...`
|
|
82
73
|
|
|
83
|
-
The command
|
|
84
|
-
If both are installed and you're in an interactive terminal, it asks which one you want to configure.
|
|
74
|
+
The command auto-detects whether `openclaw` and/or `opencode` are installed and configures them automatically.
|
|
85
75
|
|
|
86
76
|
## Notes
|
|
87
77
|
|
package/dist/commands/proxy.js
CHANGED
|
@@ -82,36 +82,70 @@ async function askClientChoice() {
|
|
|
82
82
|
output: process.stdout,
|
|
83
83
|
});
|
|
84
84
|
try {
|
|
85
|
-
const answer = (await rl.question("Which
|
|
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
86
|
.trim()
|
|
87
87
|
.toLowerCase();
|
|
88
|
-
if (answer === "2" || answer === "
|
|
88
|
+
if (answer === "2" || answer === "codex")
|
|
89
|
+
return "codex";
|
|
90
|
+
if (answer === "3" || answer === "openclaw")
|
|
89
91
|
return "openclaw";
|
|
90
|
-
if (answer === "
|
|
92
|
+
if (answer === "4" || answer === "opencode")
|
|
93
|
+
return "opencode";
|
|
94
|
+
if (answer === "5" || answer === "both")
|
|
91
95
|
return "both";
|
|
92
|
-
return "
|
|
96
|
+
return "all";
|
|
93
97
|
}
|
|
94
98
|
finally {
|
|
95
99
|
rl.close();
|
|
96
100
|
}
|
|
97
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
|
+
}
|
|
98
123
|
async function resolveClientChoice(mode) {
|
|
99
|
-
if (mode === "
|
|
100
|
-
|
|
124
|
+
if (mode === "all") {
|
|
125
|
+
const detected = detectedClientTargets();
|
|
126
|
+
return countEnabledTargets(detected) > 0 ? detected : { codex: true, openclaw: false, opencode: false };
|
|
101
127
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (codexInstalled && !openclawInstalled)
|
|
105
|
-
return "codex";
|
|
106
|
-
if (!codexInstalled && openclawInstalled)
|
|
107
|
-
return "openclaw";
|
|
108
|
-
if (codexInstalled && openclawInstalled) {
|
|
109
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
110
|
-
return askClientChoice();
|
|
111
|
-
}
|
|
112
|
-
return "both";
|
|
128
|
+
if (mode === "codex" || mode === "openclaw" || mode === "opencode" || mode === "both") {
|
|
129
|
+
return clientTargetsForChoice(mode);
|
|
113
130
|
}
|
|
114
|
-
|
|
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;
|
|
115
149
|
}
|
|
116
150
|
class ProxyCommand extends base_command_1.BaseCommand {
|
|
117
151
|
async run() {
|
|
@@ -128,7 +162,7 @@ class ProxyCommand extends base_command_1.BaseCommand {
|
|
|
128
162
|
}
|
|
129
163
|
const basePath = ensureLeadingSlash(flags["base-path"].trim()).replace(/\/+$/g, "");
|
|
130
164
|
const proxyBase = `${trimTrailingSlash(backendUrl)}${basePath}`;
|
|
131
|
-
const
|
|
165
|
+
const targets = await resolveClientChoice(flags.client);
|
|
132
166
|
const server = node_http_1.default.createServer(async (req, res) => {
|
|
133
167
|
const method = req.method?.toUpperCase() ?? "GET";
|
|
134
168
|
const incomingUrl = new URL(req.url || "/", "http://local-proxy.invalid");
|
|
@@ -177,7 +211,7 @@ class ProxyCommand extends base_command_1.BaseCommand {
|
|
|
177
211
|
this.log(`theclawbay WAN relay listening at ${localBase}`);
|
|
178
212
|
this.log(`Forwarding to ${proxyBase}`);
|
|
179
213
|
this.log("");
|
|
180
|
-
if (
|
|
214
|
+
if (targets.codex) {
|
|
181
215
|
this.log("Codex CLI config snippet:");
|
|
182
216
|
this.log('model_provider = "theclawbay-wan"');
|
|
183
217
|
this.log("");
|
|
@@ -189,11 +223,11 @@ class ProxyCommand extends base_command_1.BaseCommand {
|
|
|
189
223
|
this.log("requires_openai_auth = true");
|
|
190
224
|
this.log("");
|
|
191
225
|
}
|
|
192
|
-
if (
|
|
226
|
+
if (targets.openclaw) {
|
|
193
227
|
const openClawProviderJson = JSON.stringify({
|
|
194
228
|
baseUrl: `${localBase}/v1`,
|
|
195
229
|
apiKey: "theclawbay-local",
|
|
196
|
-
api: "openai-
|
|
230
|
+
api: "openai-completions",
|
|
197
231
|
models: [
|
|
198
232
|
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
|
199
233
|
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
|
|
@@ -203,9 +237,30 @@ class ProxyCommand extends base_command_1.BaseCommand {
|
|
|
203
237
|
});
|
|
204
238
|
this.log("OpenClaw setup:");
|
|
205
239
|
this.log("Run these once:");
|
|
206
|
-
this.log('openclaw config set agents.defaults.model.primary "
|
|
240
|
+
this.log('openclaw config set agents.defaults.model.primary "codex-lb/gpt-5.3-codex"');
|
|
207
241
|
this.log('openclaw config set models.mode "merge"');
|
|
208
|
-
this.log(`openclaw config set models.providers.
|
|
242
|
+
this.log(`openclaw config set models.providers.codex-lb '${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
|
+
"codex-lb": {
|
|
251
|
+
npm: "@ai-sdk/openai-compatible",
|
|
252
|
+
name: "codex-lb",
|
|
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: "codex-lb/gpt-5.3-codex",
|
|
263
|
+
}, null, 2));
|
|
209
264
|
this.log("");
|
|
210
265
|
}
|
|
211
266
|
await new Promise((resolve) => {
|
|
@@ -244,7 +299,7 @@ ProxyCommand.flags = {
|
|
|
244
299
|
client: core_1.Flags.string({
|
|
245
300
|
required: false,
|
|
246
301
|
default: "auto",
|
|
247
|
-
options: ["auto", "codex", "openclaw", "both"],
|
|
302
|
+
options: ["auto", "codex", "openclaw", "opencode", "both", "all"],
|
|
248
303
|
description: "Client target to guide setup for",
|
|
249
304
|
}),
|
|
250
305
|
};
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -4,12 +4,6 @@ export default class SetupCommand extends BaseCommand {
|
|
|
4
4
|
static flags: {
|
|
5
5
|
backend: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
6
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
|
-
model: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
|
-
"skip-login": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
12
|
-
"openai-sync": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
13
7
|
};
|
|
14
8
|
run(): Promise<void>;
|
|
15
9
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -4,9 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
7
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
7
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
9
|
const node_child_process_1 = require("node:child_process");
|
|
9
|
-
const promises_2 = require("node:readline/promises");
|
|
10
10
|
const core_1 = require("@oclif/core");
|
|
11
11
|
const base_command_1 = require("../lib/base-command");
|
|
12
12
|
const paths_1 = require("../lib/config/paths");
|
|
@@ -14,18 +14,34 @@ const api_key_1 = require("../lib/managed/api-key");
|
|
|
14
14
|
const config_1 = require("../lib/managed/config");
|
|
15
15
|
const errors_1 = require("../lib/managed/errors");
|
|
16
16
|
const DEFAULT_BACKEND_URL = "https://theclawbay.com";
|
|
17
|
-
const DEFAULT_PROVIDER_ID = "
|
|
18
|
-
const MANAGED_START = "# theclawbay-managed:start";
|
|
19
|
-
const MANAGED_END = "# theclawbay-managed:end";
|
|
20
|
-
const DEFAULT_OPENCLAW_MODEL = "gpt-5.3-codex";
|
|
17
|
+
const DEFAULT_PROVIDER_ID = "codex-lb";
|
|
21
18
|
const DEFAULT_CODEX_MODEL = "gpt-5.2-codex";
|
|
22
|
-
const
|
|
19
|
+
const DEFAULT_MODELS = ["gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.2-codex", "gpt-5.1-codex"];
|
|
20
|
+
const PREFERRED_MODELS = [
|
|
23
21
|
"gpt-5.3-codex",
|
|
24
22
|
"gpt-5.3-codex-spark",
|
|
25
23
|
"gpt-5.2-codex",
|
|
26
24
|
"gpt-5.1-codex-max",
|
|
27
25
|
"gpt-5.1-codex-mini",
|
|
26
|
+
"gpt-5.1-codex",
|
|
28
27
|
];
|
|
28
|
+
const MODEL_DISPLAY_NAMES = {
|
|
29
|
+
"gpt-5.3-codex": "GPT-5.3 Codex",
|
|
30
|
+
"gpt-5.3-codex-spark": "GPT-5.3 Codex Spark",
|
|
31
|
+
"gpt-5.2-codex": "GPT-5.2 Codex",
|
|
32
|
+
"gpt-5.1-codex-max": "GPT-5.1 Codex Max",
|
|
33
|
+
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
|
|
34
|
+
"gpt-5.1-codex": "GPT-5.1 Codex",
|
|
35
|
+
};
|
|
36
|
+
const ENV_KEY_NAME = "CODEX_LB_API_KEY";
|
|
37
|
+
const ENV_FILE = node_path_1.default.join(node_os_1.default.homedir(), ".config", "theclawbay", "env");
|
|
38
|
+
const MIGRATION_STATE_FILE = node_path_1.default.join(paths_1.codexDir, "theclawbay.migration.json");
|
|
39
|
+
const MANAGED_START = "# theclawbay-managed:start";
|
|
40
|
+
const MANAGED_END = "# theclawbay-managed:end";
|
|
41
|
+
const SHELL_START = "# theclawbay-shell-managed:start";
|
|
42
|
+
const SHELL_END = "# theclawbay-shell-managed:end";
|
|
43
|
+
const OPENCLAW_PROVIDER_ID = "codex-lb";
|
|
44
|
+
const HISTORY_PROVIDER_NEUTRALIZE_SOURCES = new Set(["openai", "theclawbay-wan", "codex-lb"]);
|
|
29
45
|
function trimTrailingSlash(value) {
|
|
30
46
|
return value.replace(/\/+$/g, "");
|
|
31
47
|
}
|
|
@@ -42,58 +58,27 @@ function hasCommand(name) {
|
|
|
42
58
|
const result = (0, node_child_process_1.spawnSync)("which", [name], { stdio: "ignore" });
|
|
43
59
|
return result.status === 0;
|
|
44
60
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
output: process.stdout,
|
|
49
|
-
});
|
|
50
|
-
try {
|
|
51
|
-
const answer = (await rl.question("Which client do you want to configure? [1] Codex [2] OpenClaw [3] Both: "))
|
|
52
|
-
.trim()
|
|
53
|
-
.toLowerCase();
|
|
54
|
-
if (answer === "2" || answer === "openclaw")
|
|
55
|
-
return "openclaw";
|
|
56
|
-
if (answer === "3" || answer === "both")
|
|
57
|
-
return "both";
|
|
58
|
-
return "codex";
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
rl.close();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async function resolveClientChoice(mode) {
|
|
65
|
-
if (mode === "codex" || mode === "openclaw" || mode === "both") {
|
|
66
|
-
return mode;
|
|
67
|
-
}
|
|
68
|
-
const codexInstalled = hasCommand("codex");
|
|
69
|
-
const openclawInstalled = hasCommand("openclaw");
|
|
70
|
-
if (codexInstalled && !openclawInstalled)
|
|
71
|
-
return "codex";
|
|
72
|
-
if (!codexInstalled && openclawInstalled)
|
|
73
|
-
return "openclaw";
|
|
74
|
-
if (codexInstalled && openclawInstalled) {
|
|
75
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
76
|
-
return askClientChoice();
|
|
77
|
-
}
|
|
78
|
-
return "both";
|
|
79
|
-
}
|
|
80
|
-
return "codex";
|
|
81
|
-
}
|
|
82
|
-
function removeManagedBlock(source) {
|
|
83
|
-
const start = source.indexOf(MANAGED_START);
|
|
84
|
-
if (start < 0)
|
|
61
|
+
function removeManagedBlock(source, start, end) {
|
|
62
|
+
const markerStart = source.indexOf(start);
|
|
63
|
+
if (markerStart < 0)
|
|
85
64
|
return source;
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
return source.slice(0,
|
|
89
|
-
return (source.slice(0,
|
|
65
|
+
const markerEnd = source.indexOf(end, markerStart);
|
|
66
|
+
if (markerEnd < 0)
|
|
67
|
+
return source.slice(0, markerStart).trimEnd() + "\n";
|
|
68
|
+
return (source.slice(0, markerStart) + source.slice(markerEnd + end.length)).trimEnd() + "\n";
|
|
69
|
+
}
|
|
70
|
+
function appendManagedBlock(source, lines) {
|
|
71
|
+
const body = lines.join("\n").trimEnd() + "\n";
|
|
72
|
+
if (!source.trim())
|
|
73
|
+
return body;
|
|
74
|
+
return `${source.trimEnd()}\n\n${body}`;
|
|
90
75
|
}
|
|
91
76
|
function removeProviderTable(source, providerId) {
|
|
92
77
|
const header = `[model_providers.${providerId}]`;
|
|
93
78
|
const lines = source.split(/\r?\n/);
|
|
94
79
|
const output = [];
|
|
95
80
|
for (let i = 0; i < lines.length; i++) {
|
|
96
|
-
if (lines[i]
|
|
81
|
+
if ((lines[i] ?? "").trim() !== header) {
|
|
97
82
|
output.push(lines[i] ?? "");
|
|
98
83
|
continue;
|
|
99
84
|
}
|
|
@@ -120,6 +105,226 @@ function upsertFirstKeyLine(source, key, tomlValue) {
|
|
|
120
105
|
}
|
|
121
106
|
return `${`${key} = ${tomlValue}\n${source}`.trimEnd()}\n`;
|
|
122
107
|
}
|
|
108
|
+
function shellQuote(value) {
|
|
109
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
110
|
+
}
|
|
111
|
+
function modelDisplayName(modelId) {
|
|
112
|
+
return MODEL_DISPLAY_NAMES[modelId] ?? modelId;
|
|
113
|
+
}
|
|
114
|
+
function renderProgressBar(current, total) {
|
|
115
|
+
if (total <= 0)
|
|
116
|
+
return "";
|
|
117
|
+
const width = 24;
|
|
118
|
+
const ratio = Math.max(0, Math.min(1, current / total));
|
|
119
|
+
const filled = Math.round(width * ratio);
|
|
120
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}] ${Math.round(ratio * 100)
|
|
121
|
+
.toString()
|
|
122
|
+
.padStart(3, " ")}%`;
|
|
123
|
+
}
|
|
124
|
+
function inferRolloutUpdatedAt(source) {
|
|
125
|
+
const lines = source.split(/\r?\n/);
|
|
126
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
127
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
128
|
+
if (!trimmed)
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(trimmed);
|
|
132
|
+
if (typeof parsed.timestamp !== "string")
|
|
133
|
+
continue;
|
|
134
|
+
const ms = Date.parse(parsed.timestamp);
|
|
135
|
+
if (!Number.isFinite(ms))
|
|
136
|
+
continue;
|
|
137
|
+
return new Date(ms);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
async function listSessionRollouts(root) {
|
|
146
|
+
const results = [];
|
|
147
|
+
const walk = async (dir) => {
|
|
148
|
+
let entries = [];
|
|
149
|
+
try {
|
|
150
|
+
entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const err = error;
|
|
154
|
+
if (err.code === "ENOENT")
|
|
155
|
+
return;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const nextPath = node_path_1.default.join(dir, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
await walk(nextPath);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (entry.isFile() && nextPath.endsWith(".jsonl")) {
|
|
165
|
+
results.push(nextPath);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
await walk(root);
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
async function readMigrationState() {
|
|
173
|
+
try {
|
|
174
|
+
const raw = await promises_1.default.readFile(MIGRATION_STATE_FILE, "utf8");
|
|
175
|
+
const parsed = JSON.parse(raw);
|
|
176
|
+
if (parsed.version === 1 &&
|
|
177
|
+
typeof parsed.fileCount === "number" &&
|
|
178
|
+
Number.isFinite(parsed.fileCount) &&
|
|
179
|
+
typeof parsed.maxMtimeMs === "number" &&
|
|
180
|
+
Number.isFinite(parsed.maxMtimeMs)) {
|
|
181
|
+
return {
|
|
182
|
+
version: 1,
|
|
183
|
+
fileCount: Math.max(0, Math.floor(parsed.fileCount)),
|
|
184
|
+
maxMtimeMs: Math.max(0, Math.floor(parsed.maxMtimeMs)),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function writeMigrationState(state) {
|
|
194
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(MIGRATION_STATE_FILE), { recursive: true });
|
|
195
|
+
await promises_1.default.writeFile(MIGRATION_STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
196
|
+
}
|
|
197
|
+
async function computeSessionSnapshot(files) {
|
|
198
|
+
let maxMtimeMs = 0;
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
try {
|
|
201
|
+
const stats = await promises_1.default.stat(file);
|
|
202
|
+
const mtimeMs = Math.floor(stats.mtimeMs);
|
|
203
|
+
if (mtimeMs > maxMtimeMs)
|
|
204
|
+
maxMtimeMs = mtimeMs;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// ignore transient missing file/race
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { fileCount: files.length, maxMtimeMs };
|
|
211
|
+
}
|
|
212
|
+
async function migrateSessionProviders(params) {
|
|
213
|
+
const sessionsRoot = node_path_1.default.join(params.codexHome, "sessions");
|
|
214
|
+
const files = await listSessionRollouts(sessionsRoot);
|
|
215
|
+
const snapshot = await computeSessionSnapshot(files);
|
|
216
|
+
const previousState = await readMigrationState();
|
|
217
|
+
if (previousState &&
|
|
218
|
+
previousState.fileCount === snapshot.fileCount &&
|
|
219
|
+
previousState.maxMtimeMs === snapshot.maxMtimeMs) {
|
|
220
|
+
return { scanned: 0, rewritten: 0, skipped: 0, retimed: 0, fastSkipped: true };
|
|
221
|
+
}
|
|
222
|
+
let rewritten = 0;
|
|
223
|
+
let skipped = 0;
|
|
224
|
+
let retimed = 0;
|
|
225
|
+
const useProgress = process.stdout.isTTY && files.length > 0;
|
|
226
|
+
const drawProgress = (current) => {
|
|
227
|
+
if (!useProgress)
|
|
228
|
+
return;
|
|
229
|
+
const bar = renderProgressBar(current, files.length);
|
|
230
|
+
process.stdout.write(`\rMigrating conversations ${current}/${files.length} ${bar}`);
|
|
231
|
+
if (current >= files.length)
|
|
232
|
+
process.stdout.write("\n");
|
|
233
|
+
};
|
|
234
|
+
drawProgress(0);
|
|
235
|
+
for (let i = 0; i < files.length; i++) {
|
|
236
|
+
const file = files[i];
|
|
237
|
+
let source = "";
|
|
238
|
+
let originalStats = null;
|
|
239
|
+
try {
|
|
240
|
+
source = await promises_1.default.readFile(file, "utf8");
|
|
241
|
+
originalStats = await promises_1.default.stat(file);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
skipped++;
|
|
245
|
+
drawProgress(i + 1);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const desiredMtime = inferRolloutUpdatedAt(source) ?? originalStats.mtime;
|
|
249
|
+
const desiredAtime = originalStats.atime;
|
|
250
|
+
const maybeRetainMtime = async () => {
|
|
251
|
+
const diffMs = Math.abs(desiredMtime.getTime() - originalStats.mtime.getTime());
|
|
252
|
+
if (diffMs <= 1500)
|
|
253
|
+
return;
|
|
254
|
+
try {
|
|
255
|
+
await promises_1.default.utimes(file, desiredAtime, desiredMtime);
|
|
256
|
+
retimed++;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// best-effort only
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
if (!source.trim()) {
|
|
263
|
+
skipped++;
|
|
264
|
+
await maybeRetainMtime();
|
|
265
|
+
drawProgress(i + 1);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const lines = source.split(/\r?\n/);
|
|
269
|
+
const firstNonEmptyIndex = lines.findIndex((line) => line.trim().length > 0);
|
|
270
|
+
if (firstNonEmptyIndex < 0) {
|
|
271
|
+
skipped++;
|
|
272
|
+
drawProgress(i + 1);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(lines[firstNonEmptyIndex] ?? "");
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
skipped++;
|
|
281
|
+
drawProgress(i + 1);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if ((parsed.type ?? "") !== "session_meta") {
|
|
285
|
+
skipped++;
|
|
286
|
+
drawProgress(i + 1);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const payload = parsed.payload;
|
|
290
|
+
if (!payload || typeof payload !== "object") {
|
|
291
|
+
skipped++;
|
|
292
|
+
drawProgress(i + 1);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const currentProvider = payload.model_provider;
|
|
296
|
+
if (typeof currentProvider !== "string") {
|
|
297
|
+
skipped++;
|
|
298
|
+
drawProgress(i + 1);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (!HISTORY_PROVIDER_NEUTRALIZE_SOURCES.has(currentProvider)) {
|
|
302
|
+
skipped++;
|
|
303
|
+
drawProgress(i + 1);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Remove explicit provider tag so the same thread stays visible across provider contexts.
|
|
307
|
+
delete payload.model_provider;
|
|
308
|
+
lines[firstNonEmptyIndex] = JSON.stringify(parsed);
|
|
309
|
+
const next = lines.join("\n");
|
|
310
|
+
if (next === source) {
|
|
311
|
+
skipped++;
|
|
312
|
+
await maybeRetainMtime();
|
|
313
|
+
drawProgress(i + 1);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
await promises_1.default.writeFile(file, next, "utf8");
|
|
317
|
+
await maybeRetainMtime();
|
|
318
|
+
rewritten++;
|
|
319
|
+
drawProgress(i + 1);
|
|
320
|
+
}
|
|
321
|
+
await writeMigrationState({
|
|
322
|
+
version: 1,
|
|
323
|
+
fileCount: snapshot.fileCount,
|
|
324
|
+
maxMtimeMs: snapshot.maxMtimeMs,
|
|
325
|
+
});
|
|
326
|
+
return { scanned: files.length, rewritten, skipped, retimed, fastSkipped: false };
|
|
327
|
+
}
|
|
123
328
|
async function fetchBackendModelIds(backendUrl, apiKey) {
|
|
124
329
|
const url = `${trimTrailingSlash(backendUrl)}/api/codex-auth/v1/proxy/v1/models`;
|
|
125
330
|
try {
|
|
@@ -127,40 +332,54 @@ async function fetchBackendModelIds(backendUrl, apiKey) {
|
|
|
127
332
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
128
333
|
signal: AbortSignal.timeout(4500),
|
|
129
334
|
});
|
|
130
|
-
if (!response.ok)
|
|
335
|
+
if (!response.ok)
|
|
131
336
|
return null;
|
|
132
|
-
}
|
|
133
337
|
const body = (await response.json());
|
|
134
338
|
const ids = (body.data ?? [])
|
|
135
339
|
.map((entry) => (typeof entry.id === "string" ? entry.id.trim() : ""))
|
|
136
340
|
.filter((id) => id.length > 0);
|
|
137
|
-
return ids.length
|
|
341
|
+
return ids.length ? ids : null;
|
|
138
342
|
}
|
|
139
343
|
catch {
|
|
140
344
|
return null;
|
|
141
345
|
}
|
|
142
346
|
}
|
|
143
|
-
async function
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
if (!modelIds) {
|
|
150
|
-
return {
|
|
151
|
-
model: DEFAULT_CODEX_MODEL,
|
|
152
|
-
note: `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
const available = new Set(modelIds);
|
|
156
|
-
for (const preferred of PREFERRED_CODEX_MODELS) {
|
|
347
|
+
async function resolveModels(backendUrl, apiKey) {
|
|
348
|
+
const ids = await fetchBackendModelIds(backendUrl, apiKey);
|
|
349
|
+
const available = new Set(ids ?? []);
|
|
350
|
+
let selected = DEFAULT_CODEX_MODEL;
|
|
351
|
+
let note;
|
|
352
|
+
for (const preferred of PREFERRED_MODELS) {
|
|
157
353
|
if (available.has(preferred)) {
|
|
158
|
-
|
|
354
|
+
selected = preferred;
|
|
355
|
+
break;
|
|
159
356
|
}
|
|
160
357
|
}
|
|
358
|
+
if (ids?.length && !available.has(selected)) {
|
|
359
|
+
selected = ids[0] ?? DEFAULT_CODEX_MODEL;
|
|
360
|
+
note = "No preferred Codex model advertised by backend; selected first available model.";
|
|
361
|
+
}
|
|
362
|
+
else if (!ids) {
|
|
363
|
+
note = `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`;
|
|
364
|
+
}
|
|
365
|
+
const unique = [];
|
|
366
|
+
const pushUnique = (modelId) => {
|
|
367
|
+
if (!modelId || unique.includes(modelId))
|
|
368
|
+
return;
|
|
369
|
+
unique.push(modelId);
|
|
370
|
+
};
|
|
371
|
+
pushUnique(selected);
|
|
372
|
+
for (const modelId of DEFAULT_MODELS)
|
|
373
|
+
pushUnique(modelId);
|
|
374
|
+
for (const modelId of ids ?? []) {
|
|
375
|
+
if (unique.length >= 8)
|
|
376
|
+
break;
|
|
377
|
+
pushUnique(modelId);
|
|
378
|
+
}
|
|
161
379
|
return {
|
|
162
|
-
model:
|
|
163
|
-
|
|
380
|
+
model: selected,
|
|
381
|
+
models: unique.map((modelId) => ({ id: modelId, name: modelDisplayName(modelId) })),
|
|
382
|
+
note,
|
|
164
383
|
};
|
|
165
384
|
}
|
|
166
385
|
async function writeCodexConfig(params) {
|
|
@@ -177,41 +396,82 @@ async function writeCodexConfig(params) {
|
|
|
177
396
|
}
|
|
178
397
|
const proxyRoot = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy`;
|
|
179
398
|
let next = existing;
|
|
180
|
-
next = removeManagedBlock(next);
|
|
181
|
-
next = removeProviderTable(next,
|
|
182
|
-
next = upsertFirstKeyLine(next, "model_provider", `"${
|
|
183
|
-
next = upsertFirstKeyLine(next, "model", `"${params.
|
|
184
|
-
const managedBlock = [
|
|
399
|
+
next = removeManagedBlock(next, MANAGED_START, MANAGED_END);
|
|
400
|
+
next = removeProviderTable(next, DEFAULT_PROVIDER_ID);
|
|
401
|
+
next = upsertFirstKeyLine(next, "model_provider", `"${DEFAULT_PROVIDER_ID}"`);
|
|
402
|
+
next = upsertFirstKeyLine(next, "model", `"${params.model}"`);
|
|
403
|
+
const managedBlock = appendManagedBlock("", [
|
|
185
404
|
MANAGED_START,
|
|
186
|
-
`[model_providers.${
|
|
405
|
+
`[model_providers.${DEFAULT_PROVIDER_ID}]`,
|
|
187
406
|
'name = "OpenAI"',
|
|
188
407
|
`base_url = "${proxyRoot}/backend-api/codex"`,
|
|
189
408
|
'wire_api = "responses"',
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: []),
|
|
409
|
+
"requires_openai_auth = true",
|
|
410
|
+
`experimental_bearer_token = "${params.apiKey}"`,
|
|
193
411
|
MANAGED_END,
|
|
412
|
+
]);
|
|
413
|
+
next = appendManagedBlock(next, managedBlock.trimEnd().split("\n"));
|
|
414
|
+
await promises_1.default.writeFile(configPath, next, "utf8");
|
|
415
|
+
return configPath;
|
|
416
|
+
}
|
|
417
|
+
async function persistApiKeyEnv(apiKey) {
|
|
418
|
+
const envDir = node_path_1.default.dirname(ENV_FILE);
|
|
419
|
+
await promises_1.default.mkdir(envDir, { recursive: true });
|
|
420
|
+
const envContents = [
|
|
421
|
+
"# Generated by theclawbay setup",
|
|
422
|
+
`export ${ENV_KEY_NAME}=${shellQuote(apiKey)}`,
|
|
194
423
|
"",
|
|
195
424
|
].join("\n");
|
|
196
|
-
|
|
197
|
-
|
|
425
|
+
await promises_1.default.writeFile(ENV_FILE, envContents, "utf8");
|
|
426
|
+
await promises_1.default.chmod(ENV_FILE, 0o600);
|
|
427
|
+
const sourceLine = `[ -f "$HOME/.config/theclawbay/env" ] && . "$HOME/.config/theclawbay/env"`;
|
|
428
|
+
const shellRcPaths = [".bashrc", ".zshrc", ".profile"].map((name) => node_path_1.default.join(node_os_1.default.homedir(), name));
|
|
429
|
+
const updated = [];
|
|
430
|
+
for (const rcPath of shellRcPaths) {
|
|
431
|
+
let existing = "";
|
|
432
|
+
try {
|
|
433
|
+
existing = await promises_1.default.readFile(rcPath, "utf8");
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
const err = error;
|
|
437
|
+
if (err.code !== "ENOENT")
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
const cleaned = removeManagedBlock(existing, SHELL_START, SHELL_END);
|
|
441
|
+
const withBlock = appendManagedBlock(cleaned, [SHELL_START, sourceLine, SHELL_END]);
|
|
442
|
+
await promises_1.default.writeFile(rcPath, withBlock, "utf8");
|
|
443
|
+
updated.push(rcPath);
|
|
198
444
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return configPath;
|
|
445
|
+
process.env[ENV_KEY_NAME] = apiKey;
|
|
446
|
+
return updated;
|
|
202
447
|
}
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
448
|
+
async function persistVsCodeServerEnvSource() {
|
|
449
|
+
const homes = [".vscode-server", ".vscode-server-insiders"];
|
|
450
|
+
const sourceLine = `[ -f "$HOME/.config/theclawbay/env" ] && . "$HOME/.config/theclawbay/env"`;
|
|
451
|
+
const updated = [];
|
|
452
|
+
for (const home of homes) {
|
|
453
|
+
const setupPath = node_path_1.default.join(node_os_1.default.homedir(), home, "server-env-setup");
|
|
454
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(setupPath), { recursive: true });
|
|
455
|
+
let existing = "";
|
|
456
|
+
try {
|
|
457
|
+
existing = await promises_1.default.readFile(setupPath, "utf8");
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const err = error;
|
|
461
|
+
if (err.code !== "ENOENT")
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
let next = existing.trimEnd();
|
|
465
|
+
if (!next.startsWith("#!")) {
|
|
466
|
+
next = `#!/usr/bin/env sh\n${next}`.trimEnd();
|
|
467
|
+
}
|
|
468
|
+
next = `${removeManagedBlock(next + "\n", SHELL_START, SHELL_END).trimEnd()}\n`;
|
|
469
|
+
next = appendManagedBlock(next, [SHELL_START, sourceLine, SHELL_END]);
|
|
470
|
+
await promises_1.default.writeFile(setupPath, next, "utf8");
|
|
471
|
+
await promises_1.default.chmod(setupPath, 0o700);
|
|
472
|
+
updated.push(setupPath);
|
|
473
|
+
}
|
|
474
|
+
return updated;
|
|
215
475
|
}
|
|
216
476
|
function runOpenClawConfigCommand(args) {
|
|
217
477
|
const run = (0, node_child_process_1.spawnSync)("openclaw", args, {
|
|
@@ -226,29 +486,78 @@ function runOpenClawConfigCommand(args) {
|
|
|
226
486
|
throw new Error(`failed to update OpenClaw config: ${details}`);
|
|
227
487
|
}
|
|
228
488
|
function setupOpenClaw(params) {
|
|
229
|
-
const base = trimTrailingSlash(params.backendUrl);
|
|
230
|
-
const model = params.model.trim() || DEFAULT_OPENCLAW_MODEL;
|
|
231
489
|
const provider = {
|
|
232
|
-
baseUrl: `${
|
|
233
|
-
apiKey:
|
|
234
|
-
api: "openai-
|
|
235
|
-
models:
|
|
236
|
-
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
|
237
|
-
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
|
|
238
|
-
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
|
239
|
-
{ id: "gpt-5.1-codex", name: "GPT-5.1 Codex" },
|
|
240
|
-
],
|
|
490
|
+
baseUrl: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
|
|
491
|
+
apiKey: "${CODEX_LB_API_KEY}",
|
|
492
|
+
api: "openai-completions",
|
|
493
|
+
models: params.models,
|
|
241
494
|
};
|
|
242
|
-
runOpenClawConfigCommand([
|
|
495
|
+
runOpenClawConfigCommand([
|
|
496
|
+
"config",
|
|
497
|
+
"set",
|
|
498
|
+
"agents.defaults.model.primary",
|
|
499
|
+
`${OPENCLAW_PROVIDER_ID}/${params.model.trim() || DEFAULT_CODEX_MODEL}`,
|
|
500
|
+
]);
|
|
243
501
|
runOpenClawConfigCommand(["config", "set", "models.mode", "merge"]);
|
|
244
502
|
runOpenClawConfigCommand([
|
|
245
503
|
"config",
|
|
246
504
|
"set",
|
|
247
|
-
|
|
505
|
+
`models.providers.${OPENCLAW_PROVIDER_ID}`,
|
|
248
506
|
JSON.stringify(provider),
|
|
249
507
|
"--json",
|
|
250
508
|
]);
|
|
251
509
|
}
|
|
510
|
+
function objectRecordOr(value, fallback) {
|
|
511
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
512
|
+
return { ...value };
|
|
513
|
+
}
|
|
514
|
+
return fallback;
|
|
515
|
+
}
|
|
516
|
+
function openCodeModelsObject(models) {
|
|
517
|
+
const result = {};
|
|
518
|
+
for (const model of models) {
|
|
519
|
+
if (!model.id)
|
|
520
|
+
continue;
|
|
521
|
+
result[model.id] = { name: model.name || model.id };
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
524
|
+
}
|
|
525
|
+
async function writeOpenCodeConfig(params) {
|
|
526
|
+
const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json");
|
|
527
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
|
|
528
|
+
let existingRaw = "";
|
|
529
|
+
try {
|
|
530
|
+
existingRaw = await promises_1.default.readFile(configPath, "utf8");
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
const err = error;
|
|
534
|
+
if (err.code !== "ENOENT")
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
let doc = {};
|
|
538
|
+
if (existingRaw.trim()) {
|
|
539
|
+
try {
|
|
540
|
+
doc = objectRecordOr(JSON.parse(existingRaw), {});
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
throw new Error(`invalid JSON in OpenCode config: ${configPath}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const providerRoot = objectRecordOr(doc.provider, {});
|
|
547
|
+
providerRoot[DEFAULT_PROVIDER_ID] = {
|
|
548
|
+
npm: "@ai-sdk/openai-compatible",
|
|
549
|
+
name: DEFAULT_PROVIDER_ID,
|
|
550
|
+
options: {
|
|
551
|
+
baseURL: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
|
|
552
|
+
apiKey: `{env:${ENV_KEY_NAME}}`,
|
|
553
|
+
},
|
|
554
|
+
models: openCodeModelsObject(params.models),
|
|
555
|
+
};
|
|
556
|
+
doc.provider = providerRoot;
|
|
557
|
+
doc.model = `${DEFAULT_PROVIDER_ID}/${params.model}`;
|
|
558
|
+
await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
|
559
|
+
return configPath;
|
|
560
|
+
}
|
|
252
561
|
class SetupCommand extends base_command_1.BaseCommand {
|
|
253
562
|
async run() {
|
|
254
563
|
await this.runSafe(async () => {
|
|
@@ -262,77 +571,73 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
262
571
|
throw error;
|
|
263
572
|
}
|
|
264
573
|
const apiKey = (flags["api-key"] ?? managed?.apiKey ?? "").trim();
|
|
265
|
-
if (!apiKey)
|
|
574
|
+
if (!apiKey)
|
|
266
575
|
throw new Error('API key is required. Run "theclawbay setup --api-key <key>".');
|
|
267
|
-
}
|
|
268
576
|
const backendRaw = flags.backend ?? (0, api_key_1.tryInferBackendUrlFromApiKey)(apiKey) ?? managed?.backendUrl ?? DEFAULT_BACKEND_URL;
|
|
269
577
|
const backendUrl = normalizeUrl(backendRaw, "--backend");
|
|
270
|
-
const clientChoice = await resolveClientChoice(flags.client);
|
|
271
|
-
const codexWanted = clientChoice === "codex" || clientChoice === "both";
|
|
272
|
-
const openClawWanted = clientChoice === "openclaw" || clientChoice === "both";
|
|
273
578
|
await (0, config_1.writeManagedConfig)({ backendUrl, apiKey });
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
579
|
+
const resolved = await resolveModels(backendUrl, apiKey);
|
|
580
|
+
const codexConfigPath = await writeCodexConfig({ backendUrl, model: resolved.model, apiKey });
|
|
581
|
+
const updatedShellFiles = await persistApiKeyEnv(apiKey);
|
|
582
|
+
const updatedVsCodeEnvFiles = await persistVsCodeServerEnvSource();
|
|
583
|
+
const sessionMigration = await migrateSessionProviders({ codexHome: paths_1.codexDir });
|
|
584
|
+
const hasOpenClaw = hasCommand("openclaw");
|
|
585
|
+
const hasOpenCode = hasCommand("opencode");
|
|
586
|
+
let openCodePath = null;
|
|
587
|
+
if (hasOpenClaw) {
|
|
588
|
+
setupOpenClaw({
|
|
278
589
|
backendUrl,
|
|
279
|
-
|
|
280
|
-
|
|
590
|
+
model: resolved.model,
|
|
591
|
+
models: resolved.models,
|
|
281
592
|
});
|
|
282
|
-
|
|
593
|
+
}
|
|
594
|
+
if (hasOpenCode) {
|
|
595
|
+
openCodePath = await writeOpenCodeConfig({
|
|
283
596
|
backendUrl,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
openaiSync: flags["openai-sync"],
|
|
597
|
+
model: resolved.model,
|
|
598
|
+
models: resolved.models,
|
|
287
599
|
});
|
|
288
600
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
601
|
+
this.log("Setup complete");
|
|
602
|
+
this.log(`- Managed config: ${paths_1.managedConfigPath}`);
|
|
603
|
+
this.log(`- Backend: ${backendUrl}`);
|
|
604
|
+
this.log(`- Codex config: ${codexConfigPath}`);
|
|
605
|
+
this.log(`- Codex model: ${resolved.model}`);
|
|
606
|
+
if (resolved.note)
|
|
607
|
+
this.log(resolved.note);
|
|
608
|
+
if (sessionMigration.rewritten > 0) {
|
|
609
|
+
this.log(`- Conversations: updated ${sessionMigration.rewritten}/${sessionMigration.scanned} local sessions for cross-provider visibility.`);
|
|
294
610
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
backendUrl,
|
|
301
|
-
apiKey,
|
|
302
|
-
model: flags["openclaw-model"],
|
|
303
|
-
});
|
|
611
|
+
else if (sessionMigration.fastSkipped) {
|
|
612
|
+
this.log("- Conversations: already migrated (fast check).");
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
this.log("- Conversations: no local sessions required migration.");
|
|
304
616
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (codexConfigPath) {
|
|
308
|
-
this.log(`Codex config updated at ${codexConfigPath}`);
|
|
309
|
-
if (codexModel) {
|
|
310
|
-
this.log(`Codex model set to ${codexModel.model}`);
|
|
311
|
-
if (codexModel.note) {
|
|
312
|
-
this.log(codexModel.note);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (flags["openai-sync"]) {
|
|
316
|
-
this.log("Codex configured in OpenAI-sync mode (ChatGPT-linked history view).");
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
this.log("Codex configured in local-history mode (default).");
|
|
320
|
-
}
|
|
617
|
+
if (sessionMigration.retimed > 0) {
|
|
618
|
+
this.log(`- Conversation ordering: repaired timestamps on ${sessionMigration.retimed} local sessions.`);
|
|
321
619
|
}
|
|
322
|
-
|
|
323
|
-
|
|
620
|
+
this.log(`- API key env: ${ENV_FILE}`);
|
|
621
|
+
this.log(`- Shell profiles updated: ${updatedShellFiles.join(", ")}`);
|
|
622
|
+
this.log(`- VS Code env hooks updated: ${updatedVsCodeEnvFiles.join(", ")}`);
|
|
623
|
+
if (hasOpenClaw) {
|
|
624
|
+
this.log("- OpenClaw: configured");
|
|
324
625
|
}
|
|
325
|
-
|
|
326
|
-
this.log("
|
|
626
|
+
else {
|
|
627
|
+
this.log("- OpenClaw: not detected (skipped)");
|
|
327
628
|
}
|
|
328
|
-
|
|
329
|
-
this.log(
|
|
629
|
+
if (hasOpenCode) {
|
|
630
|
+
this.log(`- OpenCode: configured (${openCodePath})`);
|
|
330
631
|
}
|
|
331
|
-
|
|
632
|
+
else {
|
|
633
|
+
this.log("- OpenCode: not detected (skipped)");
|
|
634
|
+
}
|
|
635
|
+
this.log("- Codex login state: unchanged");
|
|
636
|
+
this.log("Next: restart terminal/VS Code window once so OpenClaw/OpenCode pick up CODEX_LB_API_KEY.");
|
|
332
637
|
});
|
|
333
638
|
}
|
|
334
639
|
}
|
|
335
|
-
SetupCommand.description = "One-time
|
|
640
|
+
SetupCommand.description = "One-time setup: configure Codex/OpenClaw/OpenCode to route through The Claw Bay using your API key";
|
|
336
641
|
SetupCommand.flags = {
|
|
337
642
|
backend: core_1.Flags.string({
|
|
338
643
|
required: false,
|
|
@@ -343,36 +648,5 @@ SetupCommand.flags = {
|
|
|
343
648
|
aliases: ["apiKey"],
|
|
344
649
|
description: "API key issued by your The Claw Bay dashboard",
|
|
345
650
|
}),
|
|
346
|
-
provider: core_1.Flags.string({
|
|
347
|
-
required: false,
|
|
348
|
-
default: DEFAULT_PROVIDER_ID,
|
|
349
|
-
description: 'Codex model provider id to write into ~/.codex/config.toml (default: "openai")',
|
|
350
|
-
}),
|
|
351
|
-
client: core_1.Flags.string({
|
|
352
|
-
required: false,
|
|
353
|
-
default: "auto",
|
|
354
|
-
options: ["auto", "codex", "openclaw", "both"],
|
|
355
|
-
description: "Client target to configure",
|
|
356
|
-
}),
|
|
357
|
-
"openclaw-model": core_1.Flags.string({
|
|
358
|
-
required: false,
|
|
359
|
-
default: DEFAULT_OPENCLAW_MODEL,
|
|
360
|
-
description: "OpenClaw model id to set as default (without provider prefix)",
|
|
361
|
-
}),
|
|
362
|
-
model: core_1.Flags.string({
|
|
363
|
-
required: false,
|
|
364
|
-
default: "auto",
|
|
365
|
-
description: 'Codex model id to set in ~/.codex/config.toml (default: "auto" picks highest available from backend)',
|
|
366
|
-
}),
|
|
367
|
-
"skip-login": core_1.Flags.boolean({
|
|
368
|
-
required: false,
|
|
369
|
-
default: false,
|
|
370
|
-
description: "Skip `codex login --with-api-key`",
|
|
371
|
-
}),
|
|
372
|
-
"openai-sync": core_1.Flags.boolean({
|
|
373
|
-
required: false,
|
|
374
|
-
default: false,
|
|
375
|
-
description: "Enable OpenAI-synced conversation mode (writes chatgpt_base_url/requires_openai_auth); may prefer remote ChatGPT history over local-only history",
|
|
376
|
-
}),
|
|
377
651
|
};
|
|
378
652
|
exports.default = SetupCommand;
|