theclawbay 0.2.4 → 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 CHANGED
@@ -32,27 +32,17 @@ 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
- By default, Codex setup preserves local conversation history (no ChatGPT history sync mode).
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.
36
36
 
37
- Explicit client targeting:
37
+ It also configures OpenClaw/OpenCode automatically when those CLIs are installed.
38
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
43
- ```
44
-
45
- If needed, skip the automatic Codex login step:
46
-
47
- ```sh
48
- theclawbay setup --api-key <apiKey> --skip-login
49
- ```
39
+ `CODEX_LB_API_KEY` is persisted for restarts in:
50
40
 
51
- If you explicitly want the prior ChatGPT-linked history mode:
52
-
53
- ```sh
54
- theclawbay setup --api-key <apiKey> --openai-sync
55
- ```
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)
56
46
 
57
47
  If you operate a custom backend, pass it explicitly:
58
48
 
@@ -60,6 +50,14 @@ If you operate a custom backend, pass it explicitly:
60
50
  theclawbay setup --api-key <apiKey> --backend https://your-domain.com
61
51
  ```
62
52
 
53
+ After setup, restart terminal/VS Code once so env-backed clients (OpenClaw/OpenCode) pick up `CODEX_LB_API_KEY`.
54
+
55
+ ## Codex Extension Behavior
56
+
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).
60
+
63
61
  ## Run Relay (Optional)
64
62
 
65
63
  Only needed as a fallback compatibility mode:
@@ -73,8 +71,7 @@ By default this starts a local relay on `http://127.0.0.1:2455` and forwards to:
73
71
 
74
72
  - `https://theclawbay.com/api/codex-auth/v1/proxy/...`
75
73
 
76
- The command now auto-detects whether `codex`, `openclaw`, or both are installed and prints setup steps for the detected client(s).
77
- 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.
78
75
 
79
76
  ## Notes
80
77
 
@@ -82,36 +82,70 @@ async function askClientChoice() {
82
82
  output: process.stdout,
83
83
  });
84
84
  try {
85
- const answer = (await rl.question("Which client do you want to configure? [1] Codex [2] OpenClaw [3] Both: "))
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 === "openclaw")
88
+ if (answer === "2" || answer === "codex")
89
+ return "codex";
90
+ if (answer === "3" || answer === "openclaw")
89
91
  return "openclaw";
90
- if (answer === "3" || answer === "both")
92
+ if (answer === "4" || answer === "opencode")
93
+ return "opencode";
94
+ if (answer === "5" || answer === "both")
91
95
  return "both";
