theclawbay 0.3.31 → 0.3.33

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
@@ -18,6 +18,7 @@ theclawbay setup --api-key <apiKey>
18
18
 
19
19
  In an interactive terminal, setup shows one picker for Codex, Continue, Cline, OpenClaw, OpenCode, Kilo, Roo Code, Aider, and Windows Trae.
20
20
  Each row includes a terminal-friendly icon plus the tool's official site, and you can toggle items with arrow keys plus `Enter` or by pressing their number.
21
+ Re-running setup also migrates legacy OpenCode and Kilo patches to the current reasoning-capable provider path and refreshes OpenClaw model metadata.
21
22
 
22
23
  ## Optional
23
24
 
@@ -13,6 +13,7 @@ const codex_auth_seeding_1 = require("../lib/codex-auth-seeding");
13
13
  const codex_history_migration_1 = require("../lib/codex-history-migration");
14
14
  const codex_model_cache_migration_1 = require("../lib/codex-model-cache-migration");
15
15
  const paths_1 = require("../lib/config/paths");
16
+ const supported_models_1 = require("../lib/supported-models");
16
17
  const OPENAI_PROVIDER_ID = "openai";
17
18
  const DEFAULT_PROVIDER_ID = "theclawbay";
18
19
  const WAN_PROVIDER_ID = "theclawbay-wan";
@@ -29,6 +30,8 @@ const CLINE_GLOBAL_STATE_PATH = node_path_1.default.join(node_os_1.default.homed
29
30
  const CLINE_SECRETS_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".cline", "data", "secrets.json");
30
31
  const AIDER_CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".aider.conf.yml");
31
32
  const CLINE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "cline.restore.json");
33
+ const OPENCODE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "opencode.restore.json");
34
+ const KILO_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "kilo.restore.json");
32
35
  const ROO_SETTINGS_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "roo-settings.restore.json");
33
36
  const ROO_IMPORT_FILE = node_path_1.default.join(paths_1.theclawbayConfigDir, "roo-code-settings.json");
34
37
  const MIGRATION_STATE_FILE = node_path_1.default.join(paths_1.codexDir, "theclawbay.migration.json");
@@ -39,6 +42,7 @@ const HISTORY_PROVIDER_NEUTRALIZE_SOURCES = new Set([
39
42
  ]);
40
43
  const HISTORY_PROVIDER_DB_MIGRATE_SOURCES = [DEFAULT_PROVIDER_ID, WAN_PROVIDER_ID];
41
44
  const THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
45
+ const SUPPORTED_MODEL_IDS = new Set((0, supported_models_1.getSupportedModelIds)());
42
46
  const CONTINUE_MODEL_NAME = "The Claw Bay";
43
47
  const TRAE_PATCH_MARKER = "theclawbay-trae-patch";
44
48
  const TRAE_BUNDLE_BACKUP_SUFFIX = ".theclawbay-managed-backup";
@@ -179,6 +183,18 @@ async function writeJsonFile(filePath, value, mode) {
179
183
  if (mode !== undefined)
180
184
  await promises_1.default.chmod(filePath, mode);
181
185
  }
