| import path from 'node:path'; |
| import fs from 'node:fs'; |
| import http2 from 'node:http2'; |
| import process from 'node:process'; |
| import { Readable } from 'node:stream'; |
| import { createRequire } from 'node:module'; |
| import { Buffer } from 'node:buffer'; |
| import { promises as dnsPromise } from 'node:dns'; |
| import os from 'node:os'; |
| import crypto from 'node:crypto'; |
| import readline from 'node:readline'; |
|
|
| import yaml from 'yaml'; |
| import { sync as commandExistsSync } from 'command-exists'; |
| import _ from 'lodash'; |
| import yauzl from 'yauzl'; |
| import mime from 'mime-types'; |
| import { default as simpleGit } from 'simple-git'; |
| import chalk from 'chalk'; |
| import bytes from 'bytes'; |
| import { LOG_LEVELS, CHAT_COMPLETION_SOURCES, MEDIA_REQUEST_TYPE } from './constants.js'; |
| import { serverDirectory } from './server-directory.js'; |
| import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| import { isFirefox } from './express-common.js'; |
|
|
| |
| |
| |
| let CACHED_CONFIG = null; |
| let CONFIG_PATH = null; |
|
|
| |
| |
| |
| |
| |
| |
| export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_'); |
|
|
| |
| |
| |
| |
| export function setConfigFilePath(configFilePath) { |
| if (CONFIG_PATH !== null) { |
| console.error(color.red('Config file path already set. Please restart the server to change the config file path.')); |
| } |
| CONFIG_PATH = path.resolve(configFilePath); |
| } |
|
|
| |
| |
| |
| |
| export function getConfig() { |
| if (CONFIG_PATH === null) { |
| console.trace(); |
| console.error(color.red('No config file path set. Please set the config file path using setConfigFilePath().')); |
| process.exit(1); |
| } |
| if (CACHED_CONFIG) { |
| return CACHED_CONFIG; |
| } |
| if (!fs.existsSync(CONFIG_PATH)) { |
| console.error(color.red('No config file found. Please create a config.yaml file. The default config file can be found in the /default folder.')); |
| console.error(color.red('The program will now exit.')); |
| process.exit(1); |
| } |
|
|
| try { |
| const config = yaml.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); |
| CACHED_CONFIG = config; |
| return config; |
| } catch (error) { |
| console.error(color.red('FATAL: Failed to read config.yaml. Please check the file for syntax errors.')); |
| console.error(error.message); |
| process.exit(1); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function getConfigValue(key, defaultValue = null, typeConverter = null) { |
| function _getValue() { |
| const envKey = keyToEnv(key); |
| if (envKey in process.env) { |
| const needsJsonParse = defaultValue && typeof defaultValue === 'object'; |
| const envValue = process.env[envKey]; |
| return needsJsonParse ? (tryParse(envValue) ?? defaultValue) : envValue; |
| } |
| const config = getConfig(); |
| return _.get(config, key, defaultValue); |
| } |
|
|
| const value = _getValue(); |
| switch (typeConverter) { |
| case 'number': |
| return isNaN(parseFloat(value)) ? defaultValue : parseFloat(value); |
| case 'boolean': |
| return toBoolean(value); |
| default: |
| return value; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function setConfigValue(_key, _value) { |
| console.trace(color.yellow('setConfigValue is deprecated and should not be used.')); |
| } |
|
|
| |
| |
| |
| |
| |
| export function getBasicAuthHeader(auth) { |
| const encoded = Buffer.from(`${auth}`).toString('base64'); |
| return `Basic ${encoded}`; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getVersion() { |
| let pkgVersion = 'UNKNOWN'; |
| let gitRevision = null; |
| let gitBranch = null; |
| let commitDate = null; |
| let isLatest = true; |
|
|
| try { |
| const require = createRequire(import.meta.url); |
| const pkgJson = require(path.join(serverDirectory, './package.json')); |
| pkgVersion = pkgJson.version; |
| if (commandExistsSync('git')) { |
| const git = simpleGit({ baseDir: serverDirectory }); |
| gitRevision = await git.revparse(['--short', 'HEAD']); |
| gitBranch = await git.revparse(['--abbrev-ref', 'HEAD']); |
| commitDate = await git.show(['-s', '--format=%ci', gitRevision]); |
|
|
| const trackingBranch = await git.revparse(['--abbrev-ref', '@{u}']); |
|
|
| |
| const localLatest = await git.revparse(['HEAD']); |
| const remoteLatest = await git.revparse([trackingBranch]); |
| isLatest = localLatest === remoteLatest; |
| } |
| } |
| catch { |
| |
| } |
|
|
| const agent = `TavernIntern:${pkgVersion}:Cohee#1207`; |
| return { agent, pkgVersion, gitRevision, gitBranch, commitDate: commitDate?.trim() ?? null, isLatest }; |
| } |
|
|
| |
| |
| |
| |
| |
| export function delay(ms) { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getHexString(length) { |
| const chars = '0123456789abcdef'; |
| let result = ''; |
| for (let i = 0; i < length; i++) { |
| result += chars[Math.floor(Math.random() * chars.length)]; |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| export function formatBytes(numBytes) { |
| return bytes.format(numBytes) ?? ''; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function extractFileFromZipBuffer(archiveBuffer, fileExtension) { |
| return await new Promise((resolve) => { |
| try { |
| yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => { |
| if (err) { |
| console.warn(`Error opening ZIP file: ${err.message}`); |
| return resolve(null); |
| } |
|
|
| zipfile.readEntry(); |
|
|
| zipfile.on('entry', (entry) => { |
| if (entry.fileName.endsWith(fileExtension) && !entry.fileName.startsWith('__MACOSX')) { |
| zipfile.openReadStream(entry, (err, readStream) => { |
| if (err) { |
| console.warn(`Error opening read stream: ${err.message}`); |
| return zipfile.readEntry(); |
| } else { |
| const chunks = []; |
| readStream.on('data', (chunk) => { |
| chunks.push(chunk); |
| }); |
|
|
| readStream.on('end', () => { |
| const buffer = Buffer.concat(chunks); |
| resolve(buffer); |
| zipfile.readEntry(); |
| }); |
|
|
| readStream.on('error', (err) => { |
| console.warn(`Error reading stream: ${err.message}`); |
| zipfile.readEntry(); |
| }); |
| } |
| }); |
| } else { |
| zipfile.readEntry(); |
| } |
| }); |
|
|
| zipfile.on('error', (err) => { |
| console.warn('ZIP processing error', err); |
| resolve(null); |
| }); |
|
|
| zipfile.on('end', () => resolve(null)); |
| }); |
| } catch (error) { |
| console.warn('Failed to process ZIP buffer', error); |
| resolve(null); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function normalizeZipEntryPath(entryName) { |
| if (typeof entryName !== 'string') { |
| return null; |
| } |
|
|
| let normalized = entryName.replace(/\\/g, '/').trim(); |
|
|
| if (!normalized) { |
| return null; |
| } |
|
|
| normalized = normalized.replace(/^\.\/+/g, ''); |
| normalized = path.posix.normalize(normalized); |
|
|
| if (!normalized || normalized === '.' || normalized.startsWith('..')) { |
| return null; |
| } |
|
|
| if (normalized.startsWith('/')) { |
| normalized = normalized.slice(1); |
| } |
|
|
| return normalized; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function extractFilesFromZipBuffer(archiveBuffer, fileNames) { |
| const targets = new Map(); |
|
|
| if (Array.isArray(fileNames)) { |
| for (const fileName of fileNames) { |
| const normalized = normalizeZipEntryPath(fileName); |
| if (normalized && !targets.has(normalized)) { |
| targets.set(normalized, true); |
| } |
| } |
| } |
|
|
| if (targets.size === 0) { |
| return new Map(); |
| } |
|
|
| return await new Promise((resolve) => { |
| const results = new Map(); |
|
|
| try { |
| yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => { |
| if (err) { |
| console.warn(`Error opening ZIP file: ${err.message}`); |
| return resolve(results); |
| } |
|
|
| let finished = false; |
| const finalize = () => { |
| if (finished) { |
| return; |
| } |
| finished = true; |
| resolve(results); |
| }; |
|
|
| zipfile.readEntry(); |
|
|
| zipfile.on('entry', (entry) => { |
| const normalizedEntry = normalizeZipEntryPath(entry.fileName); |
| if (!normalizedEntry || !targets.has(normalizedEntry)) { |
| return zipfile.readEntry(); |
| } |
|
|
| zipfile.openReadStream(entry, (streamErr, readStream) => { |
| if (streamErr) { |
| console.warn(`Error opening read stream: ${streamErr.message}`); |
| return zipfile.readEntry(); |
| } |
|
|
| const chunks = []; |
| readStream.on('data', (chunk) => { |
| chunks.push(chunk); |
| }); |
|
|
| readStream.on('end', () => { |
| results.set(normalizedEntry, Buffer.concat(chunks)); |
| targets.delete(normalizedEntry); |
|
|
| if (targets.size === 0) { |
| finalize(); |
| } else { |
| zipfile.readEntry(); |
| } |
| }); |
|
|
| readStream.on('error', (streamError) => { |
| console.warn(`Error reading stream: ${streamError.message}`); |
| zipfile.readEntry(); |
| }); |
| }); |
| }); |
|
|
| zipfile.on('error', (zipError) => { |
| console.warn('ZIP processing error', zipError); |
| finalize(); |
| }); |
|
|
| zipfile.on('close', () => { |
| finalize(); |
| }); |
|
|
| zipfile.on('end', () => { |
| finalize(); |
| }); |
| }); |
| } catch (error) { |
| console.warn('Failed to process ZIP buffer', error); |
| resolve(results); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function ensureDirectory(dirPath) { |
| try { |
| if (!fs.existsSync(dirPath)) { |
| fs.mkdirSync(dirPath, { recursive: true }); |
| } else if (!fs.statSync(dirPath).isDirectory()) { |
| console.warn(`ensureDirectory: Path ${dirPath} exists and is not a directory.`); |
| return false; |
| } |
| return true; |
| } catch (error) { |
| console.error(`ensureDirectory: Failed to prepare directory ${dirPath}`, error); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getImageBuffers(zipFilePath) { |
| return new Promise((resolve, reject) => { |
| |
| if (!fs.existsSync(zipFilePath)) { |
| reject(new Error('File not found')); |
| return; |
| } |
|
|
| const imageBuffers = []; |
|
|
| yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => { |
| if (err) { |
| reject(err); |
| } else { |
| zipfile.readEntry(); |
| zipfile.on('entry', (entry) => { |
| const mimeType = mime.lookup(entry.fileName); |
| if (mimeType && mimeType.startsWith('image/') && !entry.fileName.startsWith('__MACOSX')) { |
| zipfile.openReadStream(entry, (err, readStream) => { |
| if (err) { |
| reject(err); |
| } else { |
| const chunks = []; |
| readStream.on('data', (chunk) => { |
| chunks.push(chunk); |
| }); |
|
|
| readStream.on('end', () => { |
| imageBuffers.push([path.parse(entry.fileName).base, Buffer.concat(chunks)]); |
| zipfile.readEntry(); |
| }); |
| } |
| }); |
| } else { |
| zipfile.readEntry(); |
| } |
| }); |
|
|
| zipfile.on('end', () => { |
| resolve(imageBuffers); |
| }); |
|
|
| zipfile.on('error', (err) => { |
| reject(err); |
| }); |
| } |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function readAllChunks(readableStream) { |
| return new Promise((resolve, reject) => { |
| |
| const chunks = []; |
| readableStream.on('data', (chunk) => { |
| chunks.push(chunk); |
| }); |
|
|
| readableStream.on('end', () => { |
| |
| resolve(chunks); |
| }); |
|
|
| readableStream.on('error', (error) => { |
| console.error('Error while reading the stream:', error); |
| reject(); |
| }); |
| }); |
| } |
|
|
| function isObject(item) { |
| return (item && typeof item === 'object' && !Array.isArray(item)); |
| } |
|
|
| export function deepMerge(target, source) { |
| let output = Object.assign({}, target); |
| if (isObject(target) && isObject(source)) { |
| Object.keys(source).forEach(key => { |
| if (isObject(source[key])) { |
| if (!(key in target)) { |
| Object.assign(output, { [key]: source[key] }); |
| } else { |
| output[key] = deepMerge(target[key], source[key]); |
| } |
| } else { |
| Object.assign(output, { [key]: source[key] }); |
| } |
| }); |
| } |
| return output; |
| } |
|
|
| export const color = chalk; |
|
|
| |
| |
| |
| |
| export function uuidv4() { |
| |
| if ('crypto' in globalThis && 'randomUUID' in globalThis.crypto) { |
| return globalThis.crypto.randomUUID(); |
| } |
| |
| if ('randomUUID' in crypto) { |
| return crypto.randomUUID(); |
| } |
| |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
| const r = Math.random() * 16 | 0; |
| const v = c === 'x' ? r : (r & 0x3 | 0x8); |
| return v.toString(16); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function humanizedDateTime(timestamp = Date.now()) { |
| const date = new Date(timestamp); |
| const dt = { |
| year: date.getFullYear(), |
| month: date.getMonth() + 1, |
| day: date.getDate(), |
| hour: date.getHours(), |
| minute: date.getMinutes(), |
| second: date.getSeconds(), |
| millisecond: date.getMilliseconds(), |
| }; |
| for (const key in dt) { |
| const padLength = key === 'millisecond' ? 3 : 2; |
| dt[key] = dt[key].toString().padStart(padLength, '0'); |
| } |
| return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`; |
| } |
|
|
| export function tryParse(str) { |
| try { |
| return JSON.parse(str); |
| } catch { |
| return undefined; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function clientRelativePath(root, inputPath) { |
| if (!inputPath.startsWith(root)) { |
| throw new Error('Input path does not start with the root directory'); |
| } |
|
|
| return inputPath.slice(root.length).split(path.sep).join('/'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getUniqueName(name, exists) { |
| let i = 1; |
| let baseName = name; |
| while (exists(name)) { |
| name = `${baseName} (${i})`; |
| i++; |
| } |
| return name; |
| } |
|
|
| |
| |
| |
| |
| |
| export function sanitizeSafeCharacterReplacements(char) { |
| return '_'; |
| } |
|
|
| |
| |
| |
| |
| |
| export function removeFileExtension(filename) { |
| return filename.replace(/\.[^.]+$/, ''); |
| } |
|
|
| export function generateTimestamp() { |
| const now = new Date(); |
| const year = now.getFullYear(); |
| const month = String(now.getMonth() + 1).padStart(2, '0'); |
| const day = String(now.getDate()).padStart(2, '0'); |
| const hours = String(now.getHours()).padStart(2, '0'); |
| const minutes = String(now.getMinutes()).padStart(2, '0'); |
| const seconds = String(now.getSeconds()).padStart(2, '0'); |
|
|
| return `${year}${month}${day}-${hours}${minutes}${seconds}`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function removeOldBackups(directory, prefix, limit = null) { |
| const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50, 'number')); |
|
|
| let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); |
| if (files.length > MAX_BACKUPS) { |
| files = files.map(f => path.join(directory, f)); |
| files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs); |
|
|
| while (files.length > MAX_BACKUPS) { |
| const oldest = files.shift(); |
| if (!oldest) { |
| break; |
| } |
|
|
| fs.unlinkSync(oldest); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function getImages(directoryPath, sortBy = 'name', type = MEDIA_REQUEST_TYPE.IMAGE) { |
| function getSortFunction() { |
| switch (sortBy) { |
| case 'name': |
| return Intl.Collator().compare; |
| case 'date': |
| return (a, b) => fs.statSync(path.join(directoryPath, a)).mtimeMs - fs.statSync(path.join(directoryPath, b)).mtimeMs; |
| default: |
| return (_a, _b) => 0; |
| } |
| } |
|
|
| return fs |
| .readdirSync(directoryPath, { withFileTypes: true }) |
| .filter(dirent => dirent.isFile()) |
| .map(dirent => dirent.name) |
| .filter(file => { |
| const fileType = mime.lookup(file); |
| if (!fileType) { |
| return false; |
| } |
| if ((type & MEDIA_REQUEST_TYPE.IMAGE) && fileType.startsWith('image/')) { |
| return true; |
| } |
| if ((type & MEDIA_REQUEST_TYPE.VIDEO) && fileType.startsWith('video/')) { |
| return true; |
| } |
| if ((type & MEDIA_REQUEST_TYPE.AUDIO) && fileType.startsWith('audio/')) { |
| return true; |
| } |
| return false; |
| }) |
| .sort(getSortFunction()); |
| } |
|
|
| |
| |
| |
| |
| |
| export function forwardFetchResponse(from, to) { |
| let statusCode = from.status; |
| let statusText = from.statusText; |
|
|
| if (!from.ok) { |
| console.warn(`Streaming request failed with status ${statusCode} ${statusText}`); |
| } |
|
|
| |
| |
| |
| |
| |
| if (statusCode === 401) { |
| statusCode = 400; |
| } |
|
|
| to.statusCode = statusCode; |
| to.statusMessage = statusText; |
|
|
| if (from.body && to.socket) { |
| from.body.pipe(to); |
|
|
| to.socket.on('close', function () { |
| if (from.body instanceof Readable) from.body.destroy(); |
|
|
| to.end(); |
| }); |
|
|
| from.body.on('end', function () { |
| console.info('Streaming request finished'); |
| to.end(); |
| }); |
| } else { |
| to.end(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function makeHttp2Request(endpoint, method, body, headers) { |
| return new Promise((resolve, reject) => { |
| try { |
| const url = new URL(endpoint); |
| const client = http2.connect(url.origin); |
|
|
| const req = client.request({ |
| ':method': method, |
| ':path': url.pathname, |
| ...headers, |
| }); |
| req.setEncoding('utf8'); |
|
|
| req.on('response', (headers) => { |
| const status = Number(headers[':status']); |
|
|
| if (status < 200 || status >= 300) { |
| reject(new Error(`Request failed with status ${status}`)); |
| } |
|
|
| let data = ''; |
|
|
| req.on('data', (chunk) => { |
| data += chunk; |
| }); |
|
|
| req.on('end', () => { |
| console.debug(data); |
| resolve(data); |
| }); |
| }); |
|
|
| req.on('error', (err) => { |
| reject(err); |
| }); |
|
|
| if (body) { |
| req.write(body); |
| } |
|
|
| req.end(); |
| } catch (e) { |
| reject(e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function mergeObjectWithYaml(obj, yamlString) { |
| if (!yamlString) { |
| return; |
| } |
|
|
| try { |
| const parsedObject = yaml.parse(yamlString); |
|
|
| if (Array.isArray(parsedObject)) { |
| for (const item of parsedObject) { |
| if (typeof item === 'object' && item && !Array.isArray(item)) { |
| Object.assign(obj, item); |
| } |
| } |
| } |
| else if (parsedObject && typeof parsedObject === 'object') { |
| Object.assign(obj, parsedObject); |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function excludeKeysByYaml(obj, yamlString) { |
| if (!yamlString) { |
| return; |
| } |
|
|
| try { |
| const parsedObject = yaml.parse(yamlString); |
|
|
| if (Array.isArray(parsedObject)) { |
| parsedObject.forEach(key => { |
| delete obj[key]; |
| }); |
| } else if (typeof parsedObject === 'object') { |
| Object.keys(parsedObject).forEach(key => { |
| delete obj[key]; |
| }); |
| } else if (typeof parsedObject === 'string') { |
| delete obj[parsedObject]; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function trimV1(str) { |
| return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, ''); |
| } |
|
|
| |
| |
| |
| |
| |
| export function trimTrailingSlash(str) { |
| return String(str ?? '').replace(/\/$/, ''); |
| } |
|
|
| |
| |
| |
| export class Cache { |
| |
| |
| |
| constructor(ttl) { |
| this.cache = new Map(); |
| this.ttl = ttl; |
| } |
|
|
| |
| |
| |
| |
| get(key) { |
| const value = this.cache.get(key); |
| if (value?.expiry > Date.now()) { |
| return value.value; |
| } |
|
|
| |
| this.cache.delete(key); |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| set(key, value) { |
| this.cache.set(key, { |
| value: value, |
| expiry: Date.now() + this.ttl, |
| }); |
| } |
|
|
| |
| |
| |
| |
| remove(key) { |
| this.cache.delete(key); |
| } |
|
|
| |
| |
| |
| clear() { |
| this.cache.clear(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function removeColorFormatting(text) { |
| |
| return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, ''); |
| } |
|
|
| |
| |
| |
| |
| |
| export function getSeparator(n) { |
| return '='.repeat(n); |
| } |
|
|
| |
| |
| |
| |
| |
| export function isValidUrl(url) { |
| try { |
| new URL(url); |
| return true; |
| } catch (error) { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function urlHostnameToIPv6(hostname) { |
| if (hostname.startsWith('[')) { |
| hostname = hostname.slice(1); |
| } |
| if (hostname.endsWith(']')) { |
| hostname = hostname.slice(0, -1); |
| } |
| return hostname; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function canResolve(name, useIPv6 = true, useIPv4 = true) { |
| try { |
| let v6Resolved = false; |
| let v4Resolved = false; |
|
|
| if (useIPv6) { |
| try { |
| await dnsPromise.resolve6(name); |
| v6Resolved = true; |
| } catch (error) { |
| v6Resolved = false; |
| } |
| } |
|
|
| if (useIPv4) { |
| try { |
| await dnsPromise.resolve(name); |
| v4Resolved = true; |
| } catch (error) { |
| v4Resolved = false; |
| } |
| } |
|
|
| return v6Resolved || v4Resolved; |
|
|
| } catch (error) { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function getHasIP() { |
| let hasIPv6Any = false; |
| let hasIPv6Local = false; |
|
|
| let hasIPv4Any = false; |
| let hasIPv4Local = false; |
|
|
| const interfaces = os.networkInterfaces(); |
|
|
| for (const iface of Object.values(interfaces)) { |
| if (iface === undefined) { |
| continue; |
| } |
|
|
| for (const info of iface) { |
| if (info.family === 'IPv6') { |
| hasIPv6Any = true; |
| if (info.address === '::1') { |
| hasIPv6Local = true; |
| } |
| } |
|
|
| if (info.family === 'IPv4') { |
| hasIPv4Any = true; |
| if (info.address === '127.0.0.1') { |
| hasIPv4Local = true; |
| } |
| } |
| if (hasIPv6Any && hasIPv4Any && hasIPv6Local && hasIPv4Local) break; |
| } |
| if (hasIPv6Any && hasIPv4Any && hasIPv6Local && hasIPv4Local) break; |
| } |
|
|
| return { hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local }; |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| export function toBoolean(value) { |
| |
| if (typeof value === 'string') { |
| |
| const trimmedLower = value.trim().toLowerCase(); |
|
|
| |
| if (trimmedLower === 'true') return true; |
| if (trimmedLower === 'false') return false; |
| } |
|
|
| |
| return Boolean(value); |
| } |
|
|
| |
| |
| |
| |
| |
| export function stringToBool(str) { |
| if (String(str).trim().toLowerCase() === 'true') return true; |
| if (String(str).trim().toLowerCase() === 'false') return false; |
| return str; |
| } |
|
|
| |
| |
| |
| export function setupLogLevel() { |
| const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG, 'number'); |
|
|
| globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => { }; |
| globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => { }; |
| globalThis.console.warn = logLevel <= LOG_LEVELS.WARN ? console.warn : () => { }; |
| globalThis.console.error = logLevel <= LOG_LEVELS.ERROR ? console.error : () => { }; |
| } |
|
|
| |
| |
| |
| export class MemoryLimitedMap { |
| |
| |
| |
| |
| constructor(cacheCapacity) { |
| this.maxMemory = bytes.parse(cacheCapacity) ?? 0; |
| this.currentMemory = 0; |
| this.map = new Map(); |
| this.queue = []; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| static estimateStringSize(str) { |
| return str ? str.length * 2 : 0; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| set(key, value) { |
| if (this.maxMemory <= 0) { |
| return; |
| } |
|
|
| if (typeof key !== 'string' || typeof value !== 'string') { |
| return; |
| } |
|
|
| const newValueSize = MemoryLimitedMap.estimateStringSize(value); |
|
|
| |
| if (newValueSize > this.maxMemory) { |
| return; |
| } |
|
|
| |
| if (this.map.has(key)) { |
| const oldValue = this.map.get(key); |
| const oldValueSize = MemoryLimitedMap.estimateStringSize(oldValue); |
| this.currentMemory -= oldValueSize; |
| |
| const index = this.queue.indexOf(key); |
| if (index > -1) { |
| this.queue.splice(index, 1); |
| } |
| } |
|
|
| |
| while (this.currentMemory + newValueSize > this.maxMemory && this.queue.length > 0) { |
| const oldestKey = this.queue.shift(); |
| const oldestValue = this.map.get(oldestKey); |
| const oldestValueSize = MemoryLimitedMap.estimateStringSize(oldestValue); |
| this.map.delete(oldestKey); |
| this.currentMemory -= oldestValueSize; |
| } |
|
|
| |
| if (this.currentMemory + newValueSize > this.maxMemory) { |
| return; |
| } |
|
|
| |
| this.map.set(key, value); |
| this.queue.push(key); |
| this.currentMemory += newValueSize; |
| } |
|
|
| |
| |
| |
| |
| |
| get(key) { |
| return this.map.get(key); |
| } |
|
|
| |
| |
| |
| |
| |
| has(key) { |
| return this.map.has(key); |
| } |
|
|
| |
| |
| |
| |
| |
| delete(key) { |
| if (!this.map.has(key)) { |
| return false; |
| } |
| const value = this.map.get(key); |
| const valueSize = MemoryLimitedMap.estimateStringSize(value); |
| this.map.delete(key); |
| this.currentMemory -= valueSize; |
|
|
| |
| const index = this.queue.indexOf(key); |
| if (index > -1) { |
| this.queue.splice(index, 1); |
| } |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| clear() { |
| this.map.clear(); |
| this.queue = []; |
| this.currentMemory = 0; |
| } |
|
|
| |
| |
| |
| |
| size() { |
| return this.map.size; |
| } |
|
|
| |
| |
| |
| |
| totalMemory() { |
| return this.currentMemory; |
| } |
|
|
| |
| |
| |
| |
| keys() { |
| return this.map.keys(); |
| } |
|
|
| |
| |
| |
| |
| values() { |
| return this.map.values(); |
| } |
|
|
| |
| |
| |
| |
| forEach(callback) { |
| this.map.forEach((value, key) => { |
| callback(value, key, this); |
| }); |
| } |
|
|
| |
| |
| |
| |
| [Symbol.iterator]() { |
| return this.map[Symbol.iterator](); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) { |
| if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options); |
| return null; |
| } |
|
|
| |
| |
| |
| |
| export function setWindowTitle(title) { |
| if (process.platform === 'win32') { |
| process.title = title; |
| } |
| else { |
| process.stdout.write(`\x1b]2;${title}\x1b\x5c`); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function mutateJsonString(jsonString, mutation) { |
| try { |
| const json = JSON.parse(jsonString); |
| mutation(json); |
| return JSON.stringify(json); |
| } catch (error) { |
| console.error('Error parsing or mutating JSON:', error); |
| return jsonString; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function setPermissionsSync(targetPath) { |
| |
| |
| |
| |
| |
| function appendWritablePermission(filePath, stats) { |
| const currentMode = stats.mode; |
| const newMode = currentMode | 0o200; |
| if (newMode != currentMode) { |
| fs.chmodSync(filePath, newMode); |
| } |
| } |
|
|
| try { |
| const stats = fs.statSync(targetPath); |
|
|
| if (stats.isDirectory()) { |
| appendWritablePermission(targetPath, stats); |
| const files = fs.readdirSync(targetPath); |
|
|
| files.forEach((file) => { |
| setPermissionsSync(path.join(targetPath, file)); |
| }); |
| } else { |
| appendWritablePermission(targetPath, stats); |
| } |
| } catch (error) { |
| console.error(`Error setting write permissions for ${targetPath}:`, error); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isPathUnderParent(parentPath, childPath) { |
| const normalizedParent = path.normalize(parentPath); |
| const normalizedChild = path.normalize(childPath); |
|
|
| const relativePath = path.relative(normalizedParent, normalizedChild); |
|
|
| return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); |
| } |
|
|
| |
| |
| |
| |
| |
| export function isFileURL(request) { |
| if (typeof request === 'string') { |
| return request.startsWith('file://'); |
| } |
| if (request instanceof URL) { |
| return request.protocol === 'file:'; |
| } |
| if (request instanceof Request) { |
| return request.url.startsWith('file://'); |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| export function getRequestURL(request) { |
| if (typeof request === 'string') { |
| return request; |
| } |
| if (request instanceof URL) { |
| return request.href; |
| } |
| if (request instanceof Request) { |
| return request.url; |
| } |
| throw new TypeError('Invalid request type'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function flattenSchema(schema, api) { |
| if (!schema || typeof schema !== 'object') { |
| return schema; |
| } |
|
|
| const schemaCopy = structuredClone(schema); |
| const isGoogleApi = [CHAT_COMPLETION_SOURCES.VERTEXAI, CHAT_COMPLETION_SOURCES.MAKERSUITE].includes(api); |
|
|
| const definitions = schemaCopy.$defs || {}; |
| delete schemaCopy.$defs; |
|
|
| function resolve(obj, parents = []) { |
| if (!obj || typeof obj !== 'object') { |
| return obj; |
| } |
| if (Array.isArray(obj)) { |
| return obj.map(item => resolve(item, parents)); |
| } |
|
|
| |
| if (obj.$ref?.startsWith('#/$defs/')) { |
| const defName = obj.$ref.split('/').pop(); |
| if (parents.includes(defName)) return {}; |
| if (definitions[defName]) { |
| return resolve(structuredClone(definitions[defName]), [...parents, defName]); |
| } |
| return {}; |
| } |
|
|
| |
| const result = {}; |
| for (const key in obj) { |
| if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; |
|
|
| |
| if (isGoogleApi && ['default', 'additionalProperties', 'exclusiveMinimum', 'propertyNames'].includes(key)) { |
| continue; |
| } |
|
|
| result[key] = resolve(obj[key], parents); |
| } |
|
|
| return result; |
| } |
|
|
| const flattenedSchema = resolve(schemaCopy); |
| delete flattenedSchema.$schema; |
| return flattenedSchema; |
| } |
|
|
| |
| |
| |
| |
| |
| export function tryWriteFileSync(filePath, data) { |
| const directory = path.dirname(filePath); |
| |
| if (!fs.existsSync(directory)) { |
| fs.mkdirSync(directory, { recursive: true }); |
| } |
| writeFileAtomicSync(filePath, data, 'utf8'); |
| } |
|
|
| |
| |
| |
| |
| |
| export function tryReadFileSync(filePath) { |
| try { |
| if (fs.existsSync(filePath)) { |
| return fs.readFileSync(filePath, 'utf8'); |
| } |
| } catch (error) { |
| console.error(`Error reading ${filePath}: ${error.message}`); |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| export function tryDeleteFile(filePath) { |
| if (fs.existsSync(filePath)) { |
| fs.unlinkSync(filePath); |
| console.info(`Deleted file: ${filePath}`); |
| return true; |
| } else { |
| console.error(`File not found '${filePath}'`); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function readFirstLine(filePath) { |
| const stream = fs.createReadStream(filePath, { encoding: 'utf8' }); |
| const rl = readline.createInterface({ input: stream }); |
| return new Promise((resolve, reject) => { |
| let resolved = false; |
| rl.on('line', line => { |
| resolved = true; |
| rl.close(); |
| stream.close(); |
| resolve(line); |
| }); |
|
|
| rl.on('error', error => { |
| resolved = true; |
| reject(error); |
| }); |
|
|
| |
| stream.on('end', () => { |
| if (!resolved) { |
| resolved = true; |
| resolve(''); |
| } |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function invalidateFirefoxCache(file, request, response) { |
| const mimeType = isFirefox(request) && mime.lookup(file); |
| if (mimeType && mimeType.startsWith('image/')) { |
| response.setHeader('Cache-Control', 'must-understand, no-store'); |
| } |
| } |
|
|