92
- return "codex";
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 === "codex" || mode === "openclaw" || mode === "both") {
100
- return mode;
124
+ if (mode === "all") {
125
+ const detected = detectedClientTargets();
126
+ return countEnabledTargets(detected) > 0 ? detected : { codex: true, openclaw: false, opencode: false };
101
127
  }
102
- const codexInstalled = hasCommand("codex");
103
- const openclawInstalled = hasCommand("openclaw");
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
- return "codex";
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 clientChoice = await resolveClientChoice(flags.client);
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 (clientChoice === "codex" || clientChoice === "both") {
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 (clientChoice === "openclaw" || clientChoice === "both") {
226
+ if (targets.openclaw) {
193
227
  const openClawProviderJson = JSON.stringify({
194
228
  baseUrl: `${localBase}/v1`,
195
229
  apiKey: "theclawbay-local",
196
- api: "openai-responses",
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 "openai/gpt-5.3-codex"');
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.openai '${openClawProviderJson}' --json`);
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
  };
@@ -4,11 +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
- "skip-login": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
- "openai-sync": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
7
  };
13
8
  run(): Promise<void>;
14
9
  }
@@ -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,10 +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 = "theclawbay-wan";
17
+ const DEFAULT_PROVIDER_ID = "codex-lb";
18
+ const DEFAULT_CODEX_MODEL = "gpt-5.2-codex";
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 = [
21
+ "gpt-5.3-codex",
22
+ "gpt-5.3-codex-spark",
23
+ "gpt-5.2-codex",
24
+ "gpt-5.1-codex-max",
25
+ "gpt-5.1-codex-mini",
26
+ "gpt-5.1-codex",
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");
18
39
  const MANAGED_START = "# theclawbay-managed:start";
19
40
  const MANAGED_END = "# theclawbay-managed:end";
20
- const DEFAULT_OPENCLAW_MODEL = "gpt-5.3-codex";
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"]);
21
45
  function trimTrailingSlash(value) {
22
46
  return value.replace(/\/+$/g, "");
23
47
  }
@@ -34,58 +58,27 @@ function hasCommand(name) {
34
58
  const result = (0, node_child_process_1.spawnSync)("which", [name], { stdio: "ignore" });
35
59
  return result.status === 0;
36
60
  }
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)
61
+ function removeManagedBlock(source, start, end) {
62
+ const markerStart = source.indexOf(start);
63
+ if (markerStart < 0)
77
64
  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";
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}`;
82
75
  }
83
76
  function removeProviderTable(source, providerId) {
84
77
  const header = `[model_providers.${providerId}]`;
85
78
  const lines = source.split(/\r?\n/);
86
79
  const output = [];
87
80
  for (let i = 0; i < lines.length; i++) {
88
- if (lines[i]?.trim() !== header) {
81
+ if ((lines[i] ?? "").trim() !== header) {
89
82
  output.push(lines[i] ?? "");
90
83
  continue;
91
84
  }
@@ -112,6 +105,283 @@ function upsertFirstKeyLine(source, key, tomlValue) {
112
105
  }
113
106
  return `${`${key} = ${tomlValue}\n${source}`.trimEnd()}\n`;
114
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
+ }
328
+ async function fetchBackendModelIds(backendUrl, apiKey) {
329
+ const url = `${trimTrailingSlash(backendUrl)}/api/codex-auth/v1/proxy/v1/models`;
330
+ try {
331
+ const response = await fetch(url, {
332
+ headers: { Authorization: `Bearer ${apiKey}` },
333
+ signal: AbortSignal.timeout(4500),
334
+ });
335
+ if (!response.ok)
336
+ return null;
337
+ const body = (await response.json());
338
+ const ids = (body.data ?? [])
339
+ .map((entry) => (typeof entry.id === "string" ? entry.id.trim() : ""))
340
+ .filter((id) => id.length > 0);
341
+ return ids.length ? ids : null;
342
+ }
343
+ catch {
344
+ return null;
345
+ }
346
+ }
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) {
353
+ if (available.has(preferred)) {
354
+ selected = preferred;
355
+ break;
356
+ }
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
+ }
379
+ return {
380
+ model: selected,
381
+ models: unique.map((modelId) => ({ id: modelId, name: modelDisplayName(modelId) })),
382
+ note,
383
+ };
384
+ }
115
385
  async function writeCodexConfig(params) {
116
386
  const configPath = node_path_1.default.join(paths_1.codexDir, "config.toml");
117
387
  await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
@@ -126,40 +396,82 @@ async function writeCodexConfig(params) {
126
396
  }
127
397
  const proxyRoot = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy`;
128
398
  let next = existing;
129
- next = removeManagedBlock(next);
130
- next = removeProviderTable(next, params.providerId);
131
- next = upsertFirstKeyLine(next, "model_provider", `"${params.providerId}"`);
132
- 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("", [
133
404
  MANAGED_START,
134
- `[model_providers.${params.providerId}]`,
405
+ `[model_providers.${DEFAULT_PROVIDER_ID}]`,
135
406
  'name = "OpenAI"',
136
407
  `base_url = "${proxyRoot}/backend-api/codex"`,
137
408
  'wire_api = "responses"',
138
- ...(params.openaiSync
139
- ? [`chatgpt_base_url = "${proxyRoot}"`, "requires_openai_auth = true"]
140
- : []),
409
+ "requires_openai_auth = true",
410
+ `experimental_bearer_token = "${params.apiKey}"`,
141
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)}`,
142
423
  "",
143
424
  ].join("\n");
144
- if (next.trim().length) {
145
- next = `${next.trimEnd()}\n\n`;
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);
146
444
  }
147
- next += managedBlock;
148
- await promises_1.default.writeFile(configPath, next, "utf8");
149
- return configPath;
445
+ process.env[ENV_KEY_NAME] = apiKey;
446
+ return updated;
150
447
  }
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}`);
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;
163
475
  }
164
476
  function runOpenClawConfigCommand(args) {
165
477
  const run = (0, node_child_process_1.spawnSync)("openclaw", args, {
@@ -174,29 +486,78 @@ function runOpenClawConfigCommand(args) {
174
486
  throw new Error(`failed to update OpenClaw config: ${details}`);
175
487
  }
176
488
  function setupOpenClaw(params) {
177
- const base = trimTrailingSlash(params.backendUrl);
178
- const model = params.model.trim() || DEFAULT_OPENCLAW_MODEL;
179
489
  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
- ],
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,
189
494
  };
190
- runOpenClawConfigCommand(["config", "set", "agents.defaults.model.primary", `openai/${model}`]);
495
+ runOpenClawConfigCommand([
496
+ "config",
497
+ "set",
498
+ "agents.defaults.model.primary",
499
+ `${OPENCLAW_PROVIDER_ID}/${params.model.trim() || DEFAULT_CODEX_MODEL}`,
500
+ ]);
191
501
  runOpenClawConfigCommand(["config", "set", "models.mode", "merge"]);
192
502
  runOpenClawConfigCommand([
193
503
  "config",
194
504
  "set",
195
- "models.providers.openai",
505
+ `models.providers.${OPENCLAW_PROVIDER_ID}`,
196
506
  JSON.stringify(provider),
197
507
  "--json",
198
508
  ]);
199
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
+ }
200
561
  class SetupCommand extends base_command_1.BaseCommand {
201
562
  async run() {
202
563
  await this.runSafe(async () => {
@@ -210,64 +571,73 @@ class SetupCommand extends base_command_1.BaseCommand {
210
571
  throw error;
211
572
  }
212
573
  const apiKey = (flags["api-key"] ?? managed?.apiKey ?? "").trim();
213
- if (!apiKey) {
574
+ if (!apiKey)
214
575
  throw new Error('API key is required. Run "theclawbay setup --api-key <key>".');
215
- }
216
576
  const backendRaw = flags.backend ?? (0, api_key_1.tryInferBackendUrlFromApiKey)(apiKey) ?? managed?.backendUrl ?? DEFAULT_BACKEND_URL;
217
577
  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
578
  await (0, config_1.writeManagedConfig)({ backendUrl, apiKey });
222
- let codexConfigPath = null;
223
- if (codexWanted) {
224
- codexConfigPath = await writeCodexConfig({
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({
225
589
  backendUrl,
226
- providerId: flags.provider.trim() || DEFAULT_PROVIDER_ID,
227
- openaiSync: flags["openai-sync"],
590
+ model: resolved.model,
591
+ models: resolved.models,
228
592
  });
229
593
  }
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({
594
+ if (hasOpenCode) {
595
+ openCodePath = await writeOpenCodeConfig({
241
596
  backendUrl,
242
- apiKey,
243
- model: flags["openclaw-model"],
597
+ model: resolved.model,
598
+ models: resolved.models,
244
599
  });
245
600
  }
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
- }
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.`);
610
+ }
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.");
256
616
  }
