@bman654/clodex 2.18.6 → 2.18.7

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
@@ -326,7 +326,7 @@ account's model entitlement.
326
326
 
327
327
  Providers supported: `openai` (API key, platform.openai.com), `openai-oauth` (ChatGPT/Codex plan), and `opencode-go` (OpenCode Go API key). OpenCode Go exposes its Anthropic Messages, Chat Completions, and Responses models; entries whose transport has not been verified against the live endpoint are left out. See [OpenCode Go provider](docs/opencode-go.md).
328
328
 
329
- A **custom OpenAI-compatible server** is any OpenAI-style API, given by its base URL (the part before `/chat/completions`), such as `https://openrouter.ai/api/v1`. `providers add` asks for a name, the base URL and an API key (leave it empty for a local server without auth), lists the server's models to check the connection, and saves the provider as `custom-<name>`. Its models then appear in `clodex models`; once saved as favorites they are addressed as `clodex:custom-<name>:<model>` and can be given a short alias with `clodex models --alias`. The base URL is checked before anything is saved: plain `http://` is accepted only after you confirm it, and only for loopback or private-network addresses, and an `https://` URL whose host is, or resolves to, a loopback (`127.0.0.0/8`, `::1`), private, link-local, unique-local, carrier-grade-NAT or well-known cloud-metadata address is refused. A model's id and name are stored with surrounding whitespace trimmed, and a model whose trimmed id or name still contains a control character is skipped, whether the list is fetched by `providers add` or by `providers refresh-models`, and error text from the server is shown with control characters replaced by spaces. Only the OpenAI-compatible kind is offered in the menu.
329
+ A **custom OpenAI-compatible server** is any OpenAI-style API, given by its base URL (the part before `/chat/completions`), such as `https://openrouter.ai/api/v1`. `providers add` asks for a name, the base URL and an API key (leave it empty for a local server without auth), lists the server's models to check the connection, and saves the provider as `custom-<name>`. Its models then appear in `clodex models`; once saved as favorites they are addressed as `clodex:custom-<name>:<model>` and can be given a short alias with `clodex models --alias`. The base URL is checked before anything is saved and again when its models are refreshed: plain `http://` is accepted only after you confirm it, and only if every address the host resolves to is on this machine, your LAN or a private network such as Tailscale (loopback, private, unique-local IPv6 or carrier-grade-NAT `100.64.0.0/10` addresses); `https://` is accepted on any of those networks as well as the public internet; and a link-local (`169.254.0.0/16`, `fe80::/10`) or well-known cloud-metadata address is refused whether or not HTTP was approved. A server on another machine in your tailnet can be added by its `100.x` address, its MagicDNS name, or the HTTPS name `tailscale serve` gives it. A model's id and name are stored with surrounding whitespace trimmed, and a model whose trimmed id or name still contains a control character is skipped, whether the list is fetched by `providers add` or by `providers refresh-models`, and error text from the server is shown with control characters replaced by spaces. Only the OpenAI-compatible kind is offered in the menu.
330
330
 
331
331
  ### `clodex install-vscode-launcher`
332
332
 
package/dist/cli.js CHANGED
@@ -346,7 +346,7 @@ import { join } from "path";
346
346
  // package.json
347
347
  var package_default = {
348
348
  name: "@bman654/clodex",
349
- version: "2.18.6",
349
+ version: "2.18.7",
350
350
  publishConfig: {
351
351
  access: "public"
352
352
  },
@@ -16009,21 +16009,17 @@ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
16009
16009
  "169.254.169.254",
16010
16010
  "metadata.google.internal",
16011
16011
  "169.254.170.2",
16012
- "fd00:ec2::254"
16012
+ "fd00:ec2::254",
16013
+ "100.100.100.200",
16014
+ // Alibaba Cloud ECS
16015
+ "fd20:ce::254"
16016
+ // Google Cloud IPv6
16013
16017
  ]);
16014
- function isBlockedIp(ipStr, allowInsecureLocal) {
16018
+ var NON_PUBLIC_RANGES = /* @__PURE__ */ new Set(["loopback", "private", "uniqueLocal", "carrierGradeNat"]);
16019
+ function isBlockedIp(ipStr) {
16015
16020
  try {
16016
16021
  const ip = ipaddr.process(ipStr);
16017
- const range = ip.range();
16018
- if (allowInsecureLocal && (range === "loopback" || range === "private")) {
16019
- return false;
16020
- }
16021
- if (range === "loopback") return true;
16022
- if (range === "private") return true;
16023
- if (range === "linkLocal") return true;
16024
- if (range === "uniqueLocal") return true;
16025
- if (range === "carrierGradeNat") return true;
16026
- return false;
16022
+ return ip.range() === "linkLocal" || BLOCKED_HOSTNAMES.has(ip.toString());
16027
16023
  } catch {
16028
16024
  return true;
16029
16025
  }
@@ -16057,10 +16053,10 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16057
16053
  return {
16058
16054
  ok: false,
16059
16055
  error: "Only HTTPS URLs are allowed.",
16060
- hint: "For local or LAN servers (Ollama, LM Studio, vLLM), allow insecure HTTP when prompted."
16056
+ hint: "For a server on this machine, your LAN or Tailscale (Ollama, LM Studio, vLLM), allow insecure HTTP when prompted."
16061
16057
  };
16062
16058
  } else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
16063
- return { ok: false, error: "URL must use https:// or user-approved http:// for local/LAN servers." };
16059
+ return { ok: false, error: "URL must use https:// or user-approved http:// for local, LAN or Tailscale servers." };
16064
16060
  }