186
+ function isManagedOpenCodeModel(value) {
187
+ if (typeof value !== "string")
188
+ return false;
189
+ if (!value.startsWith(`${OPENAI_PROVIDER_ID}/`))
190
+ return false;
191
+ return SUPPORTED_MODEL_IDS.has(value.slice(`${OPENAI_PROVIDER_ID}/`.length));
192
+ }
193
+ function isManagedOpenCodeProvider(value) {
194
+ const provider = objectRecordOr(value, {});
195
+ const options = objectRecordOr(provider.options, {});
196
+ return isTheClawBayOpenAiCompatibleBaseUrl(options.baseURL);
197
+ }
182
198
  function roamingAppDataDir() {
183
199
  if (process.env.APPDATA?.trim())
184
200
  return process.env.APPDATA;
@@ -433,11 +449,11 @@ async function cleanupOpenClawConfig() {
433
449
  await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
434
450
  return true;
435
451
  }
436
- async function cleanupOpenCodeConfig() {
437
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json");
438
- const existingRaw = await readFileIfExists(configPath);
439
- if (existingRaw === null || !existingRaw.trim())
440
- return false;
452
+ async function cleanupOpenCodeFamilyConfig(params) {
453
+ const existingRaw = await readFileIfExists(params.configPath);
454
+ if (existingRaw === null || !existingRaw.trim()) {
455
+ return removeFileIfExists(params.restoreStatePath);
456
+ }
441
457
  let doc;
442
458
  try {
443
459
  doc = objectRecordOr(JSON.parse(existingRaw), {});
@@ -445,56 +461,76 @@ async function cleanupOpenCodeConfig() {
445
461
  catch {
446
462
  return false;
447
463
  }
464
+ const snapshotRaw = await readFileIfExists(params.restoreStatePath);
448
465
  let changed = false;
449
466
  const providerRoot = objectRecordOr(doc.provider, {});
450
- for (const id of [DEFAULT_PROVIDER_ID]) {
451
- if (id in providerRoot) {
452
- delete providerRoot[id];
453
- changed = true;
454
- }
455
- }
456
- doc.provider = providerRoot;
457
- const model = doc.model;
458
- if (typeof model === "string" && model.startsWith(`${DEFAULT_PROVIDER_ID}/`)) {
459
- delete doc.model;
467
+ if (DEFAULT_PROVIDER_ID in providerRoot) {
468
+ delete providerRoot[DEFAULT_PROVIDER_ID];
460
469
  changed = true;
461
470
  }
462
- if (!changed)
463
- return false;
464
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
465
- return true;
466
- }
467
- async function cleanupKiloConfig() {
468
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "kilo", "opencode.json");
469
- const existingRaw = await readFileIfExists(configPath);
470
- if (existingRaw === null || !existingRaw.trim())
471
- return false;
472
- let doc;
473
- try {
474
- doc = objectRecordOr(JSON.parse(existingRaw), {});
475
- }
476
- catch {
477
- return false;
478
- }
479
- let changed = false;
480
- const providerRoot = objectRecordOr(doc.provider, {});
481
- for (const id of [DEFAULT_PROVIDER_ID]) {
482
- if (id in providerRoot) {
483
- delete providerRoot[id];
471
+ if (snapshotRaw?.trim()) {
472
+ try {
473
+ const snapshot = JSON.parse(snapshotRaw);
474
+ if (snapshot.openAiProvider) {
475
+ providerRoot[OPENAI_PROVIDER_ID] = snapshot.openAiProvider;
476
+ }
477
+ else if (OPENAI_PROVIDER_ID in providerRoot) {
478
+ delete providerRoot[OPENAI_PROVIDER_ID];
479
+ }
480
+ doc.provider = providerRoot;
481
+ if (snapshot.model === null) {
482
+ if (isManagedOpenCodeModel(doc.model) || typeof doc.model === "string" && doc.model.startsWith(`${DEFAULT_PROVIDER_ID}/`)) {
483
+ delete doc.model;
484
+ }
485
+ }
486
+ else {
487
+ doc.model = snapshot.model;
488
+ }
489
+ if (snapshot.schema === null) {
490
+ if (typeof doc.$schema === "string")
491
+ delete doc.$schema;
492
+ }
493
+ else {
494
+ doc.$schema = snapshot.schema;
495
+ }
484
496
  changed = true;
497
+ await promises_1.default.writeFile(params.configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
498
+ await removeFileIfExists(params.restoreStatePath);
499
+ return changed;
500
+ }
501
+ catch {
502
+ // Fall through to best-effort cleanup below.
485
503
  }
486
504
  }
487
505
  doc.provider = providerRoot;
488
506
  const model = doc.model;
489
- if (typeof model === "string" && model.startsWith(`${DEFAULT_PROVIDER_ID}/`)) {
507
+ if (typeof model === "string" && (model.startsWith(`${DEFAULT_PROVIDER_ID}/`) || isManagedOpenCodeModel(model))) {
490
508
  delete doc.model;
491
509
  changed = true;
492
510
  }
511
+ if (isManagedOpenCodeProvider(providerRoot[OPENAI_PROVIDER_ID])) {
512
+ delete providerRoot[OPENAI_PROVIDER_ID];
513
+ doc.provider = providerRoot;
514
+ changed = true;
515
+ }
493
516
  if (!changed)
494
517
  return false;
495
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
518
+ await promises_1.default.writeFile(params.configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
519
+ await removeFileIfExists(params.restoreStatePath);
496
520
  return true;
497
521
  }
522
+ async function cleanupOpenCodeConfig() {
523
+ return cleanupOpenCodeFamilyConfig({
524
+ configPath: node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json"),
525
+ restoreStatePath: OPENCODE_RESTORE_STATE_PATH,
526
+ });
527
+ }
528
+ async function cleanupKiloConfig() {
529
+ return cleanupOpenCodeFamilyConfig({
530
+ configPath: node_path_1.default.join(node_os_1.default.homedir(), ".config", "kilo", "opencode.json"),
531
+ restoreStatePath: KILO_RESTORE_STATE_PATH,
532
+ });
533
+ }
498
534
  async function cleanupTraeBundle() {
499
535
  const bundlePath = traeBundlePath();
500
536
  if (!bundlePath)
@@ -678,10 +714,10 @@ class LogoutCommand extends base_command_1.BaseCommand {
678
714
  this.log("- Codex login state: no cleanup needed.");
679
715
  }
680
716
  if (modelCacheCleanup.action === "removed") {
681
- this.log("- Codex model cache: removed the Claw Bay GPT-5.4 seed.");
717
+ this.log("- Codex model cache: removed the Claw Bay GPT-5.4 / GPT-5.4 Mini seed.");
682
718
  }
683
719
  else if (modelCacheCleanup.action === "preserved") {
684
- this.log("- Codex model cache: preserved an existing GPT-5.4 entry.");
720
+ this.log("- Codex model cache: preserved an existing GPT-5.4 / GPT-5.4 Mini entry.");
685
721
  }
686
722
  else if (modelCacheCleanup.warning) {
687
723
  this.log(`- Codex model cache: ${modelCacheCleanup.warning}`);
@@ -42,6 +42,8 @@ const CLINE_GLOBAL_STATE_PATH = node_path_1.default.join(node_os_1.default.homed
42
42
  const CLINE_SECRETS_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".cline", "data", "secrets.json");
43
43
  const AIDER_CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".aider.conf.yml");
44
44
  const CLINE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "cline.restore.json");
45
+ const OPENCODE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "opencode.restore.json");
46
+ const KILO_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "kilo.restore.json");
45
47
  const ROO_SETTINGS_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "roo-settings.restore.json");
46
48
  const ROO_IMPORT_FILE = node_path_1.default.join(paths_1.theclawbayConfigDir, "roo-code-settings.json");
47
49
  const MIGRATION_STATE_FILE = node_path_1.default.join(paths_1.codexDir, "theclawbay.migration.json");
@@ -56,6 +58,9 @@ const HISTORY_PROVIDER_NEUTRALIZE_SOURCES = new Set(["openai", "theclawbay-wan",
56
58
  const HISTORY_PROVIDER_DB_MIGRATE_SOURCES = ["openai", "theclawbay-wan"];
57
59
  const SETUP_CLIENT_IDS = ["codex", "continue", "cline", "openclaw", "opencode", "kilo", "roo", "trae", "aider"];
58
60
  const THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
61
+ const OPENAI_PROVIDER_ID = "openai";
62
+ const OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
63
+ const KILO_CONFIG_SCHEMA_URL = "https://kilo.ai/config.json";
59
64
  const CONTINUE_MODEL_NAME = "The Claw Bay";
60
65
  const ROO_PROFILE_NAME = "The Claw Bay";
61
66
  const ROO_PROFILE_ID = "theclawbay-openai-compatible";
@@ -460,25 +465,51 @@ async function promptForSetupClients(clients) {
460
465
  const wasRaw = stdin.isRaw;
461
466
  const applyIndex = options.length;
462
467
  const hyperlinksEnabled = supportsTerminalHyperlinks();
468
+ const ansi = {
469
+ reset: "\x1b[0m",
470
+ bold: "\x1b[1m",
471
+ dim: "\x1b[2m",
472
+ inverse: "\x1b[7m",
473
+ red: "\x1b[31m",
474
+ green: "\x1b[32m",
475
+ yellow: "\x1b[33m",
476
+ gray: "\x1b[90m",
477
+ };
478
+ const paint = (text, ...codes) => `${codes.join("")}${text}${ansi.reset}`;
463
479
  const clearScreen = () => {
464
480
  stdout.write("\x1b[2J\x1b[H");
465
481
  };
466
482
  const selectedCount = () => options.filter((option) => option.checked).length;
467
483
  const render = () => {
468
484
  clearScreen();
469
- stdout.write("Choose local clients to configure\n");
470
- stdout.write(`Use ↑/↓ to move, Enter to toggle the highlighted integration, or press 1-${options.length} to toggle directly.\n`);
471
- stdout.write("Each tool name links to its official site when your terminal supports it.\n");
472
- stdout.write("Move to Apply setup and press Enter when you're ready.\n\n");
485
+ stdout.write(`${paint("Choose local clients to configure", ansi.bold)}\n`);
486
+ stdout.write(`${paint(`Use ↑/↓ to move, Enter to toggle the highlighted integration, or press 1-${options.length} to toggle directly.`, ansi.dim, ansi.gray)}\n`);
487
+ stdout.write(`${paint("Each tool name links to its official site when your terminal supports it.", ansi.dim, ansi.gray)}\n`);
488
+ stdout.write(`${paint("Move to Apply setup and press Enter when you're ready.", ansi.dim, ansi.gray)}\n\n`);
473
489
  for (const [index, option] of options.entries()) {
474
- const pointer = index === cursor ? ">" : " ";
475
- const mark = option.detected ? (option.checked ? "[x]" : "[ ]") : "[-]";
476
- const badge = option.detected ? (option.recommended ? "recommended" : "optional") : "not detected";
477
- stdout.write(`${pointer} ${index + 1}. ${mark} ${formatSetupClientLabel(option, hyperlinksEnabled)} ${badge}\n`);
490
+ const pointer = index === cursor ? paint(">", ansi.bold) : " ";
491
+ const mark = option.detected
492
+ ? option.checked
493
+ ? paint("[x]", ansi.green, ansi.bold)
494
+ : paint("[ ]", ansi.gray)
495
+ : paint("[-]", ansi.red, ansi.bold);
496
+ const badge = option.detected
497
+ ? option.recommended
498
+ ? paint("recommended", ansi.green)
499
+ : paint("optional", ansi.yellow)
500
+ : paint("not detected", ansi.red);
501
+ const label = option.detected
502
+ ? formatSetupClientLabel(option, hyperlinksEnabled)
503
+ : paint(formatSetupClientLabel(option, hyperlinksEnabled), ansi.dim, ansi.gray);
504
+ stdout.write(`${pointer} ${index + 1}. ${mark} ${label} ${badge}\n`);
478
505
  }
479
506
  const applyPointer = cursor === applyIndex ? ">" : " ";
480
- stdout.write(`${applyPointer} Apply setup with ${selectedCount()} selected integration${selectedCount() === 1 ? "" : "s"}\n`);
481
- stdout.write(`\n${hint}\n`);
507
+ const applyLabel = `Apply setup (${selectedCount()} selected)`;
508
+ const applyStyled = cursor === applyIndex
509
+ ? paint(` ${applyLabel} `, ansi.inverse, ansi.bold, ansi.green)
510
+ : paint(applyLabel, ansi.bold, ansi.green);
511
+ stdout.write(`\n${applyPointer} ${applyStyled}\n`);
512
+ stdout.write(`\n${paint(hint, ansi.dim, ansi.gray)}\n`);
482
513
  };
483
514
  const finish = () => {
484
515
  if (settled)
@@ -992,7 +1023,7 @@ async function patchOpenClawConfigFile(params) {
992
1023
  baseUrl: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
993
1024
  apiKey: params.apiKey,
994
1025
  api: "openai-responses",
995
- models: params.models,
1026
+ models: buildOpenClawModels(params.models),
996
1027
  };
997
1028
  const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".openclaw", "openclaw.json");
998
1029
  await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
@@ -1024,6 +1055,9 @@ async function patchOpenClawConfigFile(params) {
1024
1055
  const defaultsRoot = objectRecordOr(agentsRoot.defaults, {});
1025
1056
  const modelRoot = objectRecordOr(defaultsRoot.model, {});
1026
1057
  modelRoot.primary = resolveOpenClawPrimaryModel(params);
1058
+ if (typeof defaultsRoot.thinkingDefault !== "string") {
1059
+ defaultsRoot.thinkingDefault = "medium";
1060
+ }
1027
1061
  defaultsRoot.model = modelRoot;
1028
1062
  agentsRoot.defaults = defaultsRoot;
1029
1063
  doc.agents = agentsRoot;
@@ -1040,21 +1074,57 @@ function objectRecordOr(value, fallback) {
1040
1074
  }
1041
1075
  return fallback;
1042
1076
  }
1043
- function openCodeModelsObject(models) {
1077
+ function modelContextLimit(modelId) {
1078
+ if (modelId === "gpt-5.4")
1079
+ return 1050000;
1080
+ return 272000;
1081
+ }
1082
+ function modelOutputLimit(modelId) {
1083
+ if (modelId === "gpt-5.4")
1084
+ return 128000;
1085
+ return 65536;
1086
+ }
1087
+ function buildOpenCodeModelConfig(model) {
1088
+ return {
1089
+ name: model.name || model.id,
1090
+ reasoning: true,
1091
+ tool_call: true,
1092
+ attachment: true,
1093
+ modalities: {
1094
+ input: ["text", "image"],
1095
+ output: ["text"],
1096
+ },
1097
+ limit: {
1098
+ context: modelContextLimit(model.id),
1099
+ output: modelOutputLimit(model.id),
1100
+ },
1101
+ };
1102
+ }
1103
+ function buildOpenCodeModelsObject(models) {
1044
1104
  const result = {};
1045
1105
  for (const model of models) {
1046
1106
  if (!model.id)
1047
1107
  continue;
1048
- result[model.id] = { name: model.name || model.id };
1108
+ result[model.id] = buildOpenCodeModelConfig(model);
1049
1109
  }
1050
1110
  return result;
1051
1111
  }
1052
- async function writeOpenCodeConfig(params) {
1053
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json");
1054
- await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
1112
+ function isManagedOpenCodeProvider(provider) {
1113
+ const options = objectRecordOr(provider.options, {});
1114
+ return isTheClawBayOpenAiCompatibleBaseUrl(options.baseURL);
1115
+ }
1116
+ function isManagedOpenCodeModel(value, supportedModelIds) {
1117
+ if (typeof value !== "string")
1118
+ return false;
1119
+ if (!value.startsWith(`${OPENAI_PROVIDER_ID}/`))
1120
+ return false;
1121
+ return supportedModelIds.has(value.slice(`${OPENAI_PROVIDER_ID}/`.length));
1122
+ }
1123
+ async function writeOpenCodeFamilyConfig(params) {
1124
+ await promises_1.default.mkdir(node_path_1.default.dirname(params.configPath), { recursive: true });
1055
1125
  let existingRaw = "";
1056
1126
  try {
1057
- existingRaw = await promises_1.default.readFile(configPath, "utf8");
1127
+ existingRaw = await promises_1.default.readFile(params.configPath, "utf8");
1058
1128
  }
1059
1129
  catch (error) {
1060
1130
  const err = error;
@@ -1067,60 +1137,81 @@ async function writeOpenCodeConfig(params) {
1067
1137
  doc = objectRecordOr(JSON.parse(existingRaw), {});
1068
1138
  }
1069
1139
  catch {
1070
- throw new Error(`invalid JSON in OpenCode config: ${configPath}`);
1140
+ throw new Error(`invalid JSON in config: ${params.configPath}`);
1071
1141
  }
1072
1142
  }
1073
1143
  const providerRoot = objectRecordOr(doc.provider, {});
1074
- providerRoot[DEFAULT_PROVIDER_ID] = {
1075
- npm: "@ai-sdk/openai-compatible",
1076
- name: DEFAULT_PROVIDER_ID,
1077
- options: {
1078
- baseURL: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
1079
- apiKey: params.apiKey,
1080
- },
1081
- models: openCodeModelsObject(params.models),
1082
- };
1144
+ const openAiProvider = objectRecordOr(providerRoot[OPENAI_PROVIDER_ID], {});
1145
+ const supportedModelIds = new Set(params.models.map((entry) => entry.id).filter(Boolean));
1146
+ const alreadyManaged = isManagedOpenCodeProvider(openAiProvider) && isManagedOpenCodeModel(doc.model, supportedModelIds);
1147
+ if (!alreadyManaged) {
1148
+ const snapshot = {
1149
+ openAiProvider: Object.keys(openAiProvider).length > 0 ? openAiProvider : null,
1150
+ model: typeof doc.model === "string" && !doc.model.startsWith(`${DEFAULT_PROVIDER_ID}/`)
1151
+ ? doc.model
1152
+ : null,
1153
+ schema: typeof doc.$schema === "string" ? doc.$schema : null,
1154
+ };
1155
+ await writeJsonObjectFile(params.restoreStatePath, snapshot, 0o600);
1156
+ }
1157
+ const managedOpenAiProvider = objectRecordOr(providerRoot[OPENAI_PROVIDER_ID], {});
1158
+ const optionsRoot = objectRecordOr(managedOpenAiProvider.options, {});
1159
+ optionsRoot.baseURL = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`;
1160
+ optionsRoot.apiKey = params.apiKey;
1161
+ managedOpenAiProvider.options = optionsRoot;
1162
+ managedOpenAiProvider.models = buildOpenCodeModelsObject(params.models);
1163
+ managedOpenAiProvider.whitelist = params.models.map((entry) => entry.id).filter(Boolean);
1164
+ if ("blacklist" in managedOpenAiProvider) {
1165
+ delete managedOpenAiProvider.blacklist;
1166
+ }
1167
+ providerRoot[OPENAI_PROVIDER_ID] = managedOpenAiProvider;
1168
+ if (DEFAULT_PROVIDER_ID in providerRoot) {
1169
+ delete providerRoot[DEFAULT_PROVIDER_ID];
1170
+ }
1171
+ doc.$schema = params.schemaUrl;
1083
1172
  doc.provider = providerRoot;
1084
- doc.model = `${DEFAULT_PROVIDER_ID}/${params.model}`;
1085
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
1086
- return configPath;
1173
+ doc.model = `${OPENAI_PROVIDER_ID}/${params.model}`;
1174
+ await promises_1.default.writeFile(params.configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
1175
+ return params.configPath;
1176
+ }
1177
+ function buildOpenClawModels(models) {
1178
+ const supportedModelMap = new Map((0, supported_models_1.getSupportedModels)().map((model) => [model.id, model]));
1179
+ return models
1180
+ .filter((model) => Boolean(model.id))
1181
+ .map((model) => {
1182
+ const pricing = supportedModelMap.get(model.id);
1183
+ return {
1184
+ id: model.id,
1185
+ name: model.name || model.id,
1186
+ api: "openai-responses",
1187
+ reasoning: true,
1188
+ input: ["text", "image"],
1189
+ cost: {
1190
+ input: pricing?.inputPer1M ?? 0,
1191
+ output: pricing?.outputPer1M ?? 0,
1192
+ cacheRead: pricing?.cachedInputPer1M ?? 0,
1193
+ cacheWrite: pricing?.inputPer1M ?? 0,
1194
+ },
1195
+ contextWindow: modelContextLimit(model.id),
1196
+ maxTokens: modelOutputLimit(model.id),
1197
+ };
1198
+ });
1199
+ }
1200
+ async function writeOpenCodeConfig(params) {
1201
+ return writeOpenCodeFamilyConfig({
1202
+ configPath: node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json"),
1203
+ restoreStatePath: OPENCODE_RESTORE_STATE_PATH,
1204
+ schemaUrl: OPENCODE_CONFIG_SCHEMA_URL,
1205
+ ...params,
1206
+ });
1087
1207
  }
1088
1208
  async function writeKiloConfig(params) {
1089
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "kilo", "opencode.json");
1090
- await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
1091
- let existingRaw = "";
1092
- try {
1093
- existingRaw = await promises_1.default.readFile(configPath, "utf8");
1094
- }
1095
- catch (error) {
1096
- const err = error;
1097
- if (err.code !== "ENOENT")
1098
- throw error;
1099
- }
1100
- let doc = {};
1101
- if (existingRaw.trim()) {
1102
- try {
1103
- doc = objectRecordOr(JSON.parse(existingRaw), {});
1104
- }
1105
- catch {
1106
- throw new Error(`invalid JSON in Kilo config: ${configPath}`);
1107
- }
1108
- }
1109
- const providerRoot = objectRecordOr(doc.provider, {});
1110
- providerRoot[DEFAULT_PROVIDER_ID] = {
1111
- npm: "@ai-sdk/openai-compatible",
1112
- name: DEFAULT_PROVIDER_ID,
1113
- options: {
1114
- baseURL: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
1115
- apiKey: params.apiKey,
1116
- },
1117
- models: openCodeModelsObject(params.models),
1118
- };
1119
- doc.$schema = "https://kilo.ai/config.json";
1120
- doc.provider = providerRoot;
1121
- doc.model = `${DEFAULT_PROVIDER_ID}/${params.model || DEFAULT_KILO_MODEL}`;
1122
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
1123
- return configPath;
1209
+ return writeOpenCodeFamilyConfig({
1210
+ configPath: node_path_1.default.join(node_os_1.default.homedir(), ".config", "kilo", "opencode.json"),
1211
+ restoreStatePath: KILO_RESTORE_STATE_PATH,
1212
+ schemaUrl: KILO_CONFIG_SCHEMA_URL,
1213
+ ...params,
1214
+ });
1124
1215
  }
1125
1216
  function traePatchSnippet(params) {
1126
1217
  const patchedModels = params.models.length
@@ -1467,19 +1558,19 @@ class SetupCommand extends base_command_1.BaseCommand {
1467
1558
  this.log(`- Conversation cache: could not update local Codex history DB.${detail}`);
1468
1559
  }
1469
1560
  if (modelCacheMigration?.action === "seeded") {
1470
- this.log("- Codex model cache: added GPT-5.4 to the local picker cache.");
1561
+ this.log("- Codex model cache: added GPT-5.4 / GPT-5.4 Mini to the local picker cache.");
1471
1562
  }
1472
1563
  else if (modelCacheMigration?.action === "created") {
1473
- this.log("- Codex model cache: created a local picker cache with GPT-5.4.");
1564
+ this.log("- Codex model cache: created a local picker cache with GPT-5.4 / GPT-5.4 Mini.");
1474
1565
  }
1475
1566
  else if (modelCacheMigration?.action === "refreshed") {
1476
- this.log("- Codex model cache: refreshed the local picker cache for GPT-5.4.");
1567
+ this.log("- Codex model cache: refreshed the local picker cache for GPT-5.4 / GPT-5.4 Mini.");
1477
1568
  }
1478
1569
  else if (modelCacheMigration?.action === "already_seeded") {
1479
- this.log("- Codex model cache: GPT-5.4 was already seeded locally.");
1570
+ this.log("- Codex model cache: GPT-5.4 / GPT-5.4 Mini were already seeded locally.");
1480
1571
  }
1481
1572
  else if (modelCacheMigration?.action === "already_present") {
1482
- this.log("- Codex model cache: GPT-5.4 already existed locally.");
1573
+ this.log("- Codex model cache: GPT-5.4 / GPT-5.4 Mini already existed locally.");
1483
1574
  }
1484
1575
  else if (modelCacheMigration?.warning) {
1485
1576
  this.log(`- Codex model cache: ${modelCacheMigration.warning}`);
@@ -14,10 +14,13 @@ const supported_models_1 = require("./supported-models");
14
14
  const MODELS_CACHE_FILE = "models_cache.json";
15
15
  const MODELS_CACHE_STATE_FILE = "theclawbay.models-cache.json";
16
16
  const STATE_DB_FILE_PATTERN = /^state_\d+\.sqlite$/;
17
- const TARGET_MODEL_ID = "gpt-5.4";
17
+ const TARGET_MODEL_IDS = ["gpt-5.4", "gpt-5.4-mini"];
18
+ const TARGET_MODEL_ID_SET = new Set(TARGET_MODEL_IDS);
18
19
  const SEED_MARKER_KEY = "_theclawbay_seeded";
19
- const SEED_MARKER_VALUE = "gpt-5.4";
20
- const TEMPLATE_MODEL_IDS = (0, supported_models_1.getSupportedModelIds)().filter((id) => id !== TARGET_MODEL_ID);
20
+ // Older releases stored the model id as the marker value; accept those so cleanup still works.
21
+ const SEED_MARKER_VALUE = "theclawbay";
22
+ const LEGACY_SEED_MARKER_VALUES = new Set(["gpt-5.4", SEED_MARKER_VALUE, true]);
23
+ const TEMPLATE_MODEL_IDS = (0, supported_models_1.getSupportedModelIds)().filter((id) => !TARGET_MODEL_ID_SET.has(id));
21
24
  const DEFAULT_REASONING_LEVELS = [
22
25
  { effort: "low", description: "Fast responses with lighter reasoning" },
23
26
  { effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
@@ -88,29 +91,66 @@ async function removeFileIfExists(filePath) {
88
91
  throw error;
89
92
  }
90
93
  }
94
+ function normalizePatchFingerprintMap(state) {
95
+ if (!state)
96
+ return {};
97
+ if (state.version === 1) {
98
+ if (!TARGET_MODEL_ID_SET.has(state.modelId))
99
+ return {};
100
+ return state.fingerprint ? { [state.modelId]: state.fingerprint } : {};
101
+ }
102
+ const output = {};
103
+ for (const entry of state.models) {
104
+ if (!TARGET_MODEL_ID_SET.has(entry.modelId))
105
+ continue;
106
+ if (entry.fingerprint)
107
+ output[entry.modelId] = entry.fingerprint;
108
+ }
109
+ return output;
110
+ }
91
111
  async function readPatchState(statePath) {
92
112
  const parsed = await readJsonIfExists(statePath);
93
113
  const obj = objectRecordOr(parsed);
94
114
  if (!obj)
95
- return null;
96
- if (obj.version !== 1)
97
- return null;
98
- if (obj.modelId !== TARGET_MODEL_ID)
99
- return null;
100
- if (typeof obj.fingerprint !== "string" || !obj.fingerprint)
101
- return null;
102
- return {
103
- version: 1,
104
- modelId: TARGET_MODEL_ID,
105
- fingerprint: obj.fingerprint,
106
- };
115
+ return {};
116
+ const version = obj.version;
117
+ if (version === 1) {
118
+ const modelId = typeof obj.modelId === "string" ? obj.modelId : "";
119
+ const fingerprint = typeof obj.fingerprint === "string" ? obj.fingerprint : "";
120
+ if (!modelId || !fingerprint)
121
+ return {};
122
+ return normalizePatchFingerprintMap({ version: 1, modelId, fingerprint });
123
+ }
124
+ if (version === 2) {
125
+ const modelsRaw = obj.models;
126
+ if (!Array.isArray(modelsRaw))
127
+ return {};
128
+ const models = [];
129
+ for (const entry of modelsRaw) {
130
+ const record = objectRecordOr(entry);
131
+ if (!record)
132
+ continue;
133
+ const modelId = typeof record.modelId === "string" ? record.modelId : "";
134
+ const fingerprint = typeof record.fingerprint === "string" ? record.fingerprint : "";
135
+ if (!modelId || !fingerprint)
136
+ continue;
137
+ models.push({ modelId, fingerprint });
138
+ }
139
+ return normalizePatchFingerprintMap({ version: 2, models });
140
+ }
141
+ return {};
107
142
  }
108
- async function writePatchState(statePath, fingerprint) {
143
+ async function writePatchState(statePath, fingerprints) {
109
144
  await promises_1.default.mkdir(node_path_1.default.dirname(statePath), { recursive: true });
145
+ const models = TARGET_MODEL_IDS.flatMap((modelId) => {
146
+ const fingerprint = fingerprints[modelId];
147
+ if (!fingerprint)
148
+ return [];
149
+ return [{ modelId, fingerprint }];
150
+ });
110
151
  const contents = JSON.stringify({
111
- version: 1,
112
- modelId: TARGET_MODEL_ID,
113
- fingerprint,
152
+ version: 2,
153
+ models,
114
154
  }, null, 2);
115
155
  await promises_1.default.writeFile(statePath, `${contents}\n`, "utf8");
116
156
  }
@@ -118,18 +158,23 @@ function findModel(models, slug) {
118
158
  return models.find((entry) => entry.slug === slug) ?? null;
119
159
  }
120
160
  function hasSeedMarker(model) {
121
- return model[SEED_MARKER_KEY] === SEED_MARKER_VALUE;
161
+ return LEGACY_SEED_MARKER_VALUES.has(model[SEED_MARKER_KEY]);
122
162
  }
123
163
  function applySeedMarker(model) {
124
164
  const next = cloneJson(model);
125
165
  next[SEED_MARKER_KEY] = SEED_MARKER_VALUE;
126
166
  return next;
127
167
  }
128
- function normalizeSeedModel(template) {
168
+ function seedDescription(modelId) {
169
+ if (modelId === "gpt-5.4-mini")
170
+ return "Smaller GPT-5.4 variant for quick iterations.";
171
+ return "Latest frontier agentic coding model.";
172
+ }
173
+ function normalizeSeedModel(template, modelId) {
129
174
  const seed = applySeedMarker(template ?? {});
130
- seed.slug = TARGET_MODEL_ID;
131
- seed.display_name = TARGET_MODEL_ID;
132
- seed.description = "Latest frontier agentic coding model.";
175
+ seed.slug = modelId;
176
+ seed.display_name = modelId;
177
+ seed.description = seedDescription(modelId);
133
178
  if (typeof seed.default_reasoning_level !== "string") {
134
179
  seed.default_reasoning_level = "medium";
135
180
  }
@@ -182,13 +227,13 @@ function normalizeSeedModel(template) {
182
227
  }
183
228
  return seed;
184
229
  }
185
- function buildSeedModel(models) {
230
+ function buildSeedModel(models, modelId) {
186
231
  for (const candidate of TEMPLATE_MODEL_IDS) {
187
232
  const template = findModel(models, candidate);
188
233
  if (template)
189
- return normalizeSeedModel(template);
234
+ return normalizeSeedModel(template, modelId);
190
235
  }
191
- return normalizeSeedModel(null);
236
+ return normalizeSeedModel(null, modelId);
192
237
  }
193
238
  function setCacheFreshnessMetadata(doc, clientVersion) {
194
239
  let changed = false;
@@ -342,70 +387,115 @@ async function ensureCodexModelCacheHasGpt54(params) {
342
387
  await removeFileIfExists(statePath);
343
388
  return {
344
389
  action: "skipped",
345
- warning: "Codex models cache was not found, and Codex version could not be inferred for a safe GPT-5.4 seed.",
390
+ warning: "Codex models cache was not found, and Codex version could not be inferred for a safe GPT-5.4 / GPT-5.4 Mini seed.",
346
391
  };
347
392
  }
348
- const seed = buildSeedModel([]);
393
+ const docModels = [];
394
+ const nextState = {};
395
+ for (const modelId of TARGET_MODEL_IDS) {
396
+ const seed = buildSeedModel(docModels, modelId);
397
+ docModels.push(seed);
398
+ nextState[modelId] = fingerprintModel(seed);
399
+ }
349
400
  const doc = {
350
401
  fetched_at: new Date().toISOString(),
351
402
  client_version: clientVersion,
352
- models: [seed],
403
+ models: docModels,
353
404
  };
354
405
  await promises_1.default.mkdir(params.codexHome, { recursive: true });
355
406
  await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
356
- await writePatchState(statePath, fingerprintModel(seed));
407
+ await writePatchState(statePath, nextState);
357
408
  return { action: "created" };
358
409
  }
359
410
  const doc = normalizeCacheDocument(parsed);
360
411
  if (!doc) {
361
412
  return {
362
413
  action: "skipped",
363
- warning: "Codex models cache exists but is not valid JSON in the expected format; skipped GPT-5.4 seed.",
414
+ warning: "Codex models cache exists but is not valid JSON in the expected format; skipped GPT-5.4 / GPT-5.4 Mini seed.",
364
415
  };
365
416
  }
366
417
  const clientVersion = (await inferCodexClientVersion(params.codexHome)) ??
367
418
  (typeof doc.client_version === "string" && doc.client_version ? doc.client_version : null);
368
- const existingModel = findModel(doc.models, TARGET_MODEL_ID);
369
- if (existingModel) {
370
- const currentFingerprint = fingerprintModel(existingModel);
371
- if (existingState && existingState.fingerprint === currentFingerprint && !hasSeedMarker(existingModel)) {
372
- const seededModel = applySeedMarker(existingModel);
373
- doc.models = doc.models.map((entry) => (entry.slug === TARGET_MODEL_ID ? seededModel : entry));
374
- setCacheFreshnessMetadata(doc, clientVersion);
375
- await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
376
- await writePatchState(statePath, fingerprintModel(seededModel));
377
- return { action: "seeded" };
378
- }
379
- if (hasSeedMarker(existingModel)) {
380
- const docChanged = setCacheFreshnessMetadata(doc, clientVersion);
381
- if (!existingState || existingState.fingerprint !== currentFingerprint) {
382
- await writePatchState(statePath, currentFingerprint);
383
- await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
384
- return { action: "refreshed" };
419
+ let changed = false;
420
+ let seeded = false;
421
+ let stateChanged = false;
422
+ const nextState = { ...existingState };
423
+ for (const modelId of TARGET_MODEL_IDS) {
424
+ const existingModel = findModel(doc.models, modelId);
425
+ const trackedFingerprint = existingState[modelId];
426
+ if (existingModel) {
427
+ const currentFingerprint = fingerprintModel(existingModel);
428
+ if (hasSeedMarker(existingModel)) {
429
+ if (!trackedFingerprint || trackedFingerprint !== currentFingerprint) {
430
+ nextState[modelId] = currentFingerprint;
431
+ stateChanged = true;
432
+ }
433
+ continue;
385
434
  }
386
- if (docChanged) {
387
- await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
388
- return { action: "refreshed" };
435
+ if (trackedFingerprint && trackedFingerprint === currentFingerprint) {
436
+ const seededModel = applySeedMarker(existingModel);
437
+ doc.models = doc.models.map((entry) => (entry.slug === modelId ? seededModel : entry));
438
+ nextState[modelId] = fingerprintModel(seededModel);
439
+ seeded = true;
440
+ changed = true;
441
+ stateChanged = true;
442
+ continue;
389
443
  }
390
- return { action: "already_seeded" };
444
+ if (trackedFingerprint && trackedFingerprint !== currentFingerprint) {
445
+ delete nextState[modelId];
446
+ stateChanged = true;
447
+ }
448
+ continue;
391
449
  }
392
- if (existingState && existingState.fingerprint !== currentFingerprint) {
393
- await removeFileIfExists(statePath);
450
+ const seed = buildSeedModel(doc.models, modelId);
451
+ doc.models = [seed, ...doc.models];
452
+ nextState[modelId] = fingerprintModel(seed);
453
+ seeded = true;
454
+ changed = true;
455
+ stateChanged = true;
456
+ }
457
+ // Keep The Claw Bay seeds grouped at the top for easy discovery in the picker.
458
+ const bySlug = new Map();
459
+ const rest = [];
460
+ for (const entry of doc.models) {
461
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
462
+ if (slug && TARGET_MODEL_ID_SET.has(slug) && !bySlug.has(slug)) {
463
+ bySlug.set(slug, entry);
464
+ continue;
394
465
  }
395
- if (setCacheFreshnessMetadata(doc, clientVersion)) {
396
- await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
397
- return { action: "refreshed" };
466
+ rest.push(entry);
467
+ }
468
+ const orderedTargets = [];
469
+ for (const id of TARGET_MODEL_IDS) {
470
+ const entry = bySlug.get(id);
471
+ if (entry)
472
+ orderedTargets.push(entry);
473
+ }
474
+ doc.models = [...orderedTargets, ...rest];
475
+ if (setCacheFreshnessMetadata(doc, clientVersion)) {
476
+ changed = true;
477
+ }
478
+ if (changed) {
479
+ await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
480
+ }
481
+ const hasAnyTracked = TARGET_MODEL_IDS.some((id) => Boolean(nextState[id]));
482
+ if (!hasAnyTracked) {
483
+ if (Object.keys(existingState).length > 0) {
484
+ await removeFileIfExists(statePath);
398
485
  }
399
- return {
400
- action: existingState && existingState.fingerprint === currentFingerprint ? "already_seeded" : "already_present",
401
- };
402
486
  }
403
- const seed = buildSeedModel(doc.models);
404
- doc.models = [seed, ...doc.models];
405
- setCacheFreshnessMetadata(doc, clientVersion);
406
- await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
407
- await writePatchState(statePath, fingerprintModel(seed));
408
- return { action: "seeded" };
487
+ else if (stateChanged || seeded) {
488
+ await writePatchState(statePath, nextState);
489
+ }
490
+ if (seeded)
491
+ return { action: "seeded" };
492
+ if (changed || stateChanged)
493
+ return { action: "refreshed" };
494
+ const allSeeded = TARGET_MODEL_IDS.every((id) => {
495
+ const model = findModel(doc.models, id);
496
+ return Boolean(model && hasSeedMarker(model));
497
+ });
498
+ return { action: allSeeded ? "already_seeded" : "already_present" };
409
499
  }
410
500
  catch (error) {
411
501
  const message = error instanceof Error ? error.message : String(error);
@@ -420,7 +510,7 @@ async function cleanupSeededCodexModelCache(params) {
420
510
  const statePath = node_path_1.default.join(params.codexHome, MODELS_CACHE_STATE_FILE);
421
511
  try {
422
512
  const state = await readPatchState(statePath);
423
- if (!state) {
513
+ if (Object.keys(state).length === 0) {
424
514
  return { action: "none" };
425
515
  }
426
516
  const parsed = await readJsonIfExists(cachePath);
@@ -432,27 +522,26 @@ async function cleanupSeededCodexModelCache(params) {
432
522
  if (!doc) {
433
523
  return {
434
524
  action: "none",
435
- warning: "Codex models cache exists but is not valid JSON in the expected format; left GPT-5.4 cache patch state untouched.",
525
+ warning: "Codex models cache exists but is not valid JSON in the expected format; left GPT-5.4 / GPT-5.4 Mini cache patch state untouched.",
436
526
  };
437
527
  }
438
- const existingModel = findModel(doc.models, TARGET_MODEL_ID);
439
- if (!existingModel) {
440
- await removeFileIfExists(statePath);
441
- return { action: "none" };
442
- }
443
- const currentFingerprint = fingerprintModel(existingModel);
444
- if (!hasSeedMarker(existingModel)) {
445
- await removeFileIfExists(statePath);
446
- return { action: "preserved" };
447
- }
448
- if (currentFingerprint !== state.fingerprint) {
449
- await writePatchState(statePath, currentFingerprint);
450
- doc.models = doc.models.filter((entry) => entry.slug !== TARGET_MODEL_ID);
451
- await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
528
+ let removed = 0;
529
+ let preserved = 0;
530
+ doc.models = doc.models.filter((entry) => {
531
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
532
+ if (!slug || !TARGET_MODEL_ID_SET.has(slug))
533
+ return true;
534
+ if (!hasSeedMarker(entry)) {
535
+ preserved += 1;
536
+ return true;
537
+ }
538
+ removed += 1;
539
+ return false;
540
+ });
541
+ if (removed === 0) {
452
542
  await removeFileIfExists(statePath);
453
- return { action: "removed" };
543
+ return { action: preserved > 0 ? "preserved" : "none" };
454
544
  }
455
- doc.models = doc.models.filter((entry) => entry.slug !== TARGET_MODEL_ID);
456
545
  await promises_1.default.writeFile(cachePath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
457
546
  await removeFileIfExists(statePath);
458
547
  return { action: "removed" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.3.31",
3
+ "version": "0.3.33",
4
4
  "description": "CLI for connecting Codex, Continue, Cline, OpenClaw, OpenCode, Kilo, Roo Code, Aider, and experimental Trae to The Claw Bay.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,6 +8,15 @@
8
8
  "cachedInputPer1M": 0.25,
9
9
  "outputPer1M": 15.0
10
10
  },
11
+ {
12
+ "id": "gpt-5.4-mini",
13
+ "label": "GPT-5.4 Mini",
14
+ "note": "Smaller GPT-5.4 variant for quick iterations.",
15
+ "tone": "sea",
16
+ "inputPer1M": 1.25,
17
+ "cachedInputPer1M": 0.125,
18
+ "outputPer1M": 10.0
19
+ },
11
20
  {
12
21
  "id": "gpt-5.3-codex",
13
22
  "label": "GPT-5.3 Codex",