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 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
- By default, Codex setup preserves local conversation history visibility (uses provider id `openai`, no ChatGPT sync mode).
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
- Explicit client targeting:
37
+ It also configures OpenClaw/OpenCode automatically when those CLIs are installed.
39
38
 
40
- ```sh
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
- If needed, skip the automatic Codex login step:
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
- ```sh
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> --model gpt-5.2-codex
50
+ theclawbay setup --api-key <apiKey> --backend https://your-domain.com
56
51
  ```
57
52
 
58
- If you explicitly want the prior ChatGPT-linked history mode:
53
+ After setup, restart terminal/VS Code once so env-backed clients (OpenClaw/OpenCode) pick up `CODEX_LB_API_KEY`.
59
54
 
60
- ```sh
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
- ```sh
67
- theclawbay setup --api-key <apiKey> --backend https://your-domain.com
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 now auto-detects whether `codex`, `openclaw`, or both are installed and prints setup steps for the detected client(s).
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
 
@@ -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,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
  }
@@ -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 = "openai";
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 PREFERRED_CODEX_MODELS = [
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
- async function askClientChoice() {
46
- const rl = (0, promises_2.createInterface)({
47
- input: process.stdin,
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 end = source.indexOf(MANAGED_END, start);
87
- if (end < 0)
88
- return source.slice(0, start).trimEnd() + "\n";
89
- 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}`;
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]?.trim() !== header) {
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 > 0 ? ids : null;
341
+ return ids.length ? ids : null;
138
342
  }
139
343
  catch {
140
344
  return null;
141
345
  }
142
346
  }
143
- async function resolveCodexModel(params) {
144
- const explicit = params.explicitModel?.trim();
145
- if (explicit && explicit.toLowerCase() !== "auto") {
146
- return { model: explicit };
147
- }
148
- const modelIds = await fetchBackendModelIds(params.backendUrl, params.apiKey);
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
- return { model: preferred };
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: modelIds[0] ?? DEFAULT_CODEX_MODEL,
163
- note: "No preferred codex model advertised by backend; selected first available model.",
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, params.providerId);
182
- next = upsertFirstKeyLine(next, "model_provider", `"${params.providerId}"`);
183
- next = upsertFirstKeyLine(next, "model", `"${params.codexModel}"`);
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.${params.providerId}]`,
405
+ `[model_providers.${DEFAULT_PROVIDER_ID}]`,
187
406
  'name = "OpenAI"',
188
407
  `base_url = "${proxyRoot}/backend-api/codex"`,
189
408
  'wire_api = "responses"',
190
- ...(params.openaiSync
191
- ? [`chatgpt_base_url = "${proxyRoot}"`, "requires_openai_auth = true"]
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
- if (next.trim().length) {
197
- 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);
198
444
  }
199
- next += managedBlock;
200
- await promises_1.default.writeFile(configPath, next, "utf8");
201
- return configPath;
445
+ process.env[ENV_KEY_NAME] = apiKey;
446
+ return updated;
202
447
  }
203
- function codexLoginWithApiKey(apiKey) {
204
- const run = (0, node_child_process_1.spawnSync)("codex", ["login", "--with-api-key"], {
205
- input: `${apiKey}\n`,
206
- encoding: "utf8",
207
- stdio: ["pipe", "pipe", "pipe"],
208
- });
209
- if (run.status === 0)
210
- return;
211
- const stderr = (run.stderr ?? "").trim();
212
- const stdout = (run.stdout ?? "").trim();
213
- const details = stderr || stdout || "codex login returned a non-zero exit status";
214
- 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;
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: `${base}/api/codex-auth/v1/proxy/v1`,
233
- apiKey: params.apiKey,
234
- api: "openai-responses",
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(["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
+ ]);
243
501
  runOpenClawConfigCommand(["config", "set", "models.mode", "merge"]);
244
502
  runOpenClawConfigCommand([
245
503
  "config",
246
504
  "set",
247
- "models.providers.openai",
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
- let codexConfigPath = null;
275
- let codexModel = null;
276
- if (codexWanted) {
277
- codexModel = await resolveCodexModel({
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
- apiKey,
280
- explicitModel: flags.model,
590
+ model: resolved.model,
591
+ models: resolved.models,
281
592
  });
282
- codexConfigPath = await writeCodexConfig({
593
+ }
594
+ if (hasOpenCode) {
595
+ openCodePath = await writeOpenCodeConfig({
283
596
  backendUrl,
284
- providerId: flags.provider.trim() || DEFAULT_PROVIDER_ID,
285
- codexModel: codexModel.model,
286
- openaiSync: flags["openai-sync"],
597
+ model: resolved.model,
598
+ models: resolved.models,
287
599
  });
288
600
  }
289
- if (codexWanted && !flags["skip-login"]) {
290
- if (!hasCommand("codex")) {
291
- throw new Error('codex CLI not found. Install @openai/codex, or run setup with --client openclaw.');
292
- }
293
- codexLoginWithApiKey(apiKey);
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 (openClawWanted) {
296
- if (!hasCommand("openclaw")) {
297
- throw new Error('openclaw CLI not found. Install OpenClaw, or run setup with --client codex.');
298
- }
299
- setupOpenClaw({
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
- this.log(`Linked. Managed config written to ${paths_1.managedConfigPath}`);
306
- this.log(`Backend: ${backendUrl}`);
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
- if (openClawWanted) {
323
- this.log("OpenClaw model provider updated for direct WAN API-key routing.");
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
- if (codexWanted && flags["skip-login"]) {
326
- this.log("Skipped Codex login. Run: printenv OPENAI_API_KEY | codex login --with-api-key");
626
+ else {
627
+ this.log("- OpenClaw: not detected (skipped)");
327
628
  }
328
- else if (codexWanted) {
329
- this.log("Codex API-key login updated.");
629
+ if (hasOpenCode) {
630
+ this.log(`- OpenCode: configured (${openCodePath})`);
330
631
  }
331
- this.log('Done. Users can run directly (no "theclawbay proxy" required in recommended setup).');
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 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";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.2.5",
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": {