diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 62f2dd980..7976a9e4f 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['4391-dmr-support'] # put your current branch to create a build. Core team only. + branches: ['fix-path-patch'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js b/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js index 2373d7656..f6593702a 100644 --- a/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js +++ b/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js @@ -3,7 +3,8 @@ const fs = require("fs"); const path = require("path"); // Mock fix-path as a noop to prevent SIGSEGV (segfault) -jest.mock("fix-path", () => jest.fn()); +// Returns ESM-style default export for dynamic import() +jest.mock("fix-path", () => ({ default: jest.fn() })); const { FFMPEGWrapper } = require("../../../../utils/WhisperProviders/ffmpeg"); @@ -26,14 +27,14 @@ describeRunner("FFMPEGWrapper", () => { }); it("should find ffmpeg executable", async () => { - const knownPath = ffmpeg.ffmpegPath; + const knownPath = await ffmpeg.ffmpegPath(); expect(knownPath).toBeDefined(); expect(typeof knownPath).toBe("string"); expect(knownPath.length).toBeGreaterThan(0); }); it("should validate ffmpeg executable", async () => { - const knownPath = ffmpeg.ffmpegPath; + const knownPath = await ffmpeg.ffmpegPath(); expect(ffmpeg.isValidFFMPEG(knownPath)).toBe(true); }); @@ -56,7 +57,7 @@ describeRunner("FFMPEGWrapper", () => { const buffer = await response.arrayBuffer(); fs.writeFileSync(inputPath, Buffer.from(buffer)); - const result = ffmpeg.convertAudioToWav(inputPath, outputPath); + const result = await ffmpeg.convertAudioToWav(inputPath, outputPath); expect(result).toBe(true); expect(fs.existsSync(outputPath)).toBe(true); @@ -69,8 +70,8 @@ describeRunner("FFMPEGWrapper", () => { const nonExistentFile = path.resolve(testDir, "non-existent-file.wav"); const outputPath = path.resolve(testDir, "test-output-fail.wav"); - expect(() => { - ffmpeg.convertAudioToWav(nonExistentFile, outputPath) - }).toThrow(`Input file ${nonExistentFile} does not exist.`); + expect(async () => { + return await ffmpeg.convertAudioToWav(nonExistentFile, outputPath); + }).rejects.toThrow(`Input file ${nonExistentFile} does not exist.`); }); }); diff --git a/collector/package.json b/collector/package.json index 745d76859..1252fc266 100644 --- a/collector/package.json +++ b/collector/package.json @@ -39,6 +39,7 @@ "puppeteer": "~21.5.2", "sharp": "^0.33.5", "slugify": "^1.6.6", + "strip-ansi": "^7.1.2", "tesseract.js": "^6.0.0", "url-pattern": "^1.0.3", "uuid": "^9.0.0", @@ -54,7 +55,6 @@ }, "resolutions": { "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } } diff --git a/collector/utils/WhisperProviders/ffmpeg/index.js b/collector/utils/WhisperProviders/ffmpeg/index.js index 5b39b71ea..7e0ed75c9 100644 --- a/collector/utils/WhisperProviders/ffmpeg/index.js +++ b/collector/utils/WhisperProviders/ffmpeg/index.js @@ -1,7 +1,7 @@ const fs = require("fs"); const path = require("path"); const { execSync, spawnSync } = require("child_process"); - +const { patchShellEnvironmentPath } = require("../../shell"); /** * Custom FFMPEG wrapper class for audio file conversion. * Replaces deprecated fluent-ffmpeg package. @@ -27,20 +27,12 @@ class FFMPEGWrapper { * Locates ffmpeg binary. * Uses fix-path on non-Windows platforms to ensure we can find ffmpeg. * - * @returns {string} Path to ffmpeg binary + * @returns {Promise} Path to ffmpeg binary * @throws {Error} */ - get ffmpegPath() { + async ffmpegPath() { if (this._ffmpegPath) return this._ffmpegPath; - - if (process.platform !== "win32") { - try { - const fixPath = require("fix-path"); - fixPath(); - } catch (error) { - this.log("Could not load fix-path, using system PATH"); - } - } + await patchShellEnvironmentPath(); try { const which = process.platform === "win32" ? "where" : "which"; @@ -83,10 +75,10 @@ class FFMPEGWrapper { * * @param {string} inputPath - Input path for audio file (any format supported by ffmpeg) * @param {string} outputPath - Output path for converted file - * @returns {boolean} + * @returns {Promise} * @throws {Error} If ffmpeg binary cannot be found or conversion fails */ - convertAudioToWav(inputPath, outputPath) { + async convertAudioToWav(inputPath, outputPath) { if (!fs.existsSync(inputPath)) throw new Error(`Input file ${inputPath} does not exist.`); const outputDir = path.dirname(outputPath); @@ -95,7 +87,7 @@ class FFMPEGWrapper { this.log(`Converting ${path.basename(inputPath)} to WAV format...`); // Convert to 16k hz mono 32f const result = spawnSync( - this.ffmpegPath, + await this.ffmpegPath(), [ "-i", inputPath, diff --git a/collector/utils/WhisperProviders/localWhisper.js b/collector/utils/WhisperProviders/localWhisper.js index d707735e9..6ee124fec 100644 --- a/collector/utils/WhisperProviders/localWhisper.js +++ b/collector/utils/WhisperProviders/localWhisper.js @@ -71,7 +71,7 @@ class LocalWhisper { fs.mkdirSync(outFolder, { recursive: true }); const outputFile = path.resolve(outFolder, `${v4()}.wav`); - const success = ffmpeg.convertAudioToWav(sourcePath, outputFile); + const success = await ffmpeg.convertAudioToWav(sourcePath, outputFile); if (!success) throw new Error( "[Conversion Failed]: Could not convert file to .wav format!" @@ -136,7 +136,7 @@ class LocalWhisper { progress_callback: (data) => { if (!data.hasOwnProperty("progress")) return; console.log( - `\x1b[34m[Embedding - Downloading Model Files]\x1b[0m ${ + `\x1b[34m[ONNXWhisper - Downloading Model Files]\x1b[0m ${ data.file } ${~~data?.progress}%` ); diff --git a/collector/utils/shell.js b/collector/utils/shell.js new file mode 100644 index 000000000..b2030cbb8 --- /dev/null +++ b/collector/utils/shell.js @@ -0,0 +1,25 @@ +/** + * Patch the shell environment path to ensure the PATH is properly set for the current platform. + * On Docker, we are on Node v18 and cannot support fix-path v5. + * So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5. + * https://github.com/sindresorhus/fix-path/issues/6 + * @returns {Promise<{[key: string]: string}>} - Environment variables from shell + */ +async function patchShellEnvironmentPath() { + try { + if (process.platform === "win32") return process.env; + const { default: fixPath } = await import("fix-path"); + const { default: stripAnsi } = await import("strip-ansi"); + fixPath(); + if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH); + console.log("Shell environment path patched successfully."); + return process.env; + } catch (error) { + console.error("Failed to patch shell environment path:", error); + return process.env; + } +} + +module.exports = { + patchShellEnvironmentPath, +}; diff --git a/collector/yarn.lock b/collector/yarn.lock index 82ec934e6..747e19a3e 100644 --- a/collector/yarn.lock +++ b/collector/yarn.lock @@ -504,6 +504,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + ansi-styles@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -3422,13 +3427,20 @@ string_decoder@~1.1.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1, strip-ansi@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + strip-dirs@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" diff --git a/server/utils/MCP/hypervisor/index.js b/server/utils/MCP/hypervisor/index.js index 7720bb976..07b0b8cdf 100644 --- a/server/utils/MCP/hypervisor/index.js +++ b/server/utils/MCP/hypervisor/index.js @@ -11,6 +11,7 @@ const { const { StreamableHTTPClientTransport, } = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); +const { patchShellEnvironmentPath } = require("../../helpers/shell"); /** * @typedef {'stdio' | 'http' | 'sse'} MCPServerTypes @@ -230,33 +231,6 @@ class MCPHypervisor { this.mcpLoadingResults = {}; } - /** - * Load shell environment for desktop applications. - * MacOS and Linux don't inherit login shell environment. So this function - * fixes the PATH and accessible commands when running AnythingLLM outside of Docker during development on Mac/Linux and in-container (Linux). - * @returns {Promise<{[key: string]: string}>} - Environment variables from shell - */ - async #loadShellEnvironment() { - try { - if (process.platform === "win32") return process.env; - const { default: fixPath } = await import("fix-path"); - const { default: stripAnsi } = await import("strip-ansi"); - fixPath(); - - // Due to node v20 requirement to have a minimum version of fix-path v5, we need to strip ANSI codes manually - // which was the only patch between v4 and v5. Here we just apply manually. - // https://github.com/sindresorhus/fix-path/issues/6 - if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH); - return process.env; - } catch (error) { - console.warn( - "Failed to load shell environment, using process.env:", - error.message - ); - return process.env; - } - } - /** * Build the MCP server environment variables - ensures proper PATH and NODE_PATH * inheritance across all platforms and deployment scenarios. @@ -264,7 +238,7 @@ class MCPHypervisor { * @returns {Promise<{env: { [key: string]: string } | {}}}> - The environment variables */ async #buildMCPServerENV(server) { - const shellEnv = await this.#loadShellEnvironment(); + const shellEnv = await patchShellEnvironmentPath(); let baseEnv = { PATH: shellEnv.PATH || diff --git a/server/utils/helpers/shell.js b/server/utils/helpers/shell.js new file mode 100644 index 000000000..b2030cbb8 --- /dev/null +++ b/server/utils/helpers/shell.js @@ -0,0 +1,25 @@ +/** + * Patch the shell environment path to ensure the PATH is properly set for the current platform. + * On Docker, we are on Node v18 and cannot support fix-path v5. + * So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5. + * https://github.com/sindresorhus/fix-path/issues/6 + * @returns {Promise<{[key: string]: string}>} - Environment variables from shell + */ +async function patchShellEnvironmentPath() { + try { + if (process.platform === "win32") return process.env; + const { default: fixPath } = await import("fix-path"); + const { default: stripAnsi } = await import("strip-ansi"); + fixPath(); + if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH); + console.log("Shell environment path patched successfully."); + return process.env; + } catch (error) { + console.error("Failed to patch shell environment path:", error); + return process.env; + } +} + +module.exports = { + patchShellEnvironmentPath, +};