257
- if (openClawWanted) {
258
- this.log("OpenClaw model provider updated for direct WAN API-key routing.");
617
+ if (sessionMigration.retimed > 0) {
618
+ this.log(`- Conversation ordering: repaired timestamps on ${sessionMigration.retimed} local sessions.`);
259
619
  }
260
- if (codexWanted && flags["skip-login"]) {
261
- this.log("Skipped Codex login. Run: printenv OPENAI_API_KEY | codex login --with-api-key");
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");
262
625
  }
263
- else if (codexWanted) {
264
- this.log("Codex API-key login updated.");
626
+ else {
627
+ this.log("- OpenClaw: not detected (skipped)");
265
628
  }
266
- this.log('Done. Users can run directly (no "theclawbay proxy" required in recommended setup).');
629
+ if (hasOpenCode) {
630
+ this.log(`- OpenCode: configured (${openCodePath})`);
631
+ }
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.");
267
637
  });
268
638
  }
269
639
  }
270
- SetupCommand.description = "One-time direct setup for Codex/OpenClaw: link key and route directly to The Claw Bay backend";
640
+ SetupCommand.description = "One-time setup: configure Codex/OpenClaw/OpenCode to route through The Claw Bay using your API key";
271
641
  SetupCommand.flags = {
272
642
  backend: core_1.Flags.string({
273
643
  required: false,
@@ -278,31 +648,5 @@ SetupCommand.flags = {
278
648
  aliases: ["apiKey"],
279
649
  description: "API key issued by your The Claw Bay dashboard",
280
650
  }),
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
651
  };
308
652
  exports.default = SetupCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
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": {