16065
16061
  const rawHostname = parsed.hostname.toLowerCase();
16066
16062
  const hostname = rawHostname.replace(/^\[(.*)\]$/, "$1");
@@ -16068,7 +16064,7 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16068
16064
  return {
16069
16065
  ok: false,
16070
16066
  error: "This URL points to a blocked internal/metadata host.",
16071
- hint: "Use a public API endpoint for your provider."
16067
+ hint: "Use your model server's own loopback, LAN, Tailscale or public address."
16072
16068
  };
16073
16069
  }
16074
16070
  const addresses = await resolveHostAddresses(hostname);
@@ -16085,19 +16081,18 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16085
16081
  } catch {
16086
16082
  continue;
16087
16083
  }
16088
- if (isBlockedIp(addr, allowLocal)) {
16084
+ if (isBlockedIp(addr)) {
16089
16085
  return {
16090
16086
  ok: false,
16091
- error: "URL resolves to a private or restricted network address.",
16092
- hint: "Use a public HTTPS endpoint, or explicitly allow insecure HTTP for a trusted local/LAN server."
16087
+ error: "URL resolves to a restricted link-local or cloud metadata address.",
16088
+ hint: "Link-local addresses are not supported. Use the server's loopback, LAN, Tailscale or public address."
16093
16089
  };
16094
16090
  }
16095
16091
  }
16096
16092
  if (parsed.protocol === "http:") {
16097
16093
  const allResolvedAddressesAreLocal = addresses.every((addr) => {
16098
16094
  try {
16099
- const range = ipaddr.process(addr).range();
16100
- return range === "loopback" || range === "private" || range === "uniqueLocal";
16095
+ return NON_PUBLIC_RANGES.has(ipaddr.process(addr).range());
16101
16096
  } catch {
16102
16097
  return false;
16103
16098
  }
@@ -16105,8 +16100,8 @@ async function validateCustomEndpointUrl(rawUrl, opts = {}) {
16105
16100
  if (!allowLocal || !allResolvedAddressesAreLocal) {
16106
16101
  return {
16107
16102
  ok: false,
16108
- error: "HTTP is only allowed for local loopback or private-network addresses.",
16109
- hint: "Use https://, or allow insecure HTTP for a trusted local/LAN server."
16103
+ error: "HTTP is only allowed for addresses on this machine, your LAN or a private network such as Tailscale.",
16104
+ hint: "Use https:// for a server on the public internet."
16110
16105
  };
16111
16106
  }
16112
16107
  }
@@ -16354,9 +16349,9 @@ async function runCustomEndpointAddFlow(onResult) {
16354
16349
  }
16355
16350
  let allowInsecureLocal = false;
16356
16351
  if (/^http:\/\//i.test(String(baseUrl).trim())) {
16357
- p3.log.warn("HTTP is not encrypted. Only use it for a trusted local or LAN server.");
16352
+ p3.log.warn("HTTP is not encrypted. Only use it for a trusted server on this machine, your LAN or Tailscale.");
16358
16353
  const allow = await p3.confirm({
16359
- message: "Allow insecure HTTP for this local/LAN server?",
16354
+ message: "Allow insecure HTTP for this local, LAN or Tailscale server?",
16360
16355
  initialValue: true
16361
16356
  });
16362
16357
  if (p3.isCancel(allow)) {
@@ -16879,8 +16874,9 @@ async function refreshApiListProvider(provider, apiKey) {
16879
16874
  let safeBaseUrl = baseUrl;
16880
16875
  const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
16881
16876
  if (configuredUrl && configuredUrl !== templateDefault) {
16877
+ const customEndpoint = provider.templateId === "custom-openai" || provider.templateId === "custom-anthropic";
16882
16878
  const urlCheck = await validateCustomEndpointUrl(baseUrl, {
16883
- allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
16879
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true || customEndpoint
16884
16880
  });
16885
16881
  if (!urlCheck.ok || !urlCheck.normalizedUrl) {
16886
16882
  return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
@@ -17078,7 +17074,7 @@ async function refreshProviderModels(providerId, apiKey, registry, credentialSna
17078
17074
  }
17079
17075
  } else {
17080
17076
  const template = resolveProviderTemplate(provider);
17081
- const keyOptional = template?.apiKeyOptional === true;
17077
+ const keyOptional = template?.apiKeyOptional === true || isAnonymousProvider(provider);
17082
17078
  const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
17083
17079
  if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
17084
17080
  if (cachedModelCount(cacheProvider) > 0) {