socraticode
Version:
SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.
198 lines • 8.91 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fetch as undiciFetch } from "undici";
/**
* Support facts this module encodes (each verified by running the client
* against a live Qdrant on the real Node majors):
*
* - @qdrant/js-client-rest < 1.19 bundles undici 6, whose Agent the client
* hands to Node's built-in fetch. Node 26's built-in fetch (undici 8)
* renamed the legacy `onError` handler hook, so the v6 Agent fails
* validation and every request dies with
* `UND_ERR_INVALID_ARG: invalid onError method`.
* - Pairing the client's Agent with fetch from the same undici line avoids
* that cross-version handoff. SocratiCode already depends on undici 6 for
* its Ollama transport, so the same pairing keeps client 1.18 working on
* Node 26 without dropping Node 18 support.
* - 1.19+ bundles undici 7 and works on Node 26 without this bridge
* (https://github.com/qdrant/qdrant-js/issues/134, fixed in 1.19).
*
* The bridge below is deliberately narrow: only Qdrant-origin requests that
* carry a per-request dispatcher are routed through undici's fetch, and only
* for the affected Node/client pair. Every other request keeps using the
* process's original fetch implementation.
*
* - The breakage is a property of the *undici pair*, not of a Node version.
* It has now been observed on Node 24 as well as Node 26, so a
* `nodeMajor < 26` cutoff is too narrow to detect it. Two builds reporting
* the identical `process.versions.node` disagree: official Node 24.21.0
* (undici 7.29.1) pairs fine with the client's undici 6 Agent, while a
* distribution rebuild of the same version (e.g. Debian's
* `24.21.0+dfsg+~cs24.13.4`) does not.
* - So the transport is chosen by probing the actual capability rather than
* by matching a Node version table. `nativeFetchSupportsUndiciDispatcher`
* hands a stub dispatcher to the built-in fetch and checks whether the
* handler it passes exposes `onError`. That costs no network round trip
* and stays correct on any Node/undici combination, patched or official.
*/
const QDRANT_CLIENT_PACKAGE = "@qdrant/js-client-rest";
/**
* Version of the installed @qdrant/js-client-rest, or null when it cannot
* be determined.
*
* The package's `exports` map exposes only `.`, so
* `require("@qdrant/js-client-rest/package.json")` throws
* ERR_PACKAGE_PATH_NOT_EXPORTED; instead the entry file is resolved and the
* walk goes upward to the nearest package.json that declares the package's
* own name (the entry sits inside dist/, whose parent directories may hold
* unrelated package.json files, hence the name check).
*/
export function readInstalledQdrantClientVersion() {
let entry;
try {
entry = createRequire(import.meta.url).resolve(QDRANT_CLIENT_PACKAGE);
}
catch {
return null; // not installed / not resolvable from here
}
let dir = path.dirname(entry);
// Walk toward the filesystem root; the package root is at most a few
// levels above the entry file.
for (;;) {
try {
const parsed = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
if (parsed?.name === QDRANT_CLIENT_PACKAGE && typeof parsed.version === "string") {
return parsed.version;
}
}
catch {
// no package.json at this level (or unreadable/malformed) — keep walking
}
const parent = path.dirname(dir);
if (parent === dir)
return null;
dir = parent;
}
}
/**
* Probe whether the built-in fetch can hand a request to an undici 6
* dispatcher.
*
* undici 6's `DispatcherBase.dispatch` catches a throw from the wrapped
* dispatcher and then calls `handler.onError(err)`. Node's built-in fetch
* passes a handler without that method, so the recovery path itself throws
* `InvalidArgumentError: invalid onError method` and the caller only sees
* `TypeError: fetch failed`.
*
* The probe hands fetch a stub dispatcher, records the handler shape it
* receives, and rejects the synthetic request without a network round trip.
* Returns true when built-in fetch supplied an `onError` method (i.e. the pair
* is safe).
*/
export function nativeFetchSupportsUndiciDispatcher(nativeFetch = globalThis.fetch) {
let sawOnError;
const stubDispatcher = {
dispatch(_opts, handler) {
sawOnError = handler?.onError;
throw new Error("SocratiCode dispatcher compatibility probe completed");
},
};
try {
// An unroutable .invalid host guarantees the stub is reached during
// dispatch, before any real connection is attempted; the rejection it
// produces is irrelevant and deliberately ignored.
void Promise.resolve(nativeFetch("http://socraticode-probe.invalid/", {
dispatcher: stubDispatcher,
})).catch(() => undefined);
}
catch {
return false;
}
return typeof sawOnError === "function";
}
/** Select the fetch transport required by a Node/Qdrant-client pair. */
export function qdrantFetchMode(nodeMajor, clientVersion) {
if (!Number.isFinite(nodeMajor) || nodeMajor < 26)
return "native";
if (clientVersion === null)
return "unknown";
// Full semver shape required (prerelease/build tags allowed): a partial
// match like `1.19.not-a-version` is NOT a version the registry could
// have served, so transport selection fails closed rather than guessing.
const numericIdentifier = "(?:0|[1-9]\\d*)";
const prereleaseIdentifier = `(?:${numericIdentifier}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
const match = clientVersion.match(new RegExp(`^(${numericIdentifier})\\.(${numericIdentifier})\\.${numericIdentifier}` +
`(?:-${prereleaseIdentifier}(?:\\.${prereleaseIdentifier})*)?` +
"(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"));
if (!match)
return "unknown";
const major = Number.parseInt(match[1], 10);
const minor = Number.parseInt(match[2], 10);
return major < 1 || (major === 1 && minor < 19) ? "paired-undici" : "native";
}
function requestOrigin(input) {
try {
if (typeof input === "string")
return new URL(input).origin;
if (input instanceof URL)
return input.origin;
return new URL(input.url).origin;
}
catch {
return null;
}
}
/**
* Build a fetch wrapper that pairs Qdrant's undici dispatcher with undici's
* own fetch. Errors propagate unchanged; there is no retry or fallback path.
*/
export function createQdrantFetchBridge(nativeFetch, pairedFetch, qdrantOrigins) {
return (input, init) => {
const dispatcher = init?.dispatcher;
if (dispatcher !== undefined) {
const origin = requestOrigin(input);
if (origin !== null && qdrantOrigins.has(origin)) {
return pairedFetch(input, init);
}
}
return nativeFetch(input, init);
};
}
const bridgedQdrantOrigins = new Set();
let bridgeInstalled = false;
/**
* Install the Qdrant transport bridge once for the configured Qdrant origin.
* Repeated calls only register an additional origin.
*
* The capability probe is consulted first and is authoritative: a runtime
* whose built-in fetch cannot hand a request to the client's undici dispatcher
* needs the bridge whatever its Node version or distribution, and a runtime
* that can needs the native transport. The version table stays as the fallback
* for the (undetectable-probe) case, and the `unknown` mode still refuses
* rather than booting into a client whose first request would die opaquely.
*/
export function ensureQdrantClientCompatibility(qdrantBaseUrl) {
const nativeFetch = globalThis.fetch.bind(globalThis);
const needsBridge = !nativeFetchSupportsUndiciDispatcher(nativeFetch);
const mode = needsBridge
? "paired-undici"
: (() => {
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
return qdrantFetchMode(nodeMajor, readInstalledQdrantClientVersion());
})();
if (mode === "native")
return;
if (mode === "unknown") {
throw new Error(`socraticode: cannot determine the installed @qdrant/js-client-rest version on Node ${process.versions.node}; ` +
"cannot select a safe Qdrant transport.");
}
bridgedQdrantOrigins.add(new URL(qdrantBaseUrl).origin);
if (bridgeInstalled)
return;
globalThis.fetch = createQdrantFetchBridge(nativeFetch, undiciFetch, bridgedQdrantOrigins);
bridgeInstalled = true;
}
//# sourceMappingURL=qdrant-client-compat.js.map