theclawbay 0.3.45 → 0.3.47

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.
@@ -43,7 +43,10 @@ const HISTORY_PROVIDER_NEUTRALIZE_SOURCES = new Set([
43
43
  DEFAULT_PROVIDER_ID,
44
44
  ]);
45
45
  const HISTORY_PROVIDER_DB_MIGRATE_SOURCES = [DEFAULT_PROVIDER_ID, WAN_PROVIDER_ID];
46
- const THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
46
+ const LEGACY_THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
47
+ const CANONICAL_THECLAWBAY_OPENAI_PROXY_SUFFIX = "/v1";
48
+ const THECLAWBAY_CANONICAL_API_HOST = "api.theclawbay.com";
49
+ const THECLAWBAY_WEBSITE_HOSTS = new Set(["theclawbay.com", "www.theclawbay.com"]);
47
50
  const SUPPORTED_MODEL_IDS = new Set((0, supported_models_1.getSupportedModelIds)());
48
51
  const CONTINUE_MODEL_NAME = "The Claw Bay";
49
52
  const TRAE_PATCH_MARKER = "theclawbay-trae-patch";
@@ -166,7 +169,28 @@ function objectRecordOr(value, fallback) {
166
169
  return fallback;
167
170
  }
168
171
  function isTheClawBayOpenAiCompatibleBaseUrl(value) {
169
- return typeof value === "string" && value.trim().endsWith(THECLAWBAY_OPENAI_PROXY_SUFFIX);
172
+ if (typeof value !== "string")
173
+ return false;
174
+ const normalized = value.trim();
175
+ if (!normalized)
176
+ return false;
177
+ try {
178
+ const parsed = new URL(normalized);
179
+ const hostname = parsed.hostname.toLowerCase();
180
+ const pathname = parsed.pathname.replace(/\/+$/g, "");
181
+ if (pathname === LEGACY_THECLAWBAY_OPENAI_PROXY_SUFFIX &&
182
+ (THECLAWBAY_WEBSITE_HOSTS.has(hostname) ||
183
+ hostname === "localhost" ||
184
+ hostname === "127.0.0.1" ||
185
+ hostname === "::1")) {
186
+ return true;
187
+ }
188
+ return (hostname === THECLAWBAY_CANONICAL_API_HOST &&
189
+ pathname === CANONICAL_THECLAWBAY_OPENAI_PROXY_SUFFIX);
190
+ }
191
+ catch {
192
+ return false;
193
+ }
170
194
  }
171
195
  function uniqueStrings(values) {
172
196
  const output = [];
@@ -39,6 +39,8 @@ const DEFAULT_ZO_MODEL = DEFAULT_CODEX_MODEL;
39
39
  const DEFAULT_MODELS = [...SUPPORTED_MODEL_IDS];
40
40
  const PREFERRED_MODELS = [...SUPPORTED_MODEL_IDS];
41
41
  const ENV_KEY_NAME = "THECLAWBAY_API_KEY";
42
+ const CLAUDE_ENV_API_KEY_NAME = "ANTHROPIC_API_KEY";
43
+ const CLAUDE_ENV_BASE_URL_NAME = "ANTHROPIC_BASE_URL";
42
44
  const ENV_FILE = node_path_1.default.join(paths_1.theclawbayConfigDir, "env");
43
45
  const CONTINUE_CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".continue", "config.yaml");
44
46
  const CLINE_GLOBAL_STATE_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".cline", "data", "globalState.json");
@@ -60,7 +62,11 @@ const OPENCLAW_PROVIDER_ID = DEFAULT_PROVIDER_ID;
60
62
  const HISTORY_PROVIDER_NEUTRALIZE_SOURCES = new Set(["openai", "theclawbay-wan", DEFAULT_PROVIDER_ID]);
61
63
  const HISTORY_PROVIDER_DB_MIGRATE_SOURCES = ["openai", "theclawbay-wan"];
62
64
  const SETUP_CLIENT_IDS = ["codex", "continue", "cline", "openclaw", "opencode", "kilo", "roo", "trae", "aider", "zo"];
63
- const THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
65
+ const LEGACY_THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
66
+ const CANONICAL_THECLAWBAY_OPENAI_PROXY_SUFFIX = "/v1";
67
+ const CANONICAL_CODEX_NATIVE_PROXY_SUFFIX = "/backend-api/codex";
68
+ const THECLAWBAY_CANONICAL_API_HOST = "api.theclawbay.com";
69
+ const THECLAWBAY_WEBSITE_HOSTS = new Set(["theclawbay.com", "www.theclawbay.com"]);
64
70
  const CONTINUE_MODEL_NAME = "The Claw Bay";
65
71
  const ROO_PROFILE_NAME = "The Claw Bay";
66
72
  const ROO_PROFILE_ID = "theclawbay-openai-compatible";
@@ -303,7 +309,7 @@ async function configureZoByok(params) {
303
309
  throw new Error("Zo was detected, but no active Zo desktop session token could be read. Open Zo, sign in, and rerun setup.");
304
310
  }
305
311
  const desiredName = `${ZO_CONFIG_NAME_PREFIX} ${modelDisplayName(params.model)}`;
306
- const desiredBaseUrl = `${trimTrailingSlash(params.backendUrl)}${THECLAWBAY_OPENAI_PROXY_SUFFIX}`;
312
+ const desiredBaseUrl = openAiCompatibleProxyUrl(params.backendUrl);
307
313
  const body = {
308
314
  provider: "openai-style",
309
315
  format: "openai",
@@ -449,11 +455,51 @@ function shellQuote(value) {
449
455
  function modelDisplayName(modelId) {
450
456
  return MODEL_DISPLAY_NAMES[modelId] ?? modelId;
451
457
  }
458
+ function publicApiOriginForBackendUrl(backendUrl) {
459
+ const trimmed = trimTrailingSlash(backendUrl);
460
+ try {
461
+ const parsed = new URL(trimmed);
462
+ parsed.hash = "";
463
+ parsed.search = "";
464
+ parsed.pathname = "";
465
+ if (THECLAWBAY_WEBSITE_HOSTS.has(parsed.hostname.toLowerCase())) {
466
+ parsed.hostname = THECLAWBAY_CANONICAL_API_HOST;
467
+ }
468
+ return trimTrailingSlash(parsed.toString());
469
+ }
470
+ catch {
471
+ return trimmed;
472
+ }
473
+ }
452
474
  function openAiCompatibleProxyUrl(backendUrl) {
453
- return `${trimTrailingSlash(backendUrl)}${THECLAWBAY_OPENAI_PROXY_SUFFIX}`;
475
+ return `${publicApiOriginForBackendUrl(backendUrl)}${CANONICAL_THECLAWBAY_OPENAI_PROXY_SUFFIX}`;
476
+ }
477
+ function anthropicCompatibleProxyUrl(backendUrl) {
478
+ return `${publicApiOriginForBackendUrl(backendUrl)}/anthropic`;
454
479
  }
455
480
  function isTheClawBayOpenAiCompatibleBaseUrl(value) {
456
- return typeof value === "string" && value.trim().endsWith(THECLAWBAY_OPENAI_PROXY_SUFFIX);
481
+ if (typeof value !== "string")
482
+ return false;
483
+ const normalized = value.trim();
484
+ if (!normalized)
485
+ return false;
486
+ try {
487
+ const parsed = new URL(normalized);
488
+ const hostname = parsed.hostname.toLowerCase();
489
+ const pathname = trimTrailingSlash(parsed.pathname);
490
+ if (pathname === LEGACY_THECLAWBAY_OPENAI_PROXY_SUFFIX &&
491
+ (THECLAWBAY_WEBSITE_HOSTS.has(hostname) ||
492
+ hostname === "localhost" ||
493
+ hostname === "127.0.0.1" ||
494
+ hostname === "::1")) {
495
+ return true;
496
+ }
497
+ return (hostname === THECLAWBAY_CANONICAL_API_HOST &&
498
+ pathname === CANONICAL_THECLAWBAY_OPENAI_PROXY_SUFFIX);
499
+ }
500
+ catch {
501
+ return false;
502
+ }
457
503
  }
458
504
  function uniqueStrings(values) {
459
505
  const output = [];
@@ -913,8 +959,16 @@ async function resolveDeviceLabel(params) {
913
959
  rl.close();
914
960
  }
915
961
  }
962
+ function summarizeModelFetchFailure(detail) {
963
+ const normalized = detail.replace(/\s+/g, " ").trim();
964
+ if (!normalized)
965
+ return "unknown error";
966
+ if (normalized.length <= 140)
967
+ return normalized;
968
+ return `${normalized.slice(0, 137).trimEnd()}...`;
969
+ }
916
970
  async function fetchBackendModelIds(backendUrl, apiKey) {
917
- const url = `${trimTrailingSlash(backendUrl)}/api/codex-auth/v1/proxy/v1/models`;
971
+ const url = `${openAiCompatibleProxyUrl(backendUrl)}/models`;
918
972
  try {
919
973
  const response = await fetch(url, {
920
974
  headers: {
@@ -922,18 +976,95 @@ async function fetchBackendModelIds(backendUrl, apiKey) {
922
976
  Accept: "application/json",
923
977
  "User-Agent": CLI_HTTP_USER_AGENT,
924
978
  },
925
- signal: AbortSignal.timeout(4500),
979
+ signal: AbortSignal.timeout(10000),
926
980
  });
927
- if (!response.ok)
928
- return null;
981
+ if (!response.ok) {
982
+ const responseText = await response.text().catch(() => "");
983
+ let detail = responseText.trim();
984
+ try {
985
+ const parsed = JSON.parse(responseText);
986
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
987
+ detail = parsed.error.trim();
988
+ }
989
+ else if (typeof parsed.code === "string" && parsed.code.trim()) {
990
+ detail = parsed.code.trim();
991
+ }
992
+ }
993
+ catch {
994
+ // Keep the raw response text when the body is not JSON.
995
+ }
996
+ return {
997
+ ids: null,
998
+ failure: `HTTP ${response.status}${detail ? `: ${summarizeModelFetchFailure(detail)}` : ""}`,
999
+ };
1000
+ }
929
1001
  const body = (await response.json());
930
1002
  const ids = (body.data ?? [])
931
1003
  .map((entry) => (typeof entry.id === "string" ? entry.id.trim() : ""))
932
1004
  .filter((id) => id.length > 0);
933
- return ids.length ? ids : null;
1005
+ if (!ids.length) {
1006
+ return {
1007
+ ids: null,
1008
+ failure: "backend returned an empty model list",
1009
+ };
1010
+ }
1011
+ return { ids };
934
1012
  }
935
- catch {
936
- return null;
1013
+ catch (error) {
1014
+ return {
1015
+ ids: null,
1016
+ failure: summarizeModelFetchFailure(error instanceof Error ? error.message : String(error)),
1017
+ };
1018
+ }
1019
+ }
1020
+ async function fetchClaudeModelIds(backendUrl, apiKey) {
1021
+ const url = `${anthropicCompatibleProxyUrl(backendUrl)}/v1/models`;
1022
+ try {
1023
+ const response = await fetch(url, {
1024
+ headers: {
1025
+ "x-api-key": apiKey,
1026
+ Accept: "application/json",
1027
+ "User-Agent": CLI_HTTP_USER_AGENT,
1028
+ },
1029
+ signal: AbortSignal.timeout(10000),
1030
+ });
1031
+ if (!response.ok) {
1032
+ const responseText = await response.text().catch(() => "");
1033
+ let detail = responseText.trim();
1034
+ try {
1035
+ const parsed = JSON.parse(responseText);
1036
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
1037
+ detail = parsed.error.trim();
1038
+ }
1039
+ else if (typeof parsed.code === "string" && parsed.code.trim()) {
1040
+ detail = parsed.code.trim();
1041
+ }
1042
+ }
1043
+ catch {
1044
+ // Keep the raw response text when the body is not JSON.
1045
+ }
1046
+ return {
1047
+ ids: null,
1048
+ failure: `HTTP ${response.status}${detail ? `: ${summarizeModelFetchFailure(detail)}` : ""}`,
1049
+ };
1050
+ }
1051
+ const body = (await response.json());
1052
+ const ids = (body.data ?? [])
1053
+ .map((entry) => (typeof entry.id === "string" ? entry.id.trim() : ""))
1054
+ .filter((id) => id.length > 0);
1055
+ if (!ids.length) {
1056
+ return {
1057
+ ids: null,
1058
+ failure: "backend returned an empty Claude model list",
1059
+ };
1060
+ }
1061
+ return { ids };
1062
+ }
1063
+ catch (error) {
1064
+ return {
1065
+ ids: null,
1066
+ failure: summarizeModelFetchFailure(error instanceof Error ? error.message : String(error)),
1067
+ };
937
1068
  }
938
1069
  }
939
1070
  function kiloStorageCandidates() {
@@ -1042,7 +1173,7 @@ async function detectZoClient() {
1042
1173
  return false;
1043
1174
  }
1044
1175
  async function resolveModels(backendUrl, apiKey) {
1045
- const ids = await fetchBackendModelIds(backendUrl, apiKey);
1176
+ const { ids, failure } = await fetchBackendModelIds(backendUrl, apiKey);
1046
1177
  const available = new Set(ids ?? []);
1047
1178
  let selected = DEFAULT_CODEX_MODEL;
1048
1179
  let note;
@@ -1057,7 +1188,9 @@ async function resolveModels(backendUrl, apiKey) {
1057
1188
  note = "No preferred Codex model advertised by backend; selected first available model.";
1058
1189
  }
1059
1190
  else if (!ids) {
1060
- note = `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`;
1191
+ note = failure
1192
+ ? `Unable to query backend model list (${failure}); defaulted to ${DEFAULT_CODEX_MODEL}.`
1193
+ : `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`;
1061
1194
  }
1062
1195
  const unique = [];
1063
1196
  const pushUnique = (modelId) => {
@@ -1076,6 +1209,20 @@ async function resolveModels(backendUrl, apiKey) {
1076
1209
  note,
1077
1210
  };
1078
1211
  }
1212
+ async function resolveClaudeAccess(backendUrl, apiKey) {
1213
+ const { ids, failure } = await fetchClaudeModelIds(backendUrl, apiKey);
1214
+ if (!ids?.length) {
1215
+ return {
1216
+ enabled: false,
1217
+ models: [],
1218
+ note: failure ? `Claude Code auto-setup skipped (${failure}).` : undefined,
1219
+ };
1220
+ }
1221
+ return {
1222
+ enabled: true,
1223
+ models: ids,
1224
+ };
1225
+ }
1079
1226
  async function writeCodexConfig(params) {
1080
1227
  const configPath = node_path_1.default.join(paths_1.codexDir, "config.toml");
1081
1228
  await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
@@ -1088,18 +1235,19 @@ async function writeCodexConfig(params) {
1088
1235
  if (err.code !== "ENOENT")
1089
1236
  throw error;
1090
1237
  }
1091
- const proxyRoot = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy`;
1238
+ const proxyRoot = publicApiOriginForBackendUrl(params.backendUrl);
1239
+ const nativeCodexBaseUrl = `${proxyRoot}${CANONICAL_CODEX_NATIVE_PROXY_SUFFIX}`;
1092
1240
  let next = existing;
1093
1241
  next = removeManagedBlock(next, MANAGED_START, MANAGED_END);
1094
1242
  next = removeProviderTable(next, DEFAULT_PROVIDER_ID);
1095
1243
  next = upsertFirstKeyLine(next, "model_provider", `"${DEFAULT_PROVIDER_ID}"`);
1096
1244
  next = upsertFirstKeyLine(next, "model", `"${params.model}"`);
1245
+ next = upsertFirstKeyLine(next, "chatgpt_base_url", `"${proxyRoot}"`);
1097
1246
  const managedBlock = appendManagedBlock("", [
1098
1247
  MANAGED_START,
1099
- `chatgpt_base_url = "${proxyRoot}"`,
1100
1248
  `[model_providers.${DEFAULT_PROVIDER_ID}]`,
1101
1249
  'name = "OpenAI"',
1102
- `base_url = "${proxyRoot}/backend-api/codex"`,
1250
+ `base_url = "${nativeCodexBaseUrl}"`,
1103
1251
  'wire_api = "responses"',
1104
1252
  "requires_openai_auth = true",
1105
1253
  "supports_websockets = true",
@@ -1250,14 +1398,17 @@ async function writeAiderConfig(params) {
1250
1398
  await promises_1.default.writeFile(AIDER_CONFIG_PATH, next, "utf8");
1251
1399
  return AIDER_CONFIG_PATH;
1252
1400
  }
1253
- async function persistApiKeyEnv(apiKey) {
1401
+ async function persistApiKeyEnv(params) {
1254
1402
  const envDir = node_path_1.default.dirname(ENV_FILE);
1255
1403
  await promises_1.default.mkdir(envDir, { recursive: true });
1256
- const envContents = [
1404
+ const envLines = [
1257
1405
  "# Generated by theclawbay setup",
1258
- `export ${ENV_KEY_NAME}=${shellQuote(apiKey)}`,
1259
- "",
1260
- ].join("\n");
1406
+ `export ${ENV_KEY_NAME}=${shellQuote(params.apiKey)}`,
1407
+ ];
1408
+ if (params.claudeEnabled) {
1409
+ envLines.push("", "# Official Claude Code CLI", `export ${CLAUDE_ENV_API_KEY_NAME}=${shellQuote(params.apiKey)}`, `export ${CLAUDE_ENV_BASE_URL_NAME}=${shellQuote(anthropicCompatibleProxyUrl(params.backendUrl))}`);
1410
+ }
1411
+ const envContents = `${envLines.join("\n")}\n`;
1261
1412
  await promises_1.default.writeFile(ENV_FILE, envContents, "utf8");
1262
1413
  await promises_1.default.chmod(ENV_FILE, 0o600);
1263
1414
  const sourceLine = `[ -f "$HOME/.config/theclawbay/env" ] && . "$HOME/.config/theclawbay/env"`;
@@ -1278,7 +1429,15 @@ async function persistApiKeyEnv(apiKey) {
1278
1429
  await promises_1.default.writeFile(rcPath, withBlock, "utf8");
1279
1430
  updated.push(rcPath);
1280
1431
  }
1281
- process.env[ENV_KEY_NAME] = apiKey;
1432
+ process.env[ENV_KEY_NAME] = params.apiKey;
1433
+ if (params.claudeEnabled) {
1434
+ process.env[CLAUDE_ENV_API_KEY_NAME] = params.apiKey;
1435
+ process.env[CLAUDE_ENV_BASE_URL_NAME] = anthropicCompatibleProxyUrl(params.backendUrl);
1436
+ }
1437
+ else {
1438
+ delete process.env[CLAUDE_ENV_API_KEY_NAME];
1439
+ delete process.env[CLAUDE_ENV_BASE_URL_NAME];
1440
+ }
1282
1441
  return updated;
1283
1442
  }
1284
1443
  async function persistVsCodeServerEnvSource() {
@@ -1334,7 +1493,7 @@ function resolveOpenClawPrimaryModel(params) {
1334
1493
  }
1335
1494
  async function patchOpenClawConfigFile(params) {
1336
1495
  const provider = {
1337
- baseUrl: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
1496
+ baseUrl: openAiCompatibleProxyUrl(params.backendUrl),
1338
1497
  apiKey: params.apiKey,
1339
1498
  api: "openai-responses",
1340
1499
  models: buildOpenClawModels(params.models),
@@ -1647,7 +1806,7 @@ async function writeOpenCodeFamilyConfig(params) {
1647
1806
  }
1648
1807
  const managedOpenAiProvider = objectRecordOr(providerRoot[OPENAI_PROVIDER_ID], {});
1649
1808
  const optionsRoot = objectRecordOr(managedOpenAiProvider.options, {});
1650
- optionsRoot.baseURL = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`;
1809
+ optionsRoot.baseURL = openAiCompatibleProxyUrl(params.backendUrl);
1651
1810
  optionsRoot.apiKey = params.apiKey;
1652
1811
  managedOpenAiProvider.options = optionsRoot;
1653
1812
  managedOpenAiProvider.models = buildOpenCodeModelsObject(params.models);
@@ -1689,7 +1848,7 @@ function traePatchSnippet(params) {
1689
1848
  const patchedModels = params.models.length
1690
1849
  ? params.models
1691
1850
  : [{ id: params.model.trim() || DEFAULT_TRAE_MODEL, name: modelDisplayName(params.model.trim() || DEFAULT_TRAE_MODEL) }];
1692
- const proxyBaseUrl = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`;
1851
+ const proxyBaseUrl = openAiCompatibleProxyUrl(params.backendUrl);
1693
1852
  return `async setOriginModelListMapAndCache(e,t=!0){let{userProfile:r}=this.credentialStore.getState(),i=r?.scope!==v9.BYTEDANCE,n={},o=${JSON.stringify(patchedModels)};Object.keys(e).forEach(t=>{let r=[...(e[t]||[])],s=r[0]&&"object"==typeof r[0]?r[0]:null;if([Mh.Builder,Mh.SoloCoder].includes(t)&&s){let a=o.map((e,l)=>{let o={...s};return o.id=${JSON.stringify(TRAE_PATCH_MARKER)}+"-"+t+"-"+e.id,o.name=e.id,o.display_name=e.name,o.provider="theclawbay",o.icon={dark:"https://theclawbay.com/favicon.ico",light:"https://theclawbay.com/favicon.ico"},o.ak=${JSON.stringify(params.apiKey)},o.base_url=${JSON.stringify(proxyBaseUrl)},o.is_custom_base_url=!0,o.custom_model_id=${JSON.stringify(TRAE_PATCH_MARKER)}+"-"+e.id,o.selectable=!0,o.status=!0,o.is_default=0===l,o.config_source=MK.Personal,o.model_type=s.model_type??"chat_model",o.builder=s.builder??null,o.client_connect=!0,o.multimodal=!1!==s.multimodal,o.tags=Array.isArray(s.tags)?s.tags:[],o.auth_type="number"==typeof s.auth_type?s.auth_type:0,o.region=s.region??null,o.max_turns=s.max_turns??{default:50,max:50},o.context_window_size=s.context_window_size??{max:[128000],default:64000},o.features=s.features??{memory:{enable:!1},cost:{enable:!1,data:{manual_usage:0}},multimodal:{enable:!0},context_windows:{enable:!0,data:{dev_context:64000,max_context:128000,max_context_list:[128000],dev_turns:50,max_turns:50}}},o.commercial_info=s.commercial_info??{manual_usage:0,info:""},o.saas_usage=s.saas_usage??{max:0,default:null},o});n[t]=a;return}!i&&[Mh.SoloCoder,Mh.UIBuilder].includes(t)?n[t]=r?.filter(e=>e.config_source!==MK.Personal)||[]:n[t]=r}),this._modelStore.actions.setOriginModelListMap(n),t&&this._modelStorageService.storeModelListMap(n),this._logService.info("[ModelService.setOriginModelListMapAndCache] officialModels",Object.entries(n).map(([e,t])=>({scene:e,models:(t||[]).map(e=>e.display_name||e.name)})))}`;
1694
1853
  }
1695
1854
  async function patchTraeBundle(params) {
@@ -1895,6 +2054,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1895
2054
  }
1896
2055
  const progress = this.createProgressHandle(!debugOutput);
1897
2056
  let resolved = null;
2057
+ let claudeAccess = null;
1898
2058
  let updatedShellFiles = [];
1899
2059
  let codexConfigPath = null;
1900
2060
  let updatedVsCodeEnvFiles = [];
@@ -1918,6 +2078,8 @@ class SetupCommand extends base_command_1.BaseCommand {
1918
2078
  progress.update("Resolving supported models");
1919
2079
  resolved = await resolveModels(backendUrl, authCredential);
1920
2080
  }
2081
+ progress.update("Checking Claude access");
2082
+ claudeAccess = await resolveClaudeAccess(backendUrl, authCredential);
1921
2083
  progress.update("Saving shared machine config");
1922
2084
  await (0, config_1.writeManagedConfig)({
1923
2085
  backendUrl,
@@ -1926,7 +2088,11 @@ class SetupCommand extends base_command_1.BaseCommand {
1926
2088
  deviceSessionId,
1927
2089
  deviceLabel,
1928
2090
  });
1929
- updatedShellFiles = await persistApiKeyEnv(authCredential);
2091
+ updatedShellFiles = await persistApiKeyEnv({
2092
+ apiKey: authCredential,
2093
+ backendUrl,
2094
+ claudeEnabled: claudeAccess.enabled,
2095
+ });
1930
2096
  if (selectedSetupClients.has("codex")) {
1931
2097
  progress.update("Configuring Codex");
1932
2098
  codexConfigPath = await writeCodexConfig({
@@ -2052,6 +2218,12 @@ class SetupCommand extends base_command_1.BaseCommand {
2052
2218
  const summaryNotes = new Set();
2053
2219
  if (resolved?.note)
2054
2220
  summaryNotes.add(resolved.note);
2221
+ if (claudeAccess?.enabled) {
2222
+ summaryNotes.add("Claude Code: exported ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY for the official client.");
2223
+ }
2224
+ else if (claudeAccess?.note) {
2225
+ summaryNotes.add(claudeAccess.note);
2226
+ }
2055
2227
  if (selectedSetupClients.has("trae")) {
2056
2228
  summaryNotes.add("Trae uses an experimental patch. Restart Trae and pick a The Claw Bay model to test it.");
2057
2229
  }
@@ -2101,6 +2273,12 @@ class SetupCommand extends base_command_1.BaseCommand {
2101
2273
  }
2102
2274
  if (resolved?.note)
2103
2275
  this.log(resolved.note);
2276
+ if (claudeAccess?.enabled) {
2277
+ this.log(`- Claude Code base URL: ${anthropicCompatibleProxyUrl(backendUrl)}`);
2278
+ }
2279
+ else if (claudeAccess?.note) {
2280
+ this.log(`- Claude Code: ${claudeAccess.note}`);
2281
+ }
2104
2282
  this.log(`- Local credential env: ${ENV_FILE}`);
2105
2283
  this.log(`- Shell profiles updated: ${updatedShellFiles.join(", ")}`);
2106
2284
  if (selectedSetupClients.has("codex")) {
@@ -152,8 +152,53 @@ async function exchangeBrowserSetupSession(params) {
152
152
  deviceLabel: exchangeBody.deviceLabel,
153
153
  };
154
154
  }
155
- async function delay(ms) {
156
- await new Promise((resolve) => setTimeout(resolve, ms));
155
+ function createSetupAbortError() {
156
+ const error = new Error("browser setup cancelled");
157
+ error.name = "AbortError";
158
+ return error;
159
+ }
160
+ async function delay(ms, signal) {
161
+ if (signal?.aborted)
162
+ throw createSetupAbortError();
163
+ await new Promise((resolve, reject) => {
164
+ const timer = setTimeout(() => {
165
+ cleanup();
166
+ resolve();
167
+ }, ms);
168
+ const onAbort = () => {
169
+ clearTimeout(timer);
170
+ cleanup();
171
+ reject(createSetupAbortError());
172
+ };
173
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
174
+ signal?.addEventListener("abort", onAbort, { once: true });
175
+ });
176
+ }
177
+ async function withTimeout(promise, timeoutMs, signal) {
178
+ if (signal?.aborted)
179
+ throw createSetupAbortError();
180
+ return await new Promise((resolve, reject) => {
181
+ const timer = setTimeout(() => {
182
+ cleanup();
183
+ reject(new Error("browser setup timed out"));
184
+ }, timeoutMs);
185
+ const onAbort = () => {
186
+ clearTimeout(timer);
187
+ cleanup();
188
+ reject(createSetupAbortError());
189
+ };
190
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
191
+ signal?.addEventListener("abort", onAbort, { once: true });
192
+ promise.then((value) => {
193
+ clearTimeout(timer);
194
+ cleanup();
195
+ resolve(value);
196
+ }, (error) => {
197
+ clearTimeout(timer);
198
+ cleanup();
199
+ reject(error);
200
+ });
201
+ });
157
202
  }
158
203
  async function waitForFirstSuccessfulExchange(promises) {
159
204
  return await new Promise((resolve, reject) => {
@@ -190,6 +235,8 @@ async function createBrowserLinkedDeviceSession(params) {
190
235
  const label = (params.deviceLabel ?? "").trim() || node_os_1.default.hostname() || "This device";
191
236
  const state = node_crypto_1.default.randomUUID();
192
237
  const callbackServer = await createLocalCallbackServer(state);
238
+ const exchangeAbortController = new AbortController();
239
+ const exchangeSignal = exchangeAbortController.signal;
193
240
  try {
194
241
  const startResponse = await fetch(`${params.backendUrl}/api/setup/device-session/start`, {
195
242
  method: "POST",
@@ -215,12 +262,7 @@ async function createBrowserLinkedDeviceSession(params) {
215
262
  let fallbackNoticeShown = false;
216
263
  const timeoutError = new Error("browser setup timed out");
217
264
  const waitForLocalCallbackExchange = async () => {
218
- const callback = await Promise.race([
219
- callbackServer.waitForCallback(),
220
- new Promise((_, reject) => {
221
- setTimeout(() => reject(timeoutError), BROWSER_SETUP_TIMEOUT_MS);
222
- }),
223
- ]);
265
+ const callback = await withTimeout(callbackServer.waitForCallback(), BROWSER_SETUP_TIMEOUT_MS, exchangeSignal);
224
266
  return exchangeBrowserSetupSession({
225
267
  backendUrl: params.backendUrl,
226
268
  sessionId: callback.sessionId,
@@ -230,6 +272,8 @@ async function createBrowserLinkedDeviceSession(params) {
230
272
  };
231
273
  const waitForPolledExchange = async () => {
232
274
  while (Date.now() < deadlineMs) {
275
+ if (exchangeSignal.aborted)
276
+ throw createSetupAbortError();
233
277
  try {
234
278
  return await exchangeBrowserSetupSession({
235
279
  backendUrl: params.backendUrl,
@@ -247,14 +291,21 @@ async function createBrowserLinkedDeviceSession(params) {
247
291
  params.log("Waiting for browser approval. If the browser cannot reach localhost, setup will still finish here after you connect the device.");
248
292
  fallbackNoticeShown = true;
249
293
  }
250
- await delay(BROWSER_SETUP_POLL_INTERVAL_MS);
294
+ await delay(BROWSER_SETUP_POLL_INTERVAL_MS, exchangeSignal);
251
295
  }
252
296
  throw timeoutError;
253
297
  };
254
- const linked = await waitForFirstSuccessfulExchange([
255
- waitForLocalCallbackExchange(),
256
- waitForPolledExchange(),
257
- ]);
298
+ const linked = await (async () => {
299
+ try {
300
+ return await waitForFirstSuccessfulExchange([
301
+ waitForLocalCallbackExchange(),
302
+ waitForPolledExchange(),
303
+ ]);
304
+ }
305
+ finally {
306
+ exchangeAbortController.abort();
307
+ }
308
+ })();
258
309
  return {
259
310
  credential: linked.credential,
260
311
  authType: linked.authType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.3.45",
3
+ "version": "0.3.47",
4
4
  "description": "CLI for connecting Codex, Continue, Cline, OpenClaw, OpenCode, Kilo, Roo Code, Aider, experimental Trae, and experimental Zo to The Claw Bay.",
5
5
  "license": "MIT",
6
6
  "bin": {