theclawbay 0.3.45 → 0.3.46

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.
@@ -913,6 +913,14 @@ async function resolveDeviceLabel(params) {
913
913
  rl.close();
914
914
  }
915
915
  }
916
+ function summarizeModelFetchFailure(detail) {
917
+ const normalized = detail.replace(/\s+/g, " ").trim();
918
+ if (!normalized)
919
+ return "unknown error";
920
+ if (normalized.length <= 140)
921
+ return normalized;
922
+ return `${normalized.slice(0, 137).trimEnd()}...`;
923
+ }
916
924
  async function fetchBackendModelIds(backendUrl, apiKey) {
917
925
  const url = `${trimTrailingSlash(backendUrl)}/api/codex-auth/v1/proxy/v1/models`;
918
926
  try {
@@ -922,18 +930,45 @@ async function fetchBackendModelIds(backendUrl, apiKey) {
922
930
  Accept: "application/json",
923
931
  "User-Agent": CLI_HTTP_USER_AGENT,
924
932
  },
925
- signal: AbortSignal.timeout(4500),
933
+ signal: AbortSignal.timeout(10000),
926
934
  });
927
- if (!response.ok)
928
- return null;
935
+ if (!response.ok) {
936
+ const responseText = await response.text().catch(() => "");
937
+ let detail = responseText.trim();
938
+ try {
939
+ const parsed = JSON.parse(responseText);
940
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
941
+ detail = parsed.error.trim();
942
+ }
943
+ else if (typeof parsed.code === "string" && parsed.code.trim()) {
944
+ detail = parsed.code.trim();
945
+ }
946
+ }
947
+ catch {
948
+ // Keep the raw response text when the body is not JSON.
949
+ }
950
+ return {
951
+ ids: null,
952
+ failure: `HTTP ${response.status}${detail ? `: ${summarizeModelFetchFailure(detail)}` : ""}`,
953
+ };
954
+ }
929
955
  const body = (await response.json());
930
956
  const ids = (body.data ?? [])
931
957
  .map((entry) => (typeof entry.id === "string" ? entry.id.trim() : ""))
932
958
  .filter((id) => id.length > 0);
933
- return ids.length ? ids : null;
959
+ if (!ids.length) {
960
+ return {
961
+ ids: null,
962
+ failure: "backend returned an empty model list",
963
+ };
964
+ }
965
+ return { ids };
934
966
  }
935
- catch {
936
- return null;
967
+ catch (error) {
968
+ return {
969
+ ids: null,
970
+ failure: summarizeModelFetchFailure(error instanceof Error ? error.message : String(error)),
971
+ };
937
972
  }
938
973
  }
939
974
  function kiloStorageCandidates() {
@@ -1042,7 +1077,7 @@ async function detectZoClient() {
1042
1077
  return false;
1043
1078
  }
1044
1079
  async function resolveModels(backendUrl, apiKey) {
1045
- const ids = await fetchBackendModelIds(backendUrl, apiKey);
1080
+ const { ids, failure } = await fetchBackendModelIds(backendUrl, apiKey);
1046
1081
  const available = new Set(ids ?? []);
1047
1082
  let selected = DEFAULT_CODEX_MODEL;
1048
1083
  let note;
@@ -1057,7 +1092,9 @@ async function resolveModels(backendUrl, apiKey) {
1057
1092
  note = "No preferred Codex model advertised by backend; selected first available model.";
1058
1093
  }
1059
1094
  else if (!ids) {
1060
- note = `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`;
1095
+ note = failure
1096
+ ? `Unable to query backend model list (${failure}); defaulted to ${DEFAULT_CODEX_MODEL}.`
1097
+ : `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`;
1061
1098
  }
1062
1099
  const unique = [];
1063
1100
  const pushUnique = (modelId) => {
@@ -1094,9 +1131,9 @@ async function writeCodexConfig(params) {
1094
1131
  next = removeProviderTable(next, DEFAULT_PROVIDER_ID);
1095
1132
  next = upsertFirstKeyLine(next, "model_provider", `"${DEFAULT_PROVIDER_ID}"`);
1096
1133
  next = upsertFirstKeyLine(next, "model", `"${params.model}"`);
1134
+ next = upsertFirstKeyLine(next, "chatgpt_base_url", `"${proxyRoot}"`);
1097
1135
  const managedBlock = appendManagedBlock("", [
1098
1136
  MANAGED_START,
1099
- `chatgpt_base_url = "${proxyRoot}"`,
1100
1137
  `[model_providers.${DEFAULT_PROVIDER_ID}]`,
1101
1138
  'name = "OpenAI"',
1102
1139
  `base_url = "${proxyRoot}/backend-api/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.46",
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": {