@github/copilot
Advanced tools
| { | ||
| "foundry-local-core": { | ||
| "python": "1.0.0.dev202604172003", | ||
| "nuget": "1.0.0-dev-202604172003-977b0e71" | ||
| }, | ||
| "onnxruntime": { | ||
| "version": "1.24.4" | ||
| }, | ||
| "onnxruntime-genai": { | ||
| "version": "0.13.1" | ||
| } | ||
| } |
| import { Model } from './detail/model.js'; | ||
| import { ModelVariant } from './detail/modelVariant.js'; | ||
| /** | ||
| * Represents a catalog of AI models available in the system. | ||
| * Provides methods to discover, list, and retrieve models and their variants. | ||
| */ | ||
| export class Catalog { | ||
| _name; | ||
| coreInterop; | ||
| modelLoadManager; | ||
| _models = []; | ||
| modelAliasToModel = new Map(); | ||
| modelIdToModelVariant = new Map(); | ||
| lastFetch = 0; | ||
| constructor(coreInterop, modelLoadManager) { | ||
| this.coreInterop = coreInterop; | ||
| this.modelLoadManager = modelLoadManager; | ||
| this._name = this.coreInterop.executeCommand("get_catalog_name"); | ||
| } | ||
| /** | ||
| * Gets the name of the catalog. | ||
| * @returns The name of the catalog. | ||
| */ | ||
| get name() { | ||
| return this._name; | ||
| } | ||
| /** @internal */ | ||
| invalidateCache() { | ||
| this.lastFetch = 0; | ||
| } | ||
| async updateModels() { | ||
| // TODO: make this configurable | ||
| if ((Date.now() - this.lastFetch) < 6 * 60 * 60 * 1000) { // 6 hours | ||
| return; | ||
| } | ||
| // Potential network call to fetch model list | ||
| const modelListJson = this.coreInterop.executeCommand("get_model_list"); | ||
| let modelsInfo = []; | ||
| try { | ||
| modelsInfo = JSON.parse(modelListJson); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to parse model list JSON: ${error}`); | ||
| } | ||
| this.modelAliasToModel.clear(); | ||
| this.modelIdToModelVariant.clear(); | ||
| this._models = []; | ||
| for (const info of modelsInfo) { | ||
| const variant = new ModelVariant(info, this.coreInterop, this.modelLoadManager); | ||
| let model = this.modelAliasToModel.get(info.alias); | ||
| if (!model) { | ||
| model = new Model(variant); | ||
| this.modelAliasToModel.set(info.alias, model); | ||
| this._models.push(model); | ||
| } | ||
| else { | ||
| model.addVariant(variant); | ||
| } | ||
| this.modelIdToModelVariant.set(variant.id, variant); | ||
| } | ||
| this.lastFetch = Date.now(); | ||
| } | ||
| /** | ||
| * Lists all available models in the catalog. | ||
| * This method is asynchronous as it may fetch the model list from a remote service or perform file I/O. | ||
| * @returns A Promise that resolves to an array of IModel objects. | ||
| */ | ||
| async getModels() { | ||
| await this.updateModels(); | ||
| return this._models; | ||
| } | ||
| /** | ||
| * Retrieves a model by its alias. | ||
| * This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service. | ||
| * @param alias - The alias of the model to retrieve. | ||
| * @returns A Promise that resolves to the IModel object if found, otherwise throws an error. | ||
| * @throws Error - If alias is null, undefined, or empty. | ||
| */ | ||
| async getModel(alias) { | ||
| if (typeof alias !== 'string' || alias.trim() === '') { | ||
| throw new Error('Model alias must be a non-empty string.'); | ||
| } | ||
| await this.updateModels(); | ||
| const model = this.modelAliasToModel.get(alias); | ||
| if (!model) { | ||
| const availableAliases = Array.from(this.modelAliasToModel.keys()).join(', '); | ||
| throw new Error(`Model with alias '${alias}' not found. Available models: ${availableAliases || '(none)'}`); | ||
| } | ||
| return model; | ||
| } | ||
| /** | ||
| * Retrieves a specific model variant by its ID. | ||
| * NOTE: This will return an IModel with a single variant. Use getModel to get an IModel with all available | ||
| * variants. | ||
| * This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service. | ||
| * @param modelId - The unique identifier of the model variant. | ||
| * @returns A Promise that resolves to the IModel object if found, otherwise throws an error. | ||
| * @throws Error - If modelId is null, undefined, or empty. | ||
| */ | ||
| async getModelVariant(modelId) { | ||
| if (typeof modelId !== 'string' || modelId.trim() === '') { | ||
| throw new Error('Model ID must be a non-empty string.'); | ||
| } | ||
| await this.updateModels(); | ||
| const variant = this.modelIdToModelVariant.get(modelId); | ||
| if (!variant) { | ||
| const availableIds = Array.from(this.modelIdToModelVariant.keys()).join(', '); | ||
| throw new Error(`Model variant with ID '${modelId}' not found. Available variants: ${availableIds || '(none)'}`); | ||
| } | ||
| return variant; | ||
| } | ||
| /** | ||
| * Retrieves a list of all locally cached model variants. | ||
| * This method is asynchronous as it may involve file I/O or querying the underlying core. | ||
| * @returns A Promise that resolves to an array of cached IModel objects. | ||
| */ | ||
| async getCachedModels() { | ||
| await this.updateModels(); | ||
| const cachedModelListJson = this.coreInterop.executeCommand("get_cached_models"); | ||
| let cachedModelIds = []; | ||
| try { | ||
| cachedModelIds = JSON.parse(cachedModelListJson); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to parse cached model list JSON: ${error}`); | ||
| } | ||
| const cachedModels = new Set(); | ||
| for (const modelId of cachedModelIds) { | ||
| const variant = this.modelIdToModelVariant.get(modelId); | ||
| if (variant) { | ||
| cachedModels.add(variant); | ||
| } | ||
| } | ||
| return Array.from(cachedModels); | ||
| } | ||
| /** | ||
| * Retrieves a list of all currently loaded model variants. | ||
| * This operation is asynchronous because checking the loaded status may involve querying | ||
| * the underlying core or an external service, which can be an I/O bound operation. | ||
| * @returns A Promise that resolves to an array of loaded IModel objects. | ||
| */ | ||
| async getLoadedModels() { | ||
| await this.updateModels(); | ||
| let loadedModelIds = []; | ||
| try { | ||
| loadedModelIds = await this.modelLoadManager.listLoaded(); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to list loaded models: ${error}`); | ||
| } | ||
| const loadedModels = []; | ||
| for (const modelId of loadedModelIds) { | ||
| const variant = this.modelIdToModelVariant.get(modelId); | ||
| if (variant) { | ||
| loadedModels.push(variant); | ||
| } | ||
| } | ||
| return loadedModels; | ||
| } | ||
| /** | ||
| * Get the latest version of a model. | ||
| * This is used to check if a newer version of a model is available in the catalog for download. | ||
| * @param modelOrModelVariant - The model to check for the latest version. | ||
| * @returns The latest version of the model. Will match the input if it is the latest version. | ||
| */ | ||
| async getLatestVersion(modelOrModelVariant) { | ||
| await this.updateModels(); | ||
| // Resolve to the parent Model by alias | ||
| const model = this.modelAliasToModel.get(modelOrModelVariant.alias); | ||
| if (!model) { | ||
| throw new Error(`Model with alias '${modelOrModelVariant.alias}' not found in catalog.`); | ||
| } | ||
| // variants are sorted by version, so the first one matching the name is the latest version | ||
| const latest = model.variants.find(v => v.info.name === modelOrModelVariant.info.name); | ||
| if (!latest) { | ||
| throw new Error(`Internal error. Mismatch between model (alias:${model.alias}) and ` + | ||
| `model variant (alias:${modelOrModelVariant.alias}).`); | ||
| } | ||
| // if input was the latest return the input (could be model or model variant) | ||
| // otherwise return the latest model variant | ||
| return latest.id === modelOrModelVariant.id ? modelOrModelVariant : latest; | ||
| } | ||
| } |
| // Log level mapping from JS-style to C#-style | ||
| const LOG_LEVEL_MAP = { | ||
| 'trace': 'Verbose', | ||
| 'debug': 'Debug', | ||
| 'info': 'Information', | ||
| 'warn': 'Warning', | ||
| 'error': 'Error', | ||
| 'fatal': 'Fatal' | ||
| }; | ||
| // Internal Configuration class (not exported) | ||
| export class Configuration { | ||
| params; | ||
| constructor(config) { | ||
| if (!config) { | ||
| throw new Error("Configuration must be provided."); | ||
| } | ||
| if (!config.appName || config.appName.trim() === "") { | ||
| throw new Error("appName must be set to a valid application name."); | ||
| } | ||
| this.params = { | ||
| 'AppName': config.appName | ||
| }; | ||
| if (config.appDataDir) | ||
| this.params['AppDataDir'] = config.appDataDir; | ||
| if (config.modelCacheDir) | ||
| this.params['ModelCacheDir'] = config.modelCacheDir; | ||
| if (config.logsDir) | ||
| this.params['LogsDir'] = config.logsDir; | ||
| if (config.logLevel) | ||
| this.params['LogLevel'] = LOG_LEVEL_MAP[config.logLevel] || config.logLevel; | ||
| if (config.webServiceUrls) | ||
| this.params['WebServiceUrls'] = config.webServiceUrls; | ||
| if (config.serviceEndpoint) | ||
| this.params['WebServiceExternalUrl'] = config.serviceEndpoint; | ||
| if (config.libraryPath) | ||
| this.params['FoundryLocalCorePath'] = config.libraryPath; | ||
| // Flatten additional settings into params | ||
| if (config.additionalSettings) { | ||
| for (const key in config.additionalSettings) { | ||
| this.params[key] = config.additionalSettings[key]; | ||
| } | ||
| } | ||
| } | ||
| } |
| import path from 'path'; | ||
| import fs from 'fs'; | ||
| import { createRequire } from 'module'; | ||
| import { fileURLToPath } from 'url'; | ||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = path.dirname(__filename); | ||
| // Load the prebuilt Node-API addon | ||
| const require = createRequire(import.meta.url); | ||
| function loadAddon() { | ||
| const platform = process.platform; | ||
| const arch = process.arch; | ||
| const platformKey = `${platform}-${arch}`; | ||
| // The prebuilt addon ships inside the SDK package under prebuilds/<platform>/ | ||
| const sdkRoot = path.resolve(__dirname, '..', '..'); | ||
| const prebuiltPath = path.join(sdkRoot, 'prebuilds', platformKey, 'foundry_local_napi.node'); | ||
| if (fs.existsSync(prebuiltPath)) { | ||
| return require(prebuiltPath); | ||
| } | ||
| // Fallback: development builds from node-gyp (sdk contributors) | ||
| const devPath = path.join(sdkRoot, 'native', 'build', 'Release', 'foundry_local_napi.node'); | ||
| if (fs.existsSync(devPath)) { | ||
| return require(devPath); | ||
| } | ||
| throw new Error(`Could not find foundry_local_napi.node for platform ${platformKey}. ` + | ||
| `Searched: ${prebuiltPath}, ${devPath}. ` + | ||
| `Please ensure the SDK was installed correctly or run 'npm run build:native' to compile from source.`); | ||
| } | ||
| export class CoreInterop { | ||
| addon; | ||
| static _getLibraryExtension() { | ||
| const platform = process.platform; | ||
| if (platform === 'win32') | ||
| return '.dll'; | ||
| if (platform === 'linux') | ||
| return '.so'; | ||
| if (platform === 'darwin') | ||
| return '.dylib'; | ||
| throw new Error(`Unsupported platform: ${platform}`); | ||
| } | ||
| static _resolveDefaultCorePath(config) { | ||
| const platform = process.platform; | ||
| const arch = process.arch; | ||
| const platformKey = `${platform}-${arch}`; | ||
| // Resolve the native binary directory at foundry-local-core/<platform>, | ||
| // the shared location where install scripts place the native binaries. | ||
| const sdkRoot = path.resolve(__dirname, '..', '..'); | ||
| const packageDir = path.join(sdkRoot, 'foundry-local-core', platformKey); | ||
| const ext = CoreInterop._getLibraryExtension(); | ||
| const corePath = path.join(packageDir, `Microsoft.AI.Foundry.Local.Core${ext}`); | ||
| if (fs.existsSync(corePath)) { | ||
| config.params['FoundryLocalCorePath'] = corePath; | ||
| // Auto-detect if WinML Bootstrap is needed by checking for Bootstrap DLL in FoundryLocalCorePath | ||
| // Only auto-set if the user hasn't explicitly provided a value | ||
| if (!('Bootstrap' in config.params)) { | ||
| const bootstrapDllPath = path.join(packageDir, 'Microsoft.WindowsAppRuntime.Bootstrap.dll'); | ||
| if (fs.existsSync(bootstrapDllPath)) { | ||
| // WinML Bootstrap DLL found, enable bootstrapping | ||
| config.params['Bootstrap'] = 'true'; | ||
| } | ||
| } | ||
| return corePath; | ||
| } | ||
| return null; | ||
| } | ||
| constructor(config) { | ||
| this.addon = loadAddon(); | ||
| const corePath = config.params['FoundryLocalCorePath'] || CoreInterop._resolveDefaultCorePath(config); | ||
| if (!corePath) { | ||
| throw new Error("FoundryLocalCorePath not specified in configuration and could not auto-discover binaries. Please run 'npm install' to download native libraries."); | ||
| } | ||
| const coreDir = path.dirname(corePath); | ||
| const ext = CoreInterop._getLibraryExtension(); | ||
| // On Windows, explicitly load dependencies to work around DLL resolution challenges | ||
| const depPaths = []; | ||
| if (process.platform === 'win32') { | ||
| depPaths.push(path.join(coreDir, `onnxruntime${ext}`)); | ||
| depPaths.push(path.join(coreDir, `onnxruntime-genai${ext}`)); | ||
| const currentPath = process.env.PATH ?? ''; | ||
| process.env.PATH = currentPath ? `${coreDir};${currentPath}` : coreDir; | ||
| } | ||
| this.addon.loadLibrary(corePath, depPaths.length > 0 ? depPaths : undefined); | ||
| } | ||
| executeCommand(command, params) { | ||
| const dataStr = params ? JSON.stringify(params) : ''; | ||
| return this.addon.executeCommand(command, dataStr); | ||
| } | ||
| /** | ||
| * Execute a native command with binary data (e.g., audio PCM bytes). | ||
| * Uses the execute_command_with_binary native entry point which accepts | ||
| * both JSON params and raw binary data via StreamingRequestBuffer. | ||
| */ | ||
| executeCommandWithBinary(command, params, binaryData) { | ||
| const dataStr = params ? JSON.stringify(params) : ''; | ||
| const binBuf = Buffer.from(binaryData.buffer, binaryData.byteOffset, binaryData.byteLength); | ||
| return this.addon.executeCommandWithBinary(command, dataStr, binBuf); | ||
| } | ||
| executeCommandStreaming(command, params, callback) { | ||
| const dataStr = params ? JSON.stringify(params) : ''; | ||
| return this.addon.executeCommandStreaming(command, dataStr, callback); | ||
| } | ||
| } |
| /** | ||
| * Represents a high-level AI model that may have multiple variants (e.g., quantized versions, different formats). | ||
| * Manages the selection and interaction with a specific model variant. | ||
| */ | ||
| export class Model { | ||
| _alias; | ||
| _variants; | ||
| selectedVariant; | ||
| constructor(variant) { | ||
| this._alias = variant.alias; | ||
| this._variants = [variant]; | ||
| this.selectedVariant = variant; | ||
| } | ||
| /** | ||
| * Adds a new variant to this model. | ||
| * Automatically selects the new variant if it is cached and the current one is not. | ||
| * @param variant - The model variant to add. | ||
| * @throws Error - If the variant's alias does not match the model's alias. | ||
| * @internal | ||
| */ | ||
| addVariant(variant) { | ||
| if (!variant || variant.alias !== this._alias) { | ||
| throw new Error(`Variant alias "${variant?.alias}" does not match model alias "${this._alias}".`); | ||
| } | ||
| this._variants.push(variant); | ||
| // prefer the highest priority locally cached variant | ||
| if (variant.isCached && !this.selectedVariant.isCached) { | ||
| this.selectedVariant = variant; | ||
| } | ||
| } | ||
| /** | ||
| * Selects a specific variant. | ||
| * @param variant - The model variant to select. Must be one of the variants in `variants`. | ||
| * @throws Error - If the variant does not belong to this model. | ||
| */ | ||
| selectVariant(variant) { | ||
| const matchingVariant = this._variants.find(v => v.id === variant.id); | ||
| if (!variant.id || !matchingVariant) { | ||
| throw new Error(`Input variant was not found in Variants.`); | ||
| } | ||
| this.selectedVariant = matchingVariant; | ||
| } | ||
| /** | ||
| * Gets the ID of the currently selected variant. | ||
| * @returns The ID of the selected variant. | ||
| */ | ||
| get id() { | ||
| return this.selectedVariant.id; | ||
| } | ||
| /** | ||
| * Gets the alias of the model. | ||
| * @returns The model alias. | ||
| */ | ||
| get alias() { | ||
| return this._alias; | ||
| } | ||
| /** | ||
| * Gets the ModelInfo of the currently selected variant. | ||
| * @returns The ModelInfo object. | ||
| */ | ||
| get info() { | ||
| return this.selectedVariant.info; | ||
| } | ||
| /** | ||
| * Checks if the currently selected variant is cached locally. | ||
| * @returns True if cached, false otherwise. | ||
| */ | ||
| get isCached() { | ||
| return this.selectedVariant.isCached; | ||
| } | ||
| /** | ||
| * Checks if the currently selected variant is loaded in memory. | ||
| * @returns True if loaded, false otherwise. | ||
| */ | ||
| async isLoaded() { | ||
| return await this.selectedVariant.isLoaded(); | ||
| } | ||
| /** | ||
| * Gets all available variants for this model. | ||
| * @returns An array of IModel objects. | ||
| */ | ||
| get variants() { | ||
| return this._variants; | ||
| } | ||
| get contextLength() { | ||
| return this.selectedVariant.contextLength; | ||
| } | ||
| get inputModalities() { | ||
| return this.selectedVariant.inputModalities; | ||
| } | ||
| get outputModalities() { | ||
| return this.selectedVariant.outputModalities; | ||
| } | ||
| get capabilities() { | ||
| return this.selectedVariant.capabilities; | ||
| } | ||
| get supportsToolCalling() { | ||
| return this.selectedVariant.supportsToolCalling; | ||
| } | ||
| /** | ||
| * Downloads the currently selected variant. | ||
| * @param progressCallback - Optional callback to report download progress. | ||
| */ | ||
| download(progressCallback) { | ||
| return this.selectedVariant.download(progressCallback); | ||
| } | ||
| /** | ||
| * Gets the local file path of the currently selected variant. | ||
| * @returns The local file path. | ||
| */ | ||
| get path() { | ||
| return this.selectedVariant.path; | ||
| } | ||
| /** | ||
| * Loads the currently selected variant into memory. | ||
| * @returns A promise that resolves when the model is loaded. | ||
| */ | ||
| async load() { | ||
| await this.selectedVariant.load(); | ||
| } | ||
| /** | ||
| * Removes the currently selected variant from the local cache. | ||
| */ | ||
| removeFromCache() { | ||
| this.selectedVariant.removeFromCache(); | ||
| } | ||
| /** | ||
| * Unloads the currently selected variant from memory. | ||
| * @returns A promise that resolves when the model is unloaded. | ||
| */ | ||
| async unload() { | ||
| await this.selectedVariant.unload(); | ||
| } | ||
| /** | ||
| * Creates a ChatClient for interacting with the model via chat completions. | ||
| * @returns A ChatClient instance. | ||
| */ | ||
| createChatClient() { | ||
| return this.selectedVariant.createChatClient(); | ||
| } | ||
| /** | ||
| * Creates an AudioClient for interacting with the model via audio operations. | ||
| * @returns An AudioClient instance. | ||
| */ | ||
| createAudioClient() { | ||
| return this.selectedVariant.createAudioClient(); | ||
| } | ||
| /** | ||
| * Creates a LiveAudioTranscriptionSession for real-time audio streaming ASR. | ||
| * @returns A LiveAudioTranscriptionSession instance. | ||
| */ | ||
| createLiveTranscriptionSession() { | ||
| return this.selectedVariant.createLiveTranscriptionSession(); | ||
| } | ||
| /** | ||
| * Creates a ResponsesClient for interacting with the model via the Responses API. | ||
| * @param baseUrl - The base URL of the Foundry Local web service. | ||
| * @returns A ResponsesClient instance. | ||
| */ | ||
| createResponsesClient(baseUrl) { | ||
| return this.selectedVariant.createResponsesClient(baseUrl); | ||
| } | ||
| } |
| import packageJson from '../../package.json' with { type: "json" }; | ||
| const { version } = packageJson; | ||
| /** | ||
| * Manages the loading and unloading of models. | ||
| * Handles communication with the core system or an external service (future support). | ||
| */ | ||
| export class ModelLoadManager { | ||
| coreInterop; | ||
| externalServiceUrl; | ||
| headers; | ||
| constructor(coreInterop, externalServiceUrl) { | ||
| this.coreInterop = coreInterop; | ||
| this.externalServiceUrl = externalServiceUrl; | ||
| this.headers = { | ||
| 'User-Agent': `foundry-local-js-sdk/${version}` | ||
| }; | ||
| } | ||
| /** | ||
| * Loads a model into memory. | ||
| * @param modelId - The ID of the model to load. | ||
| * @throws Error - If loading via external service fails. | ||
| */ | ||
| async load(modelId) { | ||
| if (this.externalServiceUrl) { | ||
| const url = new URL(`models/load/${encodeURIComponent(modelId)}`, this.externalServiceUrl); | ||
| try { | ||
| const response = await fetch(url.toString(), { headers: this.headers }); | ||
| if (!response.ok) { | ||
| throw new Error(`Error loading model ${modelId} from ${this.externalServiceUrl}: ${response.statusText}`); | ||
| } | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Network error occurred while loading model ${modelId} from ${this.externalServiceUrl}: ${error.message}`); | ||
| } | ||
| return; | ||
| } | ||
| this.coreInterop.executeCommand("load_model", { Params: { Model: modelId } }); | ||
| } | ||
| /** | ||
| * Unloads a model from memory. | ||
| * @param modelId - The ID of the model to unload. | ||
| * @throws Error - If unloading via external service fails. | ||
| */ | ||
| async unload(modelId) { | ||
| if (this.externalServiceUrl) { | ||
| const url = new URL(`models/unload/${encodeURIComponent(modelId)}`, this.externalServiceUrl); | ||
| const response = await fetch(url.toString(), { headers: this.headers }); | ||
| if (!response.ok) { | ||
| throw new Error(`Error unloading model ${modelId} from ${this.externalServiceUrl}: ${response.statusText}`); | ||
| } | ||
| return; | ||
| } | ||
| this.coreInterop.executeCommand("unload_model", { Params: { Model: modelId } }); | ||
| } | ||
| /** | ||
| * Lists the IDs of all currently loaded models. | ||
| * @returns An array of loaded model IDs. | ||
| * @throws Error - If listing via external service fails or if JSON parsing fails. | ||
| */ | ||
| async listLoaded() { | ||
| if (this.externalServiceUrl) { | ||
| const url = new URL('models/loaded', this.externalServiceUrl); | ||
| const response = await fetch(url.toString(), { headers: this.headers }); | ||
| if (!response.ok) { | ||
| throw new Error(`Error listing loaded models from ${this.externalServiceUrl}: ${response.statusText}`); | ||
| } | ||
| const list = await response.json(); | ||
| return list || []; | ||
| } | ||
| const response = this.coreInterop.executeCommand("list_loaded_models"); | ||
| try { | ||
| return JSON.parse(response); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to decode JSON response: ${error}. Response was: ${response}`); | ||
| } | ||
| } | ||
| } |
| import { ChatClient } from '../openai/chatClient.js'; | ||
| import { AudioClient } from '../openai/audioClient.js'; | ||
| import { LiveAudioTranscriptionSession } from '../openai/liveAudioTranscriptionClient.js'; | ||
| import { ResponsesClient } from '../openai/responsesClient.js'; | ||
| /** | ||
| * Represents a specific variant of a model (e.g., a specific quantization or format). | ||
| * Contains the low-level implementation for interacting with the model. | ||
| * @internal | ||
| */ | ||
| export class ModelVariant { | ||
| _modelInfo; | ||
| coreInterop; | ||
| modelLoadManager; | ||
| constructor(modelInfo, coreInterop, modelLoadManager) { | ||
| this._modelInfo = modelInfo; | ||
| this.coreInterop = coreInterop; | ||
| this.modelLoadManager = modelLoadManager; | ||
| } | ||
| /** | ||
| * Gets the unique identifier of the model variant. | ||
| * @returns The model ID. | ||
| */ | ||
| get id() { | ||
| return this._modelInfo.id; | ||
| } | ||
| /** | ||
| * Gets the alias of the model. | ||
| * @returns The model alias. | ||
| */ | ||
| get alias() { | ||
| return this._modelInfo.alias; | ||
| } | ||
| /** | ||
| * Gets the detailed information about the model variant. | ||
| * @returns The ModelInfo object. | ||
| */ | ||
| get info() { | ||
| return this._modelInfo; | ||
| } | ||
| /** | ||
| * A ModelVariant is a single variant, so variants returns itself. | ||
| */ | ||
| get variants() { | ||
| return [this]; | ||
| } | ||
| /** | ||
| * SelectVariant is not supported on a ModelVariant. | ||
| * Call Catalog.getModel() to get an IModel with all variants available. | ||
| * @throws Error always. | ||
| */ | ||
| selectVariant(_variant) { | ||
| throw new Error(`selectVariant is not supported on a ModelVariant. ` + | ||
| `Call Catalog.getModel("${this.alias}") to get an IModel with all variants available.`); | ||
| } | ||
| get contextLength() { | ||
| return this._modelInfo.contextLength ?? null; | ||
| } | ||
| get inputModalities() { | ||
| return this._modelInfo.inputModalities ?? null; | ||
| } | ||
| get outputModalities() { | ||
| return this._modelInfo.outputModalities ?? null; | ||
| } | ||
| get capabilities() { | ||
| return this._modelInfo.capabilities ?? null; | ||
| } | ||
| get supportsToolCalling() { | ||
| return this._modelInfo.supportsToolCalling ?? null; | ||
| } | ||
| /** | ||
| * Checks if the model variant is cached locally. | ||
| * @returns True if cached, false otherwise. | ||
| */ | ||
| get isCached() { | ||
| const cachedModels = JSON.parse(this.coreInterop.executeCommand("get_cached_models")); | ||
| return cachedModels.includes(this._modelInfo.id); | ||
| } | ||
| /** | ||
| * Checks if the model variant is loaded in memory. | ||
| * @returns True if loaded, false otherwise. | ||
| */ | ||
| async isLoaded() { | ||
| const loadedModels = await this.modelLoadManager.listLoaded(); | ||
| return loadedModels.includes(this._modelInfo.id); | ||
| } | ||
| /** | ||
| * Downloads the model variant. | ||
| * @param progressCallback - Optional callback to report download progress (0-100). | ||
| */ | ||
| async download(progressCallback) { | ||
| const request = { Params: { Model: this._modelInfo.id } }; | ||
| if (!progressCallback) { | ||
| this.coreInterop.executeCommand("download_model", request); | ||
| } | ||
| else { | ||
| await this.coreInterop.executeCommandStreaming("download_model", request, (chunk) => { | ||
| const progress = parseFloat(chunk); | ||
| if (!isNaN(progress)) { | ||
| progressCallback(progress); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Gets the local file path of the model variant. | ||
| * @returns The local file path. | ||
| */ | ||
| get path() { | ||
| const request = { Params: { Model: this._modelInfo.id } }; | ||
| return this.coreInterop.executeCommand("get_model_path", request); | ||
| } | ||
| /** | ||
| * Loads the model variant into memory. | ||
| * @returns A promise that resolves when the model is loaded. | ||
| */ | ||
| async load() { | ||
| await this.modelLoadManager.load(this._modelInfo.id); | ||
| } | ||
| /** | ||
| * Removes the model variant from the local cache. | ||
| */ | ||
| removeFromCache() { | ||
| this.coreInterop.executeCommand("remove_cached_model", { Params: { Model: this._modelInfo.id } }); | ||
| } | ||
| /** | ||
| * Unloads the model variant from memory. | ||
| * @returns A promise that resolves when the model is unloaded. | ||
| */ | ||
| async unload() { | ||
| await this.modelLoadManager.unload(this._modelInfo.id); | ||
| } | ||
| /** | ||
| * Creates a ChatClient for interacting with the model via chat completions. | ||
| * @returns A ChatClient instance. | ||
| */ | ||
| createChatClient() { | ||
| return new ChatClient(this._modelInfo.id, this.coreInterop); | ||
| } | ||
| /** | ||
| * Creates an AudioClient for interacting with the model via audio operations. | ||
| * @returns An AudioClient instance. | ||
| */ | ||
| createAudioClient() { | ||
| return new AudioClient(this._modelInfo.id, this.coreInterop); | ||
| } | ||
| /** | ||
| * Creates a LiveAudioTranscriptionSession for real-time audio streaming ASR. | ||
| * @returns A LiveAudioTranscriptionSession instance. | ||
| */ | ||
| createLiveTranscriptionSession() { | ||
| return new LiveAudioTranscriptionSession(this._modelInfo.id, this.coreInterop); | ||
| } | ||
| /** | ||
| * Creates a ResponsesClient for interacting with the model via the Responses API. | ||
| * @param baseUrl - The base URL of the Foundry Local web service. | ||
| * @returns A ResponsesClient instance. | ||
| */ | ||
| createResponsesClient(baseUrl) { | ||
| return new ResponsesClient(baseUrl, this._modelInfo.id); | ||
| } | ||
| } |
| import { Configuration } from './configuration.js'; | ||
| import { CoreInterop } from './detail/coreInterop.js'; | ||
| import { ModelLoadManager } from './detail/modelLoadManager.js'; | ||
| import { Catalog } from './catalog.js'; | ||
| import { ResponsesClient } from './openai/responsesClient.js'; | ||
| /** | ||
| * The main entry point for the Foundry Local SDK. | ||
| * Manages the initialization of the core system and provides access to the Catalog and ModelLoadManager. | ||
| */ | ||
| export class FoundryLocalManager { | ||
| static instance; | ||
| config; | ||
| coreInterop; | ||
| _modelLoadManager; | ||
| _catalog; | ||
| _urls = []; | ||
| constructor(config) { | ||
| this.config = config; | ||
| this.coreInterop = new CoreInterop(this.config); | ||
| this.coreInterop.executeCommand("initialize", { Params: this.config.params }); | ||
| this._modelLoadManager = new ModelLoadManager(this.coreInterop); | ||
| this._catalog = new Catalog(this.coreInterop, this._modelLoadManager); | ||
| } | ||
| /** | ||
| * Creates the FoundryLocalManager singleton with the provided configuration. | ||
| * @param config - The configuration settings for the SDK (plain object). | ||
| * @returns The initialized FoundryLocalManager instance. | ||
| * @example | ||
| * ```typescript | ||
| * const manager = FoundryLocalManager.create({ | ||
| * appName: 'MyApp', | ||
| * logLevel: 'info' | ||
| * }); | ||
| * ``` | ||
| */ | ||
| static create(config) { | ||
| if (!FoundryLocalManager.instance) { | ||
| const internalConfig = new Configuration(config); | ||
| FoundryLocalManager.instance = new FoundryLocalManager(internalConfig); | ||
| } | ||
| return FoundryLocalManager.instance; | ||
| } | ||
| /** | ||
| * Gets the Catalog instance for discovering and managing models. | ||
| * @returns The Catalog instance. | ||
| */ | ||
| get catalog() { | ||
| return this._catalog; | ||
| } | ||
| /** | ||
| * Gets the URLs where the web service is listening. | ||
| * Returns an empty array if the web service is not running. | ||
| * @returns An array of URLs. | ||
| */ | ||
| get urls() { | ||
| return this._urls; | ||
| } | ||
| /** | ||
| * Starts the local web service. | ||
| * Use the `urls` property to retrieve the bound addresses after the service has started. | ||
| * If no listener address is configured, the service defaults to `127.0.0.1:0` (binding to a random ephemeral port). | ||
| * @throws Error - If starting the service fails. | ||
| */ | ||
| startWebService() { | ||
| const response = this.coreInterop.executeCommand("start_service"); | ||
| try { | ||
| this._urls = JSON.parse(response); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to decode JSON response from start_service: ${error}. Response was: ${response}`); | ||
| } | ||
| } | ||
| /** | ||
| * Stops the local web service. | ||
| * @throws Error - If stopping the service fails. | ||
| */ | ||
| stopWebService() { | ||
| if (this._urls.length > 0) { | ||
| this.coreInterop.executeCommand("stop_service"); | ||
| this._urls = []; | ||
| } | ||
| } | ||
| /** | ||
| * Whether the web service is currently running. | ||
| */ | ||
| get isWebServiceRunning() { | ||
| return this._urls.length > 0; | ||
| } | ||
| /** | ||
| * Discovers available execution providers (EPs) and their registration status. | ||
| * @returns An array of EpInfo describing each available EP. | ||
| */ | ||
| discoverEps() { | ||
| const response = this.coreInterop.executeCommand("discover_eps"); | ||
| try { | ||
| const raw = JSON.parse(response); | ||
| return raw.map((ep) => ({ | ||
| name: ep.Name, | ||
| isRegistered: ep.IsRegistered | ||
| })); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to decode JSON response from discover_eps: ${error}. Response was: ${response}`); | ||
| } | ||
| } | ||
| async downloadAndRegisterEps(namesOrCallback, progressCallback) { | ||
| let names; | ||
| if (typeof namesOrCallback === 'function') { | ||
| progressCallback = namesOrCallback; | ||
| } | ||
| else { | ||
| names = namesOrCallback; | ||
| } | ||
| const params = {}; | ||
| if (names && names.length > 0) { | ||
| params.Params = { Names: names.join(",") }; | ||
| } | ||
| let response; | ||
| if (progressCallback) { | ||
| response = await this.coreInterop.executeCommandStreaming("download_and_register_eps", Object.keys(params).length > 0 ? params : undefined, (chunk) => { | ||
| const sepIndex = chunk.indexOf('|'); | ||
| if (sepIndex >= 0) { | ||
| const epName = chunk.substring(0, sepIndex); | ||
| const percent = parseFloat(chunk.substring(sepIndex + 1)); | ||
| if (!isNaN(percent)) { | ||
| progressCallback(epName || '', percent); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| else { | ||
| response = await this.coreInterop.executeCommandStreaming("download_and_register_eps", Object.keys(params).length > 0 ? params : undefined, () => { } // no-op callback | ||
| ); | ||
| } | ||
| let epResult; | ||
| try { | ||
| const raw = JSON.parse(response); | ||
| epResult = { | ||
| success: raw.Success, | ||
| status: raw.Status, | ||
| registeredEps: raw.RegisteredEps, | ||
| failedEps: raw.FailedEps | ||
| }; | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Failed to decode JSON response from download_and_register_eps: ${error}. Response was: ${response}`); | ||
| } | ||
| // Invalidate the catalog cache if any EP was newly registered so the next access | ||
| // re-fetches models with the updated set of available EPs. | ||
| if (epResult.success || epResult.registeredEps.length > 0) { | ||
| this._catalog.invalidateCache(); | ||
| } | ||
| return epResult; | ||
| } | ||
| /** | ||
| * Creates a ResponsesClient for interacting with the Responses API. | ||
| * The web service must be started first via `startWebService()`. | ||
| * @param modelId - Optional default model ID for requests. | ||
| * @returns A ResponsesClient instance. | ||
| * @throws Error - If the web service is not running. | ||
| */ | ||
| createResponsesClient(modelId) { | ||
| if (this._urls.length === 0) { | ||
| throw new Error('Web service is not running. Call startWebService() before creating a ResponsesClient.'); | ||
| } | ||
| return new ResponsesClient(this._urls[0], modelId); | ||
| } | ||
| } |
| export { FoundryLocalManager } from './foundryLocalManager.js'; | ||
| export { Catalog } from './catalog.js'; | ||
| /** @internal */ | ||
| export { Model } from './detail/model.js'; | ||
| /** @internal */ | ||
| export { ModelVariant } from './detail/modelVariant.js'; | ||
| export { ChatClient, ChatClientSettings } from './openai/chatClient.js'; | ||
| export { AudioClient, AudioClientSettings } from './openai/audioClient.js'; | ||
| export { LiveAudioTranscriptionSession, LiveAudioTranscriptionOptions } from './openai/liveAudioTranscriptionClient.js'; | ||
| export { ResponsesClient, ResponsesClientSettings, getOutputText } from './openai/responsesClient.js'; | ||
| export { ModelLoadManager } from './detail/modelLoadManager.js'; | ||
| /** @internal */ | ||
| export { CoreInterop } from './detail/coreInterop.js'; | ||
| /** @internal */ | ||
| export { Configuration } from './configuration.js'; | ||
| export * from './types.js'; |
| import { LiveAudioTranscriptionSession } from './liveAudioTranscriptionClient.js'; | ||
| export class AudioClientSettings { | ||
| language; | ||
| temperature; | ||
| /** | ||
| * Serializes the settings into an OpenAI-compatible request object. | ||
| * @internal | ||
| */ | ||
| _serialize() { | ||
| // Standard OpenAI properties | ||
| const result = { | ||
| Language: this.language, | ||
| Temperature: this.temperature, | ||
| }; | ||
| // Foundry specific metadata properties | ||
| const metadata = {}; | ||
| if (this.language !== undefined) { | ||
| metadata["language"] = this.language; | ||
| } | ||
| if (this.temperature !== undefined) { | ||
| metadata["temperature"] = this.temperature.toString(); | ||
| } | ||
| if (Object.keys(metadata).length > 0) { | ||
| result.metadata = metadata; | ||
| } | ||
| // Filter out undefined properties | ||
| return Object.fromEntries(Object.entries(result).filter(([_, v]) => v !== undefined)); | ||
| } | ||
| } | ||
| /** | ||
| * Client for performing audio operations (transcription, translation) with a loaded model. | ||
| * Follows the OpenAI Audio API structure. | ||
| */ | ||
| export class AudioClient { | ||
| modelId; | ||
| coreInterop; | ||
| /** | ||
| * Configuration settings for audio operations. | ||
| */ | ||
| settings = new AudioClientSettings(); | ||
| /** | ||
| * @internal | ||
| * Restricted to internal use because CoreInterop is an internal implementation detail. | ||
| * Users should create clients via the Model.createAudioClient() factory method. | ||
| */ | ||
| constructor(modelId, coreInterop) { | ||
| this.modelId = modelId; | ||
| this.coreInterop = coreInterop; | ||
| } | ||
| /** | ||
| * Creates a LiveAudioTranscriptionSession for real-time audio streaming ASR. | ||
| * @returns A LiveAudioTranscriptionSession instance. | ||
| */ | ||
| createLiveTranscriptionSession() { | ||
| return new LiveAudioTranscriptionSession(this.modelId, this.coreInterop); | ||
| } | ||
| /** | ||
| * Validates that the audio file path is a non-empty string. | ||
| * @internal | ||
| */ | ||
| validateAudioFilePath(audioFilePath) { | ||
| if (typeof audioFilePath !== 'string' || audioFilePath.trim() === '') { | ||
| throw new Error('Audio file path must be a non-empty string.'); | ||
| } | ||
| } | ||
| /** | ||
| * Transcribes audio into the input language. | ||
| * @param audioFilePath - Path to the audio file to transcribe. | ||
| * @returns The transcription result. | ||
| * @throws Error - If audioFilePath is invalid or transcription fails. | ||
| */ | ||
| async transcribe(audioFilePath) { | ||
| this.validateAudioFilePath(audioFilePath); | ||
| const request = { | ||
| Model: this.modelId, | ||
| FileName: audioFilePath, | ||
| ...this.settings._serialize() | ||
| }; | ||
| try { | ||
| const response = this.coreInterop.executeCommand("audio_transcribe", { Params: { OpenAICreateRequest: JSON.stringify(request) } }); | ||
| return JSON.parse(response); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Audio transcription failed for model '${this.modelId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error }); | ||
| } | ||
| } | ||
| /** | ||
| * Transcribes audio into the input language using streaming, returning an async iterable of chunks. | ||
| * @param audioFilePath - Path to the audio file to transcribe. | ||
| * @returns An async iterable that yields parsed streaming transcription chunks. | ||
| * @throws Error - If audioFilePath is invalid, or streaming fails. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * for await (const chunk of audioClient.transcribeStreaming('recording.wav')) { | ||
| * process.stdout.write(chunk.text); | ||
| * } | ||
| * ``` | ||
| */ | ||
| transcribeStreaming(audioFilePath) { | ||
| this.validateAudioFilePath(audioFilePath); | ||
| const request = { | ||
| Model: this.modelId, | ||
| FileName: audioFilePath, | ||
| ...this.settings._serialize() | ||
| }; | ||
| // Capture instance properties to local variables because `this` is not | ||
| // accessible inside the [Symbol.asyncIterator]() method below — it's a | ||
| // regular method on the returned object literal, not on the AudioClient. | ||
| const coreInterop = this.coreInterop; | ||
| const modelId = this.modelId; | ||
| // Return an AsyncIterable object. The [Symbol.asyncIterator]() factory | ||
| // is called once when the consumer starts a `for await` loop, and it | ||
| // returns the AsyncIterator (with next() / return() methods). | ||
| return { | ||
| [Symbol.asyncIterator]() { | ||
| // Buffer for chunks received from the native callback. | ||
| // Uses a head index for O(1) dequeue instead of Array.shift() which is O(n). | ||
| // JavaScript's single-threaded event loop ensures no race conditions | ||
| // between the callback pushing chunks and next() consuming them. | ||
| const chunks = []; | ||
| let head = 0; | ||
| let done = false; | ||
| let cancelled = false; | ||
| let error = null; | ||
| let resolve = null; | ||
| let nextInFlight = false; | ||
| const streamingPromise = coreInterop.executeCommandStreaming("audio_transcribe", { Params: { OpenAICreateRequest: JSON.stringify(request) } }, (chunkStr) => { | ||
| if (cancelled || error) | ||
| return; | ||
| if (chunkStr) { | ||
| try { | ||
| const chunk = JSON.parse(chunkStr); | ||
| chunks.push(chunk); | ||
| } | ||
| catch (e) { | ||
| if (!error) { | ||
| error = new Error(`Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); | ||
| } | ||
| } | ||
| } | ||
| // Wake up any waiting next() call | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); | ||
| } | ||
| } | ||
| // When the native stream completes, mark done and wake up any | ||
| // pending next() call so it can see that iteration has ended. | ||
| ).then(() => { | ||
| done = true; | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); // resolve the pending next() promise | ||
| } | ||
| }).catch((err) => { | ||
| if (!error) { | ||
| const underlyingError = err instanceof Error ? err : new Error(String(err)); | ||
| error = new Error(`Streaming audio transcription failed for model '${modelId}': ${underlyingError.message}`, { cause: underlyingError }); | ||
| } | ||
| done = true; | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); | ||
| } | ||
| }); | ||
| // Return the AsyncIterator object consumed by `for await`. | ||
| // next() yields buffered chunks one at a time; return() is | ||
| // called automatically when the consumer breaks out early. | ||
| return { | ||
| async next() { | ||
| if (nextInFlight) { | ||
| throw new Error('next() called concurrently on streaming iterator; await each call before invoking next().'); | ||
| } | ||
| nextInFlight = true; | ||
| try { | ||
| while (true) { | ||
| if (head < chunks.length) { | ||
| const value = chunks[head]; | ||
| chunks[head] = undefined; // allow GC | ||
| head++; | ||
| // Compact the array when all buffered chunks have been consumed | ||
| if (head === chunks.length) { | ||
| chunks.length = 0; | ||
| head = 0; | ||
| } | ||
| return { value, done: false }; | ||
| } | ||
| if (error) { | ||
| throw error; | ||
| } | ||
| if (done || cancelled) { | ||
| return { value: undefined, done: true }; | ||
| } | ||
| // Wait for the next chunk or completion | ||
| await new Promise((r) => { resolve = r; }); | ||
| } | ||
| } | ||
| finally { | ||
| nextInFlight = false; | ||
| } | ||
| }, | ||
| async return() { | ||
| // Mark cancelled so the callback stops buffering. | ||
| // Note: the underlying native stream cannot be cancelled | ||
| // (CoreInterop.executeCommandStreaming has no abort support), | ||
| // so the koffi callback may still fire but will no-op due | ||
| // to the cancelled guard above. | ||
| cancelled = true; | ||
| chunks.length = 0; | ||
| head = 0; | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); | ||
| } | ||
| return { value: undefined, done: true }; | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| } |
| export class ChatClientSettings { | ||
| frequencyPenalty; | ||
| maxTokens; | ||
| n; | ||
| temperature; | ||
| presencePenalty; | ||
| randomSeed; | ||
| topK; | ||
| topP; | ||
| responseFormat; | ||
| toolChoice; | ||
| /** | ||
| * Serializes the settings into an OpenAI-compatible request object. | ||
| * @internal | ||
| */ | ||
| _serialize() { | ||
| // Run internal validations | ||
| this.validateResponseFormat(this.responseFormat); | ||
| this.validateToolChoice(this.toolChoice); | ||
| // Helper function to filter out undefined properties from objects | ||
| const filterUndefined = (obj) => { | ||
| return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)); | ||
| }; | ||
| // Standard OpenAI properties | ||
| const result = { | ||
| frequency_penalty: this.frequencyPenalty, | ||
| max_tokens: this.maxTokens, | ||
| n: this.n, | ||
| presence_penalty: this.presencePenalty, | ||
| temperature: this.temperature, | ||
| top_p: this.topP, | ||
| response_format: this.responseFormat ? filterUndefined(this.responseFormat) : undefined, | ||
| tool_choice: this.toolChoice ? filterUndefined(this.toolChoice) : undefined | ||
| }; | ||
| // Foundry specific metadata properties | ||
| const metadata = {}; | ||
| if (this.topK !== undefined) { | ||
| metadata["top_k"] = this.topK.toString(); | ||
| } | ||
| if (this.randomSeed !== undefined) { | ||
| metadata["random_seed"] = this.randomSeed.toString(); | ||
| } | ||
| if (Object.keys(metadata).length > 0) { | ||
| result.metadata = metadata; | ||
| } | ||
| // Filter out undefined properties | ||
| return filterUndefined(result); | ||
| } | ||
| /** | ||
| * Validates that the provided ResponseFormat object is well-formed. | ||
| * @internal | ||
| * @param format | ||
| */ | ||
| validateResponseFormat(format) { | ||
| if (!format) | ||
| return; | ||
| const validTypes = ['text', 'json_object', 'json_schema', 'lark_grammar']; | ||
| if (!validTypes.includes(format.type)) { | ||
| throw new Error(`ResponseFormat type must be one of: ${validTypes.join(', ')}`); | ||
| } | ||
| const validGrammarTypes = ['json_schema', 'lark_grammar']; | ||
| if (validGrammarTypes.includes(format.type)) { | ||
| if (format.type === 'json_schema' && (typeof format.jsonSchema !== 'string' || format.jsonSchema.trim() === '')) { | ||
| throw new Error('ResponseFormat with type "json_schema" must have a valid jsonSchema string.'); | ||
| } | ||
| if (format.type === 'lark_grammar' && (typeof format.larkGrammar !== 'string' || format.larkGrammar.trim() === '')) { | ||
| throw new Error('ResponseFormat with type "lark_grammar" must have a valid larkGrammar string.'); | ||
| } | ||
| } | ||
| else if (format.jsonSchema || format.larkGrammar) { | ||
| throw new Error(`ResponseFormat with type "${format.type}" should not have jsonSchema or larkGrammar properties.`); | ||
| } | ||
| } | ||
| /** | ||
| * Validates that the provided ToolChoice object is well-formed. | ||
| * @internal | ||
| * @param choice | ||
| */ | ||
| validateToolChoice(choice) { | ||
| if (!choice) | ||
| return; | ||
| const validTypes = ['none', 'auto', 'required', 'function']; | ||
| if (!validTypes.includes(choice.type)) { | ||
| throw new Error(`ToolChoice type must be one of: ${validTypes.join(', ')}`); | ||
| } | ||
| if (choice.type === 'function' && (typeof choice.name !== 'string' || choice.name.trim() === '')) { | ||
| throw new Error('ToolChoice with type "function" must have a valid name string.'); | ||
| } | ||
| else if (choice.type !== 'function' && choice.name) { | ||
| throw new Error(`ToolChoice with type "${choice.type}" should not have a name property.`); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Client for performing chat completions with a loaded model. | ||
| * Follows the OpenAI Chat Completion API structure. | ||
| */ | ||
| export class ChatClient { | ||
| modelId; | ||
| coreInterop; | ||
| /** | ||
| * Configuration settings for chat completions. | ||
| */ | ||
| settings = new ChatClientSettings(); | ||
| /** | ||
| * @internal | ||
| * Restricted to internal use because CoreInterop is an internal implementation detail. | ||
| * Users should create clients via the Model.createChatClient() factory method. | ||
| */ | ||
| constructor(modelId, coreInterop) { | ||
| this.modelId = modelId; | ||
| this.coreInterop = coreInterop; | ||
| } | ||
| /** | ||
| * Validates that messages array is properly formed. | ||
| * @internal | ||
| */ | ||
| validateMessages(messages) { | ||
| if (!messages || !Array.isArray(messages) || messages.length === 0) { | ||
| throw new Error('Messages array cannot be null, undefined, or empty.'); | ||
| } | ||
| for (const msg of messages) { | ||
| if (!msg || typeof msg !== 'object' || Array.isArray(msg)) { | ||
| throw new Error('Each message must be a non-null object with both "role" and "content" properties.'); | ||
| } | ||
| if (typeof msg.role !== 'string' || msg.role.trim() === '') { | ||
| throw new Error('Each message must have a "role" property that is a non-empty string.'); | ||
| } | ||
| if (typeof msg.content !== 'string' || msg.content.trim() === '') { | ||
| throw new Error('Each message must have a "content" property that is a non-empty string.'); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Validates that tools array is properly formed. | ||
| * @internal | ||
| */ | ||
| validateTools(tools) { | ||
| if (!tools) | ||
| return; // tools are optional | ||
| if (!Array.isArray(tools)) { | ||
| throw new Error('Tools must be an array if provided.'); | ||
| } | ||
| for (const tool of tools) { | ||
| if (!tool || typeof tool !== 'object' || Array.isArray(tool)) { | ||
| throw new Error('Each tool must be a non-null object with a valid "type" and "function" definition.'); | ||
| } | ||
| if (typeof tool.type !== 'string' || tool.type.trim() === '') { | ||
| throw new Error('Each tool must have a "type" property that is a non-empty string.'); | ||
| } | ||
| if (!tool.function || typeof tool.function !== 'object') { | ||
| throw new Error('Each tool must have a "function" property that is a non-empty object.'); | ||
| } | ||
| if (typeof tool.function.name !== 'string' || tool.function.name.trim() === '') { | ||
| throw new Error('Each tool\'s function must have a "name" property that is a non-empty string.'); | ||
| } | ||
| if (tool.function.description !== undefined && typeof tool.function.description !== 'string') { | ||
| throw new Error('Each tool\'s function "description", if provided, must be a string.'); | ||
| } | ||
| } | ||
| } | ||
| async completeChat(messages, tools) { | ||
| this.validateMessages(messages); | ||
| this.validateTools(tools); | ||
| const request = { | ||
| model: this.modelId, | ||
| messages, | ||
| ...(tools ? { tools } : {}), | ||
| // stream is undefined (false) by default | ||
| ...this.settings._serialize() | ||
| }; | ||
| try { | ||
| const response = this.coreInterop.executeCommand('chat_completions', { | ||
| Params: { OpenAICreateRequest: JSON.stringify(request) } | ||
| }); | ||
| return JSON.parse(response); | ||
| } | ||
| catch (error) { | ||
| throw new Error(`Chat completion failed for model '${this.modelId}': ${error instanceof Error ? error.message : String(error)}`, { cause: error }); | ||
| } | ||
| } | ||
| completeStreamingChat(messages, tools) { | ||
| this.validateMessages(messages); | ||
| this.validateTools(tools); | ||
| const request = { | ||
| model: this.modelId, | ||
| messages, | ||
| ...(tools ? { tools } : {}), | ||
| stream: true, | ||
| ...this.settings._serialize() | ||
| }; | ||
| // Capture instance properties to local variables because `this` is not | ||
| // accessible inside the [Symbol.asyncIterator]() method below — it's a | ||
| // regular method on the returned object literal, not on the ChatClient. | ||
| const coreInterop = this.coreInterop; | ||
| const modelId = this.modelId; | ||
| // Return an AsyncIterable object. The [Symbol.asyncIterator]() factory | ||
| // is called once when the consumer starts a `for await` loop, and it | ||
| // returns the AsyncIterator (with next() / return() methods). | ||
| return { | ||
| [Symbol.asyncIterator]() { | ||
| // Buffer for chunks received from the native callback. | ||
| // Uses a head index for O(1) dequeue instead of Array.shift() which is O(n). | ||
| // JavaScript's single-threaded event loop ensures no race conditions | ||
| // between the callback pushing chunks and next() consuming them. | ||
| const chunks = []; | ||
| let head = 0; | ||
| let done = false; | ||
| let cancelled = false; | ||
| let error = null; | ||
| let resolve = null; | ||
| let nextInFlight = false; | ||
| const streamingPromise = coreInterop.executeCommandStreaming('chat_completions', { Params: { OpenAICreateRequest: JSON.stringify(request) } }, (chunkStr) => { | ||
| if (cancelled || error) | ||
| return; | ||
| if (chunkStr) { | ||
| try { | ||
| const chunk = JSON.parse(chunkStr); | ||
| chunks.push(chunk); | ||
| } | ||
| catch (e) { | ||
| if (!error) { | ||
| error = new Error(`Failed to parse streaming chunk: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); | ||
| } | ||
| } | ||
| } | ||
| // Wake up any waiting next() call | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); | ||
| } | ||
| } | ||
| // When the native stream completes, mark done and wake up any | ||
| // pending next() call so it can see that iteration has ended. | ||
| ).then(() => { | ||
| done = true; | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); // resolve the pending next() promise | ||
| } | ||
| }).catch((err) => { | ||
| if (!error) { | ||
| const underlyingError = err instanceof Error ? err : new Error(String(err)); | ||
| error = new Error(`Streaming chat completion failed for model '${modelId}': ${underlyingError.message}`, { cause: underlyingError }); | ||
| } | ||
| done = true; | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); | ||
| } | ||
| }); | ||
| // Return the AsyncIterator object consumed by `for await`. | ||
| // next() yields buffered chunks one at a time; return() is | ||
| // called automatically when the consumer breaks out early. | ||
| return { | ||
| async next() { | ||
| if (nextInFlight) { | ||
| throw new Error('next() called concurrently on streaming iterator; await each call before invoking next().'); | ||
| } | ||
| nextInFlight = true; | ||
| try { | ||
| while (true) { | ||
| if (head < chunks.length) { | ||
| const value = chunks[head]; | ||
| chunks[head] = undefined; // allow GC | ||
| head++; | ||
| // Compact the array when all buffered chunks have been consumed | ||
| if (head === chunks.length) { | ||
| chunks.length = 0; | ||
| head = 0; | ||
| } | ||
| return { value, done: false }; | ||
| } | ||
| if (error) { | ||
| throw error; | ||
| } | ||
| if (done || cancelled) { | ||
| return { value: undefined, done: true }; | ||
| } | ||
| // Wait for the next chunk or completion | ||
| await new Promise((r) => { resolve = r; }); | ||
| } | ||
| } | ||
| finally { | ||
| nextInFlight = false; | ||
| } | ||
| }, | ||
| async return() { | ||
| // Mark cancelled so the callback stops buffering. | ||
| // Note: the underlying native stream cannot be cancelled | ||
| // (CoreInterop.executeCommandStreaming has no abort support), | ||
| // so the koffi callback may still fire but will no-op due | ||
| // to the cancelled guard above. | ||
| cancelled = true; | ||
| chunks.length = 0; | ||
| head = 0; | ||
| if (resolve) { | ||
| const r = resolve; | ||
| resolve = null; | ||
| r(); | ||
| } | ||
| return { value: undefined, done: true }; | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| } |
| import { parseTranscriptionResult, tryParseCoreError } from './liveAudioTranscriptionTypes.js'; | ||
| /** | ||
| * Audio format settings for a streaming session. | ||
| * Must be configured before calling start(). | ||
| * Settings are frozen once the session starts. | ||
| */ | ||
| export class LiveAudioTranscriptionOptions { | ||
| /** PCM sample rate in Hz. Default: 16000. */ | ||
| sampleRate = 16000; | ||
| /** Number of audio channels. Default: 1 (mono). */ | ||
| channels = 1; | ||
| /** Bits per sample. Default: 16. */ | ||
| bitsPerSample = 16; | ||
| /** Optional BCP-47 language hint (e.g., "en", "zh"). */ | ||
| language; | ||
| /** Maximum number of audio chunks buffered in the internal push queue. Default: 100. */ | ||
| pushQueueCapacity = 100; | ||
| /** @internal Create a frozen copy of these settings. */ | ||
| snapshot() { | ||
| const copy = new LiveAudioTranscriptionOptions(); | ||
| copy.sampleRate = this.sampleRate; | ||
| copy.channels = this.channels; | ||
| copy.bitsPerSample = this.bitsPerSample; | ||
| copy.language = this.language; | ||
| copy.pushQueueCapacity = this.pushQueueCapacity; | ||
| return Object.freeze(copy); | ||
| } | ||
| } | ||
| /** | ||
| * Internal async queue that acts like C#'s Channel<T>. | ||
| * Supports a single consumer reading via async iteration and multiple producers writing. | ||
| * @internal | ||
| */ | ||
| class AsyncQueue { | ||
| queue = []; | ||
| waitingResolve = null; | ||
| completed = false; | ||
| completionError = null; | ||
| maxCapacity; | ||
| backpressureQueue = []; | ||
| constructor(maxCapacity = Infinity) { | ||
| this.maxCapacity = maxCapacity; | ||
| } | ||
| /** Push an item. If at capacity, waits until space is available. */ | ||
| async write(item) { | ||
| if (this.completed) { | ||
| throw new Error('Cannot write to a completed queue.'); | ||
| } | ||
| if (this.waitingResolve) { | ||
| const resolve = this.waitingResolve; | ||
| this.waitingResolve = null; | ||
| resolve({ value: item, done: false }); | ||
| return; | ||
| } | ||
| while (this.queue.length >= this.maxCapacity) { | ||
| await new Promise((resolve) => { | ||
| this.backpressureQueue.push(resolve); | ||
| }); | ||
| } | ||
| if (this.completed) { | ||
| throw new Error('Cannot write to a completed queue.'); | ||
| } | ||
| this.queue.push(item); | ||
| } | ||
| /** Push an item synchronously (no backpressure wait). Returns false if completed or at capacity. */ | ||
| tryWrite(item) { | ||
| if (this.completed) | ||
| return false; | ||
| if (this.waitingResolve) { | ||
| const resolve = this.waitingResolve; | ||
| this.waitingResolve = null; | ||
| resolve({ value: item, done: false }); | ||
| return true; | ||
| } | ||
| if (this.queue.length >= this.maxCapacity) { | ||
| return false; | ||
| } | ||
| this.queue.push(item); | ||
| return true; | ||
| } | ||
| /** Signal that no more items will be written. */ | ||
| complete(error) { | ||
| if (this.completed) | ||
| return; | ||
| this.completed = true; | ||
| this.completionError = error ?? null; | ||
| // Release all blocked writers | ||
| for (const resolve of this.backpressureQueue) { | ||
| resolve(); | ||
| } | ||
| this.backpressureQueue = []; | ||
| if (this.waitingResolve) { | ||
| const resolve = this.waitingResolve; | ||
| this.waitingResolve = null; | ||
| resolve({ value: undefined, done: true }); | ||
| } | ||
| } | ||
| get error() { | ||
| return this.completionError; | ||
| } | ||
| /** Async iterator for consuming items. */ | ||
| async *[Symbol.asyncIterator]() { | ||
| while (true) { | ||
| if (this.backpressureQueue.length > 0 && this.queue.length < this.maxCapacity) { | ||
| const resolve = this.backpressureQueue.shift(); | ||
| resolve(); | ||
| } | ||
| if (this.queue.length > 0) { | ||
| yield this.queue.shift(); | ||
| continue; | ||
| } | ||
| if (this.completed) { | ||
| if (this.completionError) { | ||
| throw this.completionError; | ||
| } | ||
| return; | ||
| } | ||
| const result = await new Promise((resolve) => { | ||
| this.waitingResolve = resolve; | ||
| }); | ||
| if (result.done) { | ||
| if (this.completionError) { | ||
| throw this.completionError; | ||
| } | ||
| return; | ||
| } | ||
| yield result.value; | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Client for real-time audio streaming ASR (Automatic Speech Recognition). | ||
| * Audio data from a microphone (or other source) is pushed in as PCM chunks, | ||
| * and transcription results are returned as an async iterable. | ||
| * | ||
| * Mirrors the C# LiveAudioTranscriptionSession. | ||
| */ | ||
| export class LiveAudioTranscriptionSession { | ||
| modelId; | ||
| coreInterop; | ||
| sessionHandle = null; | ||
| started = false; | ||
| stopped = false; | ||
| outputQueue = null; | ||
| pushQueue = null; | ||
| pushLoopPromise = null; | ||
| activeSettings = null; | ||
| sessionAbortController = null; | ||
| streamConsumed = false; | ||
| /** | ||
| * Configuration settings for the streaming session. | ||
| * Must be configured before calling start(). Settings are snapshotted at start(); | ||
| * changes made after start() are ignored for the current session. | ||
| */ | ||
| settings = new LiveAudioTranscriptionOptions(); | ||
| /** | ||
| * @internal | ||
| * Users should create sessions via AudioClient.createLiveTranscriptionSession(). | ||
| */ | ||
| constructor(modelId, coreInterop) { | ||
| this.modelId = modelId; | ||
| this.coreInterop = coreInterop; | ||
| } | ||
| /** | ||
| * Start a real-time audio streaming session. | ||
| * Must be called before append() or getTranscriptionStream(). | ||
| * Settings are frozen after this call. | ||
| */ | ||
| async start() { | ||
| if (this.started) { | ||
| throw new Error('Streaming session already started. Call stop() first.'); | ||
| } | ||
| this.activeSettings = this.settings.snapshot(); | ||
| this.outputQueue = new AsyncQueue(); | ||
| this.pushQueue = new AsyncQueue(this.activeSettings.pushQueueCapacity); | ||
| this.streamConsumed = false; | ||
| const params = { | ||
| Model: this.modelId, | ||
| SampleRate: this.activeSettings.sampleRate.toString(), | ||
| Channels: this.activeSettings.channels.toString(), | ||
| BitsPerSample: this.activeSettings.bitsPerSample.toString(), | ||
| }; | ||
| if (this.activeSettings.language) { | ||
| params['Language'] = this.activeSettings.language; | ||
| } | ||
| try { | ||
| const response = this.coreInterop.executeCommand("audio_stream_start", { | ||
| Params: params | ||
| }); | ||
| this.sessionHandle = response; | ||
| if (!this.sessionHandle) { | ||
| throw new Error('Native core did not return a session handle.'); | ||
| } | ||
| } | ||
| catch (error) { | ||
| const err = new Error(`Error starting audio stream session: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); | ||
| this.outputQueue.complete(err); | ||
| throw err; | ||
| } | ||
| this.started = true; | ||
| this.stopped = false; | ||
| this.sessionAbortController = new AbortController(); | ||
| this.pushLoopPromise = this.pushLoop(); | ||
| } | ||
| /** | ||
| * Push a chunk of raw PCM audio data to the streaming session. | ||
| * Can be called from any context. Chunks are internally queued | ||
| * and serialized to native core one at a time. | ||
| * | ||
| * @param pcmData - Raw PCM audio bytes matching the configured format. | ||
| */ | ||
| async append(pcmData) { | ||
| if (!this.started || this.stopped) { | ||
| throw new Error('No active streaming session. Call start() first.'); | ||
| } | ||
| const copy = new Uint8Array(pcmData.length); | ||
| copy.set(pcmData); | ||
| await this.pushQueue.write(copy); | ||
| } | ||
| /** | ||
| * Internal loop that drains the push queue and sends chunks to native core one at a time. | ||
| * Terminates the session on any native error. | ||
| * @internal | ||
| */ | ||
| async pushLoop() { | ||
| try { | ||
| for await (const audioData of this.pushQueue) { | ||
| if (this.sessionAbortController?.signal.aborted) { | ||
| break; | ||
| } | ||
| try { | ||
| const responseData = this.coreInterop.executeCommandWithBinary("audio_stream_push", { | ||
| Params: { | ||
| SessionHandle: this.sessionHandle, | ||
| } | ||
| }, audioData); | ||
| // Parse transcription result from push response and surface it | ||
| if (responseData) { | ||
| try { | ||
| const result = parseTranscriptionResult(responseData); | ||
| const text = result.content?.[0]?.text; | ||
| if (text !== undefined && text !== null && text !== '') { | ||
| this.outputQueue?.tryWrite(result); | ||
| } | ||
| } | ||
| catch { | ||
| // Non-fatal: log and continue if response isn't a transcription result | ||
| } | ||
| } | ||
| } | ||
| catch (error) { | ||
| const errorMsg = error instanceof Error ? error.message : String(error); | ||
| const errorInfo = tryParseCoreError(errorMsg); | ||
| const fatalError = new Error(`Push failed (code=${errorInfo?.code ?? 'UNKNOWN'}): ${errorMsg}`, { cause: error }); | ||
| this.stopped = true; | ||
| this.started = false; | ||
| this.pushQueue?.complete(fatalError); | ||
| this.outputQueue?.complete(fatalError); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| catch (error) { | ||
| if (this.sessionAbortController?.signal.aborted) { | ||
| return; | ||
| } | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| this.outputQueue?.complete(new Error('Push loop terminated unexpectedly.', { cause: err })); | ||
| } | ||
| } | ||
| /** | ||
| * Get the async iterable of transcription results. | ||
| * Results arrive as the native ASR engine processes audio data. | ||
| * | ||
| * Usage: | ||
| * ```ts | ||
| * for await (const result of client.getTranscriptionStream()) { | ||
| * console.log(result.content[0].text); | ||
| * } | ||
| * ``` | ||
| */ | ||
| async *getTranscriptionStream() { | ||
| if (!this.outputQueue) { | ||
| throw new Error('No active streaming session. Call start() first.'); | ||
| } | ||
| if (this.streamConsumed) { | ||
| throw new Error('getTranscriptionStream() can only be called once per session. The output stream has already been consumed.'); | ||
| } | ||
| this.streamConsumed = true; | ||
| for await (const item of this.outputQueue) { | ||
| yield item; | ||
| } | ||
| } | ||
| /** | ||
| * Signal end-of-audio and stop the streaming session. | ||
| * Any remaining buffered audio in the push queue will be drained to native core first. | ||
| * Final results are delivered through getTranscriptionStream() before it completes. | ||
| */ | ||
| async stop() { | ||
| if (!this.started || this.stopped) { | ||
| return; | ||
| } | ||
| this.stopped = true; | ||
| this.pushQueue?.complete(); | ||
| if (this.pushLoopPromise) { | ||
| await this.pushLoopPromise; | ||
| } | ||
| this.sessionAbortController?.abort(); | ||
| let stopError = null; | ||
| try { | ||
| const responseData = this.coreInterop.executeCommand("audio_stream_stop", { | ||
| Params: { SessionHandle: this.sessionHandle } | ||
| }); | ||
| // Parse final transcription from stop response | ||
| if (responseData) { | ||
| try { | ||
| const finalResult = parseTranscriptionResult(responseData); | ||
| if (finalResult.content?.[0]?.text) { | ||
| this.outputQueue?.tryWrite(finalResult); | ||
| } | ||
| } | ||
| catch { | ||
| // Non-fatal | ||
| } | ||
| } | ||
| } | ||
| catch (error) { | ||
| stopError = error instanceof Error ? error : new Error(String(error)); | ||
| } | ||
| this.sessionHandle = null; | ||
| this.started = false; | ||
| this.sessionAbortController = null; | ||
| this.outputQueue?.complete(); | ||
| if (stopError) { | ||
| throw new Error(`Error stopping audio stream session: ${stopError.message}`, { cause: stopError }); | ||
| } | ||
| } | ||
| /** | ||
| * Dispose the client and stop any active session. | ||
| * Safe to call multiple times. | ||
| */ | ||
| async dispose() { | ||
| try { | ||
| if (this.started && !this.stopped) { | ||
| await this.stop(); | ||
| } | ||
| } | ||
| catch { | ||
| // Swallow errors during best-effort cleanup to keep dispose() silent. | ||
| } | ||
| } | ||
| } |
| /** | ||
| * Types for real-time audio streaming transcription results and structured errors. | ||
| * Mirrors the C# LiveAudioTranscriptionResponse (extends ConversationItem) and CoreErrorResponse. | ||
| */ | ||
| /** | ||
| * Parse raw Core JSON response into a LiveAudioTranscriptionResponse. | ||
| * Maps the flat Core format (text, is_final, start_time, end_time) into | ||
| * the ConversationItem-shaped result with content[0].text and content[0].transcript. | ||
| * @internal | ||
| */ | ||
| export function parseTranscriptionResult(json) { | ||
| const raw = JSON.parse(json); | ||
| return { | ||
| id: raw.id ?? null, | ||
| is_final: raw.is_final ?? false, | ||
| start_time: raw.start_time ?? null, | ||
| end_time: raw.end_time ?? null, | ||
| content: [ | ||
| { | ||
| text: raw.text ?? '', | ||
| transcript: raw.text ?? '' | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| /** | ||
| * Attempt to parse a native error string as a structured CoreErrorResponse. | ||
| * Handles both raw JSON and CoreInterop-prefixed messages | ||
| * (e.g., "Command 'X' failed: {...}"). | ||
| * Returns null if no valid CoreErrorResponse JSON is found. | ||
| * @internal | ||
| */ | ||
| export function tryParseCoreError(errorString) { | ||
| // Try raw JSON first, then extract JSON after "failed: " prefix | ||
| const candidates = [errorString]; | ||
| const prefixIdx = errorString.indexOf('failed: '); | ||
| if (prefixIdx !== -1) { | ||
| candidates.push(errorString.substring(prefixIdx + 8)); | ||
| } | ||
| for (const candidate of candidates) { | ||
| try { | ||
| const parsed = JSON.parse(candidate); | ||
| if (typeof parsed.code === 'string' && typeof parsed.message === 'string' && typeof parsed.isTransient === 'boolean') { | ||
| return parsed; | ||
| } | ||
| } | ||
| catch { | ||
| // not valid JSON, try next candidate | ||
| } | ||
| } | ||
| return null; | ||
| } |
| /** | ||
| * Extracts the text content from an assistant message in a Response. | ||
| * Equivalent to OpenAI Python SDK's `response.output_text`. | ||
| * | ||
| * @param response - The Response object. | ||
| * @returns The concatenated text from the first assistant message, or an empty string. | ||
| */ | ||
| export function getOutputText(response) { | ||
| for (const item of response.output) { | ||
| if (item.type === 'message' && item.role === 'assistant') { | ||
| const content = item.content; | ||
| if (typeof content === 'string') | ||
| return content; | ||
| if (Array.isArray(content)) { | ||
| return content | ||
| .filter((p) => 'text' in p) | ||
| .map((p) => p.text) | ||
| .join(''); | ||
| } | ||
| } | ||
| } | ||
| return ''; | ||
| } | ||
| /** | ||
| * Configuration settings for the Responses API client. | ||
| * Properties use camelCase in JS and are serialized to snake_case for the API. | ||
| */ | ||
| export class ResponsesClientSettings { | ||
| /** System-level instructions to guide the model. */ | ||
| instructions; | ||
| temperature; | ||
| topP; | ||
| maxOutputTokens; | ||
| frequencyPenalty; | ||
| presencePenalty; | ||
| toolChoice; | ||
| truncation; | ||
| parallelToolCalls; | ||
| store; | ||
| metadata; | ||
| reasoning; | ||
| text; | ||
| seed; | ||
| /** | ||
| * Serializes settings into an OpenAI Responses API-compatible request object. | ||
| * @internal | ||
| */ | ||
| _serialize() { | ||
| const filterUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)); | ||
| const result = { | ||
| instructions: this.instructions, | ||
| temperature: this.temperature, | ||
| top_p: this.topP, | ||
| max_output_tokens: this.maxOutputTokens, | ||
| frequency_penalty: this.frequencyPenalty, | ||
| presence_penalty: this.presencePenalty, | ||
| tool_choice: this.toolChoice, | ||
| truncation: this.truncation, | ||
| parallel_tool_calls: this.parallelToolCalls, | ||
| store: this.store, | ||
| metadata: this.metadata, | ||
| reasoning: this.reasoning ? filterUndefined(this.reasoning) : undefined, | ||
| text: this.text ? filterUndefined(this.text) : undefined, | ||
| seed: this.seed, | ||
| }; | ||
| // Filter out undefined properties | ||
| return filterUndefined(result); | ||
| } | ||
| } | ||
| /** | ||
| * Client for the OpenAI Responses API served by Foundry Local's embedded web service. | ||
| * | ||
| * Unlike ChatClient/AudioClient (which use FFI via CoreInterop), the Responses API | ||
| * is HTTP-only. This client uses fetch() for all operations and parses Server-Sent Events | ||
| * for streaming. | ||
| * | ||
| * Create via `FoundryLocalManager.createResponsesClient()` or | ||
| * `model.createResponsesClient(baseUrl)`. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const manager = FoundryLocalManager.create({ appName: 'MyApp' }); | ||
| * manager.startWebService(); | ||
| * const client = manager.createResponsesClient('my-model-id'); | ||
| * | ||
| * // Non-streaming | ||
| * const response = await client.create('Hello, world!'); | ||
| * console.log(response.output); | ||
| * | ||
| * // Streaming | ||
| * await client.createStreaming('Tell me a story', (event) => { | ||
| * if (event.type === 'response.output_text.delta') { | ||
| * process.stdout.write(event.delta); | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export class ResponsesClient { | ||
| baseUrl; | ||
| modelId; | ||
| /** | ||
| * Configuration settings for responses. | ||
| */ | ||
| settings = new ResponsesClientSettings(); | ||
| /** | ||
| * @param baseUrl - The base URL of the Foundry Local web service (e.g. "http://127.0.0.1:5273"). | ||
| * @param modelId - Optional default model ID. Can be overridden per-request via options. | ||
| */ | ||
| constructor(baseUrl, modelId) { | ||
| if (!baseUrl || typeof baseUrl !== 'string' || baseUrl.trim() === '') { | ||
| throw new Error('baseUrl must be a non-empty string.'); | ||
| } | ||
| // Strip trailing slashes for consistent URL construction | ||
| let url = baseUrl; | ||
| while (url.endsWith('/')) { | ||
| url = url.slice(0, -1); | ||
| } | ||
| this.baseUrl = url; | ||
| this.modelId = modelId; | ||
| } | ||
| // ======================================================================== | ||
| // Public API | ||
| // ======================================================================== | ||
| /** | ||
| * Creates a model response (non-streaming). | ||
| * @param input - A string prompt or array of input items. | ||
| * @param options - Additional request parameters that override client settings. | ||
| * The `model` field is optional here if a default model was set in the constructor. | ||
| * @returns The completed Response object. Check `response.status` and `response.error` | ||
| * even on success — the server returns HTTP 200 for model-level failures too. | ||
| */ | ||
| async create(input, options) { | ||
| this.validateInput(input); | ||
| if (options?.tools) { | ||
| this.validateTools(options.tools); | ||
| } | ||
| const body = this.buildRequest(input, { ...options, stream: false }); | ||
| const response = await this.fetchJson('/v1/responses', { method: 'POST', body: JSON.stringify(body) }); | ||
| return response; | ||
| } | ||
| /** | ||
| * Creates a model response with streaming via Server-Sent Events. | ||
| * @param input - A string prompt or array of input items. | ||
| * @param callback - Called for each streaming event received. | ||
| * @param options - Additional request parameters that override client settings. | ||
| */ | ||
| async createStreaming(input, callback, options) { | ||
| this.validateInput(input); | ||
| if (options?.tools) { | ||
| this.validateTools(options.tools); | ||
| } | ||
| if (!callback || typeof callback !== 'function') { | ||
| throw new Error('Callback must be a valid function.'); | ||
| } | ||
| const body = this.buildRequest(input, { ...options, stream: true }); | ||
| const res = await this.doFetch('/v1/responses', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| if (!res.body) { | ||
| throw new Error('Streaming response has no body.'); | ||
| } | ||
| let error = null; | ||
| await this.parseSSEStream(res.body, (event) => { | ||
| if (error) | ||
| return; | ||
| try { | ||
| callback(event); | ||
| } | ||
| catch (e) { | ||
| error = new Error(`User callback threw an error: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); | ||
| } | ||
| }); | ||
| if (error) { | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * Retrieves a stored response by ID. | ||
| * @param responseId - The ID of the response to retrieve. | ||
| * @returns The Response object, or throws if not found. | ||
| */ | ||
| async get(responseId) { | ||
| this.validateId(responseId, 'responseId'); | ||
| return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}`, { method: 'GET' }); | ||
| } | ||
| /** | ||
| * Deletes a stored response by ID. | ||
| * @param responseId - The ID of the response to delete. | ||
| * @returns The deletion result. | ||
| */ | ||
| async delete(responseId) { | ||
| this.validateId(responseId, 'responseId'); | ||
| return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}`, { method: 'DELETE' }); | ||
| } | ||
| /** | ||
| * Cancels an in-progress response. | ||
| * @param responseId - The ID of the response to cancel. | ||
| * @returns The cancelled Response object. | ||
| */ | ||
| async cancel(responseId) { | ||
| this.validateId(responseId, 'responseId'); | ||
| return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}/cancel`, { method: 'POST' }); | ||
| } | ||
| /** | ||
| * Retrieves input items for a stored response. | ||
| * @param responseId - The ID of the response. | ||
| * @returns The list of input items. | ||
| */ | ||
| async getInputItems(responseId) { | ||
| this.validateId(responseId, 'responseId'); | ||
| return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}/input_items`, { method: 'GET' }); | ||
| } | ||
| // ======================================================================== | ||
| // Internal helpers | ||
| // ======================================================================== | ||
| /** | ||
| * Builds the full request body by merging input, settings, and per-call options. | ||
| */ | ||
| buildRequest(input, options) { | ||
| const model = options?.model ?? this.modelId; | ||
| if (!model || typeof model !== 'string' || model.trim() === '') { | ||
| throw new Error('Model must be specified either in the constructor, via createResponsesClient(modelId), or in options.model.'); | ||
| } | ||
| const serializedSettings = this.settings._serialize(); | ||
| // Merge order: model+input → settings defaults → per-call overrides | ||
| return { | ||
| model, | ||
| input, | ||
| ...serializedSettings, | ||
| ...options, | ||
| }; | ||
| } | ||
| /** | ||
| * Validates that input is a non-empty string or a non-empty array of items. | ||
| */ | ||
| validateInput(input) { | ||
| if (input === null || input === undefined) { | ||
| throw new Error('Input cannot be null or undefined.'); | ||
| } | ||
| if (typeof input === 'string') { | ||
| if (input.trim() === '') { | ||
| throw new Error('Input string cannot be empty.'); | ||
| } | ||
| return; | ||
| } | ||
| if (Array.isArray(input)) { | ||
| if (input.length === 0) { | ||
| throw new Error('Input items array cannot be empty.'); | ||
| } | ||
| for (const item of input) { | ||
| if (!item || typeof item !== 'object') { | ||
| throw new Error('Each input item must be a non-null object.'); | ||
| } | ||
| if (typeof item.type !== 'string' || item.type.trim() === '') { | ||
| throw new Error('Each input item must have a "type" property that is a non-empty string.'); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| throw new Error('Input must be a string or an array of input items.'); | ||
| } | ||
| /** | ||
| * Validates that tools array is properly formed. | ||
| * Follows the same pattern as ChatClient.validateTools. | ||
| */ | ||
| validateTools(tools) { | ||
| if (!Array.isArray(tools)) { | ||
| throw new Error('Tools must be an array if provided.'); | ||
| } | ||
| for (const tool of tools) { | ||
| if (!tool || typeof tool !== 'object' || Array.isArray(tool)) { | ||
| throw new Error('Each tool must be a non-null object with a valid "type" and "name".'); | ||
| } | ||
| if (tool.type !== 'function') { | ||
| throw new Error('Each tool must have type "function".'); | ||
| } | ||
| if (typeof tool.name !== 'string' || tool.name.trim() === '') { | ||
| throw new Error('Each tool must have a "name" property that is a non-empty string.'); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Validates that a string ID parameter is non-empty and within length bounds. | ||
| */ | ||
| validateId(id, paramName) { | ||
| if (!id || typeof id !== 'string' || id.trim() === '') { | ||
| throw new Error(`${paramName} must be a non-empty string.`); | ||
| } | ||
| if (id.length > 1024) { | ||
| throw new Error(`${paramName} exceeds maximum length (1024).`); | ||
| } | ||
| } | ||
| /** | ||
| * Performs a fetch and parses the JSON response, handling errors. | ||
| */ | ||
| async fetchJson(path, init) { | ||
| const res = await this.doFetch(path, { | ||
| ...init, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| ...(init.headers || {}), | ||
| }, | ||
| }); | ||
| const text = await res.text(); | ||
| try { | ||
| return JSON.parse(text); | ||
| } | ||
| catch { | ||
| throw new Error(`Failed to parse response JSON: ${text.substring(0, 200)}`); | ||
| } | ||
| } | ||
| /** | ||
| * Low-level fetch wrapper with error handling. | ||
| */ | ||
| async doFetch(path, init) { | ||
| const url = `${this.baseUrl}${path}`; | ||
| let res; | ||
| try { | ||
| res = await fetch(url, init); | ||
| } | ||
| catch (e) { | ||
| throw new Error(`Network error calling ${init.method ?? 'GET'} ${path}: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); | ||
| } | ||
| if (!res.ok) { | ||
| const errorText = await res.text().catch(() => res.statusText); | ||
| throw new Error(`Responses API error (${res.status}): ${errorText}`); | ||
| } | ||
| return res; | ||
| } | ||
| /** | ||
| * Parses a Server-Sent Events stream from the fetch response body. | ||
| * Format: "event: {type}\ndata: {json}\n\n" | ||
| * Terminal signal: "data: [DONE]\n\n" | ||
| * Per SSE spec, multiple data: lines within a single event are joined with \n. | ||
| */ | ||
| async parseSSEStream(body, onEvent) { | ||
| const reader = body.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| const bufferParts = []; | ||
| let parseError = null; | ||
| try { | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) | ||
| break; | ||
| bufferParts.push(decoder.decode(value, { stream: true })); | ||
| const buffer = bufferParts.join(''); | ||
| // Process complete SSE blocks (separated by double newlines) | ||
| const blocks = buffer.split('\n\n'); | ||
| // Keep the last (potentially incomplete) block for next iteration | ||
| const incomplete = blocks.pop() ?? ''; | ||
| bufferParts.length = 0; | ||
| if (incomplete) | ||
| bufferParts.push(incomplete); | ||
| for (const block of blocks) { | ||
| if (parseError) | ||
| break; | ||
| const trimmed = block.trim(); | ||
| if (!trimmed) | ||
| continue; | ||
| // Check for terminal signal | ||
| if (trimmed === 'data: [DONE]') { | ||
| return; | ||
| } | ||
| // Parse SSE fields — per spec, multiple data: lines are joined with \n | ||
| const dataLines = []; | ||
| for (const line of trimmed.split('\n')) { | ||
| if (line.startsWith('data: ')) { | ||
| dataLines.push(line.slice(6)); | ||
| } | ||
| else if (line === 'data:') { | ||
| dataLines.push(''); | ||
| } | ||
| // 'event:' field is informational; the type is inside the JSON data | ||
| } | ||
| const eventData = dataLines.length > 0 ? dataLines.join('\n') : undefined; | ||
| if (eventData) { | ||
| try { | ||
| const parsed = JSON.parse(eventData); | ||
| onEvent(parsed); | ||
| } | ||
| catch (e) { | ||
| parseError = new Error(`Failed to parse streaming event: ${e instanceof Error ? e.message : String(e)}`, { cause: e }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| finally { | ||
| reader.releaseLock(); | ||
| } | ||
| if (parseError) { | ||
| throw parseError; | ||
| } | ||
| } | ||
| } |
| // adapted from sdk\cs\src\FoundryModelInfo.cs | ||
| export var DeviceType; | ||
| (function (DeviceType) { | ||
| DeviceType["Invalid"] = "Invalid"; | ||
| DeviceType["CPU"] = "CPU"; | ||
| DeviceType["GPU"] = "GPU"; | ||
| DeviceType["NPU"] = "NPU"; | ||
| })(DeviceType || (DeviceType = {})); |
| const Utils = require("./util"); | ||
| const pth = require("path"); | ||
| const ZipEntry = require("./zipEntry"); | ||
| const ZipFile = require("./zipFile"); | ||
| const get_Bool = (...val) => Utils.findLast(val, (c) => typeof c === "boolean"); | ||
| const get_Str = (...val) => Utils.findLast(val, (c) => typeof c === "string"); | ||
| const get_Fun = (...val) => Utils.findLast(val, (c) => typeof c === "function"); | ||
| const defaultOptions = { | ||
| // option "noSort" : if true it disables files sorting | ||
| noSort: false, | ||
| // read entries during load (initial loading may be slower) | ||
| readEntries: false, | ||
| // default method is none | ||
| method: Utils.Constants.NONE, | ||
| // file system | ||
| fs: null | ||
| }; | ||
| module.exports = function (/**String*/ input, /** object */ options) { | ||
| let inBuffer = null; | ||
| // create object based default options, allowing them to be overwritten | ||
| const opts = Object.assign(Object.create(null), defaultOptions); | ||
| // test input variable | ||
| if (input && "object" === typeof input) { | ||
| // if value is not buffer we accept it to be object with options | ||
| if (!(input instanceof Uint8Array)) { | ||
| Object.assign(opts, input); | ||
| input = opts.input ? opts.input : undefined; | ||
| if (opts.input) delete opts.input; | ||
| } | ||
| // if input is buffer | ||
| if (Buffer.isBuffer(input)) { | ||
| inBuffer = input; | ||
| opts.method = Utils.Constants.BUFFER; | ||
| input = undefined; | ||
| } | ||
| } | ||
| // assign options | ||
| Object.assign(opts, options); | ||
| // instanciate utils filesystem | ||
| const filetools = new Utils(opts); | ||
| if (typeof opts.decoder !== "object" || typeof opts.decoder.encode !== "function" || typeof opts.decoder.decode !== "function") { | ||
| opts.decoder = Utils.decoder; | ||
| } | ||
| // if input is file name we retrieve its content | ||
| if (input && "string" === typeof input) { | ||
| // load zip file | ||
| if (filetools.fs.existsSync(input)) { | ||
| opts.method = Utils.Constants.FILE; | ||
| opts.filename = input; | ||
| inBuffer = filetools.fs.readFileSync(input); | ||
| } else { | ||
| throw Utils.Errors.INVALID_FILENAME(); | ||
| } | ||
| } | ||
| // create variable | ||
| const _zip = new ZipFile(inBuffer, opts); | ||
| const { canonical, sanitize, zipnamefix } = Utils; | ||
| function getEntry(/**Object*/ entry) { | ||
| if (entry && _zip) { | ||
| var item; | ||
| // If entry was given as a file name | ||
| if (typeof entry === "string") item = _zip.getEntry(pth.posix.normalize(entry)); | ||
| // if entry was given as a ZipEntry object | ||
| if (typeof entry === "object" && typeof entry.entryName !== "undefined" && typeof entry.header !== "undefined") item = _zip.getEntry(entry.entryName); | ||
| if (item) { | ||
| return item; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function fixPath(zipPath) { | ||
| const { join, normalize, sep } = pth.posix; | ||
| // convert windows file separators and normalize | ||
| return join(pth.isAbsolute(zipPath) ? "/": '.', normalize(sep + zipPath.split("\\").join(sep) + sep)); | ||
| } | ||
| function filenameFilter(filterfn) { | ||
| if (filterfn instanceof RegExp) { | ||
| // if filter is RegExp wrap it | ||
| return (function (rx) { | ||
| return function (filename) { | ||
| return rx.test(filename); | ||
| }; | ||
| })(filterfn); | ||
| } else if ("function" !== typeof filterfn) { | ||
| // if filter is not function we will replace it | ||
| return () => true; | ||
| } | ||
| return filterfn; | ||
| } | ||
| // keep last character on folders | ||
| const relativePath = (local, entry) => { | ||
| let lastChar = entry.slice(-1); | ||
| lastChar = lastChar === filetools.sep ? filetools.sep : ""; | ||
| return pth.relative(local, entry) + lastChar; | ||
| }; | ||
| return { | ||
| /** | ||
| * Extracts the given entry from the archive and returns the content as a Buffer object | ||
| * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry | ||
| * @param {Buffer|string} [pass] - password | ||
| * @return Buffer or Null in case of error | ||
| */ | ||
| readFile: function (entry, pass) { | ||
| var item = getEntry(entry); | ||
| return (item && item.getData(pass)) || null; | ||
| }, | ||
| /** | ||
| * Returns how many child elements has on entry (directories) on files it is always 0 | ||
| * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry | ||
| * @returns {integer} | ||
| */ | ||
| childCount: function (entry) { | ||
| const item = getEntry(entry); | ||
| if (item) { | ||
| return _zip.getChildCount(item); | ||
| } | ||
| }, | ||
| /** | ||
| * Asynchronous readFile | ||
| * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry | ||
| * @param {callback} callback | ||
| * | ||
| * @return Buffer or Null in case of error | ||
| */ | ||
| readFileAsync: function (entry, callback) { | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| item.getDataAsync(callback); | ||
| } else { | ||
| callback(null, "getEntry failed for:" + entry); | ||
| } | ||
| }, | ||
| /** | ||
| * Extracts the given entry from the archive and returns the content as plain text in the given encoding | ||
| * @param {ZipEntry|string} entry - ZipEntry object or String with the full path of the entry | ||
| * @param {string} encoding - Optional. If no encoding is specified utf8 is used | ||
| * | ||
| * @return String | ||
| */ | ||
| readAsText: function (entry, encoding) { | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| var data = item.getData(); | ||
| if (data && data.length) { | ||
| return data.toString(encoding || "utf8"); | ||
| } | ||
| } | ||
| return ""; | ||
| }, | ||
| /** | ||
| * Asynchronous readAsText | ||
| * @param {ZipEntry|string} entry ZipEntry object or String with the full path of the entry | ||
| * @param {callback} callback | ||
| * @param {string} [encoding] - Optional. If no encoding is specified utf8 is used | ||
| * | ||
| * @return String | ||
| */ | ||
| readAsTextAsync: function (entry, callback, encoding) { | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| item.getDataAsync(function (data, err) { | ||
| if (err) { | ||
| callback(data, err); | ||
| return; | ||
| } | ||
| if (data && data.length) { | ||
| callback(data.toString(encoding || "utf8")); | ||
| } else { | ||
| callback(""); | ||
| } | ||
| }); | ||
| } else { | ||
| callback(""); | ||
| } | ||
| }, | ||
| /** | ||
| * Remove the entry from the file or the entry and all it's nested directories and files if the given entry is a directory | ||
| * | ||
| * @param {ZipEntry|string} entry | ||
| * @returns {void} | ||
| */ | ||
| deleteFile: function (entry, withsubfolders = true) { | ||
| // @TODO: test deleteFile | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| _zip.deleteFile(item.entryName, withsubfolders); | ||
| } | ||
| }, | ||
| /** | ||
| * Remove the entry from the file or directory without affecting any nested entries | ||
| * | ||
| * @param {ZipEntry|string} entry | ||
| * @returns {void} | ||
| */ | ||
| deleteEntry: function (entry) { | ||
| // @TODO: test deleteEntry | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| _zip.deleteEntry(item.entryName); | ||
| } | ||
| }, | ||
| /** | ||
| * Adds a comment to the zip. The zip must be rewritten after adding the comment. | ||
| * | ||
| * @param {string} comment | ||
| */ | ||
| addZipComment: function (comment) { | ||
| // @TODO: test addZipComment | ||
| _zip.comment = comment; | ||
| }, | ||
| /** | ||
| * Returns the zip comment | ||
| * | ||
| * @return String | ||
| */ | ||
| getZipComment: function () { | ||
| return _zip.comment || ""; | ||
| }, | ||
| /** | ||
| * Adds a comment to a specified zipEntry. The zip must be rewritten after adding the comment | ||
| * The comment cannot exceed 65535 characters in length | ||
| * | ||
| * @param {ZipEntry} entry | ||
| * @param {string} comment | ||
| */ | ||
| addZipEntryComment: function (entry, comment) { | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| item.comment = comment; | ||
| } | ||
| }, | ||
| /** | ||
| * Returns the comment of the specified entry | ||
| * | ||
| * @param {ZipEntry} entry | ||
| * @return String | ||
| */ | ||
| getZipEntryComment: function (entry) { | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| return item.comment || ""; | ||
| } | ||
| return ""; | ||
| }, | ||
| /** | ||
| * Updates the content of an existing entry inside the archive. The zip must be rewritten after updating the content | ||
| * | ||
| * @param {ZipEntry} entry | ||
| * @param {Buffer} content | ||
| */ | ||
| updateFile: function (entry, content) { | ||
| var item = getEntry(entry); | ||
| if (item) { | ||
| item.setData(content); | ||
| } | ||
| }, | ||
| /** | ||
| * Adds a file from the disk to the archive | ||
| * | ||
| * @param {string} localPath File to add to zip | ||
| * @param {string} [zipPath] Optional path inside the zip | ||
| * @param {string} [zipName] Optional name for the file | ||
| * @param {string} [comment] Optional file comment | ||
| */ | ||
| addLocalFile: function (localPath, zipPath, zipName, comment) { | ||
| if (filetools.fs.existsSync(localPath)) { | ||
| // fix ZipPath | ||
| zipPath = zipPath ? fixPath(zipPath) : ""; | ||
| // p - local file name | ||
| const p = pth.win32.basename(pth.win32.normalize(localPath)); | ||
| // add file name into zippath | ||
| zipPath += zipName ? zipName : p; | ||
| // read file attributes | ||
| const _attr = filetools.fs.statSync(localPath); | ||
| // get file content | ||
| const data = _attr.isFile() ? filetools.fs.readFileSync(localPath) : Buffer.alloc(0); | ||
| // if folder | ||
| if (_attr.isDirectory()) zipPath += filetools.sep; | ||
| // add file into zip file | ||
| this.addFile(zipPath, data, comment, _attr); | ||
| } else { | ||
| throw Utils.Errors.FILE_NOT_FOUND(localPath); | ||
| } | ||
| }, | ||
| /** | ||
| * Callback for showing if everything was done. | ||
| * | ||
| * @callback doneCallback | ||
| * @param {Error} err - Error object | ||
| * @param {boolean} done - was request fully completed | ||
| */ | ||
| /** | ||
| * Adds a file from the disk to the archive | ||
| * | ||
| * @param {(object|string)} options - options object, if it is string it us used as localPath. | ||
| * @param {string} options.localPath - Local path to the file. | ||
| * @param {string} [options.comment] - Optional file comment. | ||
| * @param {string} [options.zipPath] - Optional path inside the zip | ||
| * @param {string} [options.zipName] - Optional name for the file | ||
| * @param {doneCallback} callback - The callback that handles the response. | ||
| */ | ||
| addLocalFileAsync: function (options, callback) { | ||
| options = typeof options === "object" ? options : { localPath: options }; | ||
| const localPath = pth.resolve(options.localPath); | ||
| const { comment } = options; | ||
| let { zipPath, zipName } = options; | ||
| const self = this; | ||
| filetools.fs.stat(localPath, function (err, stats) { | ||
| if (err) return callback(err, false); | ||
| // fix ZipPath | ||
| zipPath = zipPath ? fixPath(zipPath) : ""; | ||
| // p - local file name | ||
| const p = pth.win32.basename(pth.win32.normalize(localPath)); | ||
| // add file name into zippath | ||
| zipPath += zipName ? zipName : p; | ||
| if (stats.isFile()) { | ||
| filetools.fs.readFile(localPath, function (err, data) { | ||
| if (err) return callback(err, false); | ||
| self.addFile(zipPath, data, comment, stats); | ||
| return setImmediate(callback, undefined, true); | ||
| }); | ||
| } else if (stats.isDirectory()) { | ||
| zipPath += filetools.sep; | ||
| self.addFile(zipPath, Buffer.alloc(0), comment, stats); | ||
| return setImmediate(callback, undefined, true); | ||
| } | ||
| }); | ||
| }, | ||
| /** | ||
| * Adds a local directory and all its nested files and directories to the archive | ||
| * | ||
| * @param {string} localPath - local path to the folder | ||
| * @param {string} [zipPath] - optional path inside zip | ||
| * @param {(RegExp|function)} [filter] - optional RegExp or Function if files match will be included. | ||
| */ | ||
| addLocalFolder: function (localPath, zipPath, filter) { | ||
| // Prepare filter | ||
| filter = filenameFilter(filter); | ||
| // fix ZipPath | ||
| zipPath = zipPath ? fixPath(zipPath) : ""; | ||
| // normalize the path first | ||
| localPath = pth.normalize(localPath); | ||
| if (filetools.fs.existsSync(localPath)) { | ||
| const items = filetools.findFiles(localPath); | ||
| const self = this; | ||
| if (items.length) { | ||
| for (const filepath of items) { | ||
| const p = pth.join(zipPath, relativePath(localPath, filepath)); | ||
| if (filter(p)) { | ||
| self.addLocalFile(filepath, pth.dirname(p)); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| throw Utils.Errors.FILE_NOT_FOUND(localPath); | ||
| } | ||
| }, | ||
| /** | ||
| * Asynchronous addLocalFolder | ||
| * @param {string} localPath | ||
| * @param {callback} callback | ||
| * @param {string} [zipPath] optional path inside zip | ||
| * @param {RegExp|function} [filter] optional RegExp or Function if files match will | ||
| * be included. | ||
| */ | ||
| addLocalFolderAsync: function (localPath, callback, zipPath, filter) { | ||
| // Prepare filter | ||
| filter = filenameFilter(filter); | ||
| // fix ZipPath | ||
| zipPath = zipPath ? fixPath(zipPath) : ""; | ||
| // normalize the path first | ||
| localPath = pth.normalize(localPath); | ||
| var self = this; | ||
| filetools.fs.open(localPath, "r", function (err) { | ||
| if (err && err.code === "ENOENT") { | ||
| callback(undefined, Utils.Errors.FILE_NOT_FOUND(localPath)); | ||
| } else if (err) { | ||
| callback(undefined, err); | ||
| } else { | ||
| var items = filetools.findFiles(localPath); | ||
| var i = -1; | ||
| var next = function () { | ||
| i += 1; | ||
| if (i < items.length) { | ||
| var filepath = items[i]; | ||
| var p = relativePath(localPath, filepath).split("\\").join("/"); //windows fix | ||
| p = p | ||
| .normalize("NFD") | ||
| .replace(/[\u0300-\u036f]/g, "") | ||
| .replace(/[^\x20-\x7E]/g, ""); // accent fix | ||
| if (filter(p)) { | ||
| filetools.fs.stat(filepath, function (er0, stats) { | ||
| if (er0) callback(undefined, er0); | ||
| if (stats.isFile()) { | ||
| filetools.fs.readFile(filepath, function (er1, data) { | ||
| if (er1) { | ||
| callback(undefined, er1); | ||
| } else { | ||
| self.addFile(zipPath + p, data, "", stats); | ||
| next(); | ||
| } | ||
| }); | ||
| } else { | ||
| self.addFile(zipPath + p + "/", Buffer.alloc(0), "", stats); | ||
| next(); | ||
| } | ||
| }); | ||
| } else { | ||
| process.nextTick(() => { | ||
| next(); | ||
| }); | ||
| } | ||
| } else { | ||
| callback(true, undefined); | ||
| } | ||
| }; | ||
| next(); | ||
| } | ||
| }); | ||
| }, | ||
| /** | ||
| * Adds a local directory and all its nested files and directories to the archive | ||
| * | ||
| * @param {object | string} options - options object, if it is string it us used as localPath. | ||
| * @param {string} options.localPath - Local path to the folder. | ||
| * @param {string} [options.zipPath] - optional path inside zip. | ||
| * @param {RegExp|function} [options.filter] - optional RegExp or Function if files match will be included. | ||
| * @param {function|string} [options.namefix] - optional function to help fix filename | ||
| * @param {doneCallback} callback - The callback that handles the response. | ||
| * | ||
| */ | ||
| addLocalFolderAsync2: function (options, callback) { | ||
| const self = this; | ||
| options = typeof options === "object" ? options : { localPath: options }; | ||
| localPath = pth.resolve(fixPath(options.localPath)); | ||
| let { zipPath, filter, namefix } = options; | ||
| if (filter instanceof RegExp) { | ||
| filter = (function (rx) { | ||
| return function (filename) { | ||
| return rx.test(filename); | ||
| }; | ||
| })(filter); | ||
| } else if ("function" !== typeof filter) { | ||
| filter = function () { | ||
| return true; | ||
| }; | ||
| } | ||
| // fix ZipPath | ||
| zipPath = zipPath ? fixPath(zipPath) : ""; | ||
| // Check Namefix function | ||
| if (namefix == "latin1") { | ||
| namefix = (str) => | ||
| str | ||
| .normalize("NFD") | ||
| .replace(/[\u0300-\u036f]/g, "") | ||
| .replace(/[^\x20-\x7E]/g, ""); // accent fix (latin1 characers only) | ||
| } | ||
| if (typeof namefix !== "function") namefix = (str) => str; | ||
| // internal, create relative path + fix the name | ||
| const relPathFix = (entry) => pth.join(zipPath, namefix(relativePath(localPath, entry))); | ||
| const fileNameFix = (entry) => pth.win32.basename(pth.win32.normalize(namefix(entry))); | ||
| filetools.fs.open(localPath, "r", function (err) { | ||
| if (err && err.code === "ENOENT") { | ||
| callback(undefined, Utils.Errors.FILE_NOT_FOUND(localPath)); | ||
| } else if (err) { | ||
| callback(undefined, err); | ||
| } else { | ||
| filetools.findFilesAsync(localPath, function (err, fileEntries) { | ||
| if (err) return callback(err); | ||
| fileEntries = fileEntries.filter((dir) => filter(relPathFix(dir))); | ||
| if (!fileEntries.length) callback(undefined, false); | ||
| setImmediate( | ||
| fileEntries.reverse().reduce(function (next, entry) { | ||
| return function (err, done) { | ||
| if (err || done === false) return setImmediate(next, err, false); | ||
| self.addLocalFileAsync( | ||
| { | ||
| localPath: entry, | ||
| zipPath: pth.dirname(relPathFix(entry)), | ||
| zipName: fileNameFix(entry) | ||
| }, | ||
| next | ||
| ); | ||
| }; | ||
| }, callback) | ||
| ); | ||
| }); | ||
| } | ||
| }); | ||
| }, | ||
| /** | ||
| * Adds a local directory and all its nested files and directories to the archive | ||
| * | ||
| * @param {string} localPath - path where files will be extracted | ||
| * @param {object} props - optional properties | ||
| * @param {string} [props.zipPath] - optional path inside zip | ||
| * @param {RegExp|function} [props.filter] - optional RegExp or Function if files match will be included. | ||
| * @param {function|string} [props.namefix] - optional function to help fix filename | ||
| */ | ||
| addLocalFolderPromise: function (localPath, props) { | ||
| return new Promise((resolve, reject) => { | ||
| this.addLocalFolderAsync2(Object.assign({ localPath }, props), (err, done) => { | ||
| if (err) reject(err); | ||
| if (done) resolve(this); | ||
| }); | ||
| }); | ||
| }, | ||
| /** | ||
| * Allows you to create a entry (file or directory) in the zip file. | ||
| * If you want to create a directory the entryName must end in / and a null buffer should be provided. | ||
| * Comment and attributes are optional | ||
| * | ||
| * @param {string} entryName | ||
| * @param {Buffer | string} content - file content as buffer or utf8 coded string | ||
| * @param {string} [comment] - file comment | ||
| * @param {number | object} [attr] - number as unix file permissions, object as filesystem Stats object | ||
| */ | ||
| addFile: function (entryName, content, comment, attr) { | ||
| entryName = zipnamefix(entryName); | ||
| let entry = getEntry(entryName); | ||
| const update = entry != null; | ||
| // prepare new entry | ||
| if (!update) { | ||
| entry = new ZipEntry(opts); | ||
| entry.entryName = entryName; | ||
| } | ||
| entry.comment = comment || ""; | ||
| const isStat = "object" === typeof attr && attr instanceof filetools.fs.Stats; | ||
| // last modification time from file stats | ||
| if (isStat) { | ||
| entry.header.time = attr.mtime; | ||
| } | ||
| // Set file attribute | ||
| var fileattr = entry.isDirectory ? 0x10 : 0; // (MS-DOS directory flag) | ||
| // extended attributes field for Unix | ||
| // set file type either S_IFDIR / S_IFREG | ||
| let unix = entry.isDirectory ? 0x4000 : 0x8000; | ||
| if (isStat) { | ||
| // File attributes from file stats | ||
| unix |= 0xfff & attr.mode; | ||
| } else if ("number" === typeof attr) { | ||
| // attr from given attr values | ||
| unix |= 0xfff & attr; | ||
| } else { | ||
| // Default values: | ||
| unix |= entry.isDirectory ? 0o755 : 0o644; // permissions (drwxr-xr-x) or (-r-wr--r--) | ||
| } | ||
| fileattr = (fileattr | (unix << 16)) >>> 0; // add attributes | ||
| entry.attr = fileattr; | ||
| entry.setData(content); | ||
| if (!update) _zip.setEntry(entry); | ||
| return entry; | ||
| }, | ||
| /** | ||
| * Returns an array of ZipEntry objects representing the files and folders inside the archive | ||
| * | ||
| * @param {string} [password] | ||
| * @returns Array | ||
| */ | ||
| getEntries: function (password) { | ||
| _zip.password = password; | ||
| return _zip ? _zip.entries : []; | ||
| }, | ||
| /** | ||
| * Returns a ZipEntry object representing the file or folder specified by ``name``. | ||
| * | ||
| * @param {string} name | ||
| * @return ZipEntry | ||
| */ | ||
| getEntry: function (/**String*/ name) { | ||
| return getEntry(name); | ||
| }, | ||
| getEntryCount: function () { | ||
| return _zip.getEntryCount(); | ||
| }, | ||
| forEach: function (callback) { | ||
| return _zip.forEach(callback); | ||
| }, | ||
| /** | ||
| * Extracts the given entry to the given targetPath | ||
| * If the entry is a directory inside the archive, the entire directory and it's subdirectories will be extracted | ||
| * | ||
| * @param {string|ZipEntry} entry - ZipEntry object or String with the full path of the entry | ||
| * @param {string} targetPath - Target folder where to write the file | ||
| * @param {boolean} [maintainEntryPath=true] - If maintainEntryPath is true and the entry is inside a folder, the entry folder will be created in targetPath as well. Default is TRUE | ||
| * @param {boolean} [overwrite=false] - If the file already exists at the target path, the file will be overwriten if this is true. | ||
| * @param {boolean} [keepOriginalPermission=false] - The file will be set as the permission from the entry if this is true. | ||
| * @param {string} [outFileName] - String If set will override the filename of the extracted file (Only works if the entry is a file) | ||
| * | ||
| * @return Boolean | ||
| */ | ||
| extractEntryTo: function (entry, targetPath, maintainEntryPath, overwrite, keepOriginalPermission, outFileName) { | ||
| overwrite = get_Bool(false, overwrite); | ||
| keepOriginalPermission = get_Bool(false, keepOriginalPermission); | ||
| maintainEntryPath = get_Bool(true, maintainEntryPath); | ||
| outFileName = get_Str(keepOriginalPermission, outFileName); | ||
| var item = getEntry(entry); | ||
| if (!item) { | ||
| throw Utils.Errors.NO_ENTRY(); | ||
| } | ||
| var entryName = canonical(item.entryName); | ||
| var target = sanitize(targetPath, outFileName && !item.isDirectory ? outFileName : maintainEntryPath ? entryName : pth.basename(entryName)); | ||
| if (item.isDirectory) { | ||
| var children = _zip.getEntryChildren(item); | ||
| children.forEach(function (child) { | ||
| if (child.isDirectory) return; | ||
| var content = child.getData(); | ||
| if (!content) { | ||
| throw Utils.Errors.CANT_EXTRACT_FILE(); | ||
| } | ||
| var name = canonical(child.entryName); | ||
| var childName = sanitize(targetPath, maintainEntryPath ? name : pth.basename(name)); | ||
| // The reverse operation for attr depend on method addFile() | ||
| const fileAttr = keepOriginalPermission ? child.header.fileAttr : undefined; | ||
| filetools.writeFileTo(childName, content, overwrite, fileAttr); | ||
| }); | ||
| return true; | ||
| } | ||
| var content = item.getData(_zip.password); | ||
| if (!content) throw Utils.Errors.CANT_EXTRACT_FILE(); | ||
| if (filetools.fs.existsSync(target) && !overwrite) { | ||
| throw Utils.Errors.CANT_OVERRIDE(); | ||
| } | ||
| // The reverse operation for attr depend on method addFile() | ||
| const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; | ||
| filetools.writeFileTo(target, content, overwrite, fileAttr); | ||
| return true; | ||
| }, | ||
| /** | ||
| * Test the archive | ||
| * @param {string} [pass] | ||
| */ | ||
| test: function (pass) { | ||
| if (!_zip) { | ||
| return false; | ||
| } | ||
| for (var entry in _zip.entries) { | ||
| try { | ||
| if (entry.isDirectory) { | ||
| continue; | ||
| } | ||
| var content = _zip.entries[entry].getData(pass); | ||
| if (!content) { | ||
| return false; | ||
| } | ||
| } catch (err) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| }, | ||
| /** | ||
| * Extracts the entire archive to the given location | ||
| * | ||
| * @param {string} targetPath Target location | ||
| * @param {boolean} [overwrite=false] If the file already exists at the target path, the file will be overwriten if this is true. | ||
| * Default is FALSE | ||
| * @param {boolean} [keepOriginalPermission=false] The file will be set as the permission from the entry if this is true. | ||
| * Default is FALSE | ||
| * @param {string|Buffer} [pass] password | ||
| */ | ||
| extractAllTo: function (targetPath, overwrite, keepOriginalPermission, pass) { | ||
| keepOriginalPermission = get_Bool(false, keepOriginalPermission); | ||
| pass = get_Str(keepOriginalPermission, pass); | ||
| overwrite = get_Bool(false, overwrite); | ||
| if (!_zip) throw Utils.Errors.NO_ZIP(); | ||
| _zip.entries.forEach(function (entry) { | ||
| var entryName = sanitize(targetPath, canonical(entry.entryName)); | ||
| if (entry.isDirectory) { | ||
| filetools.makeDir(entryName); | ||
| return; | ||
| } | ||
| var content = entry.getData(pass); | ||
| if (!content) { | ||
| throw Utils.Errors.CANT_EXTRACT_FILE(); | ||
| } | ||
| // The reverse operation for attr depend on method addFile() | ||
| const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; | ||
| filetools.writeFileTo(entryName, content, overwrite, fileAttr); | ||
| try { | ||
| filetools.fs.utimesSync(entryName, entry.header.time, entry.header.time); | ||
| } catch (err) { | ||
| throw Utils.Errors.CANT_EXTRACT_FILE(); | ||
| } | ||
| }); | ||
| }, | ||
| /** | ||
| * Asynchronous extractAllTo | ||
| * | ||
| * @param {string} targetPath Target location | ||
| * @param {boolean} [overwrite=false] If the file already exists at the target path, the file will be overwriten if this is true. | ||
| * Default is FALSE | ||
| * @param {boolean} [keepOriginalPermission=false] The file will be set as the permission from the entry if this is true. | ||
| * Default is FALSE | ||
| * @param {function} callback The callback will be executed when all entries are extracted successfully or any error is thrown. | ||
| */ | ||
| extractAllToAsync: function (targetPath, overwrite, keepOriginalPermission, callback) { | ||
| callback = get_Fun(overwrite, keepOriginalPermission, callback); | ||
| keepOriginalPermission = get_Bool(false, keepOriginalPermission); | ||
| overwrite = get_Bool(false, overwrite); | ||
| if (!callback) { | ||
| return new Promise((resolve, reject) => { | ||
| this.extractAllToAsync(targetPath, overwrite, keepOriginalPermission, function (err) { | ||
| if (err) { | ||
| reject(err); | ||
| } else { | ||
| resolve(this); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| if (!_zip) { | ||
| callback(Utils.Errors.NO_ZIP()); | ||
| return; | ||
| } | ||
| targetPath = pth.resolve(targetPath); | ||
| // convert entryName to | ||
| const getPath = (entry) => sanitize(targetPath, pth.normalize(canonical(entry.entryName))); | ||
| const getError = (msg, file) => new Error(msg + ': "' + file + '"'); | ||
| // separate directories from files | ||
| const dirEntries = []; | ||
| const fileEntries = []; | ||
| _zip.entries.forEach((e) => { | ||
| if (e.isDirectory) { | ||
| dirEntries.push(e); | ||
| } else { | ||
| fileEntries.push(e); | ||
| } | ||
| }); | ||
| // Create directory entries first synchronously | ||
| // this prevents race condition and assures folders are there before writing files | ||
| for (const entry of dirEntries) { | ||
| const dirPath = getPath(entry); | ||
| // The reverse operation for attr depend on method addFile() | ||
| const dirAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; | ||
| try { | ||
| filetools.makeDir(dirPath); | ||
| if (dirAttr) filetools.fs.chmodSync(dirPath, dirAttr); | ||
| // in unix timestamp will change if files are later added to folder, but still | ||
| filetools.fs.utimesSync(dirPath, entry.header.time, entry.header.time); | ||
| } catch (er) { | ||
| callback(getError("Unable to create folder", dirPath)); | ||
| } | ||
| } | ||
| fileEntries.reverse().reduce(function (next, entry) { | ||
| return function (err) { | ||
| if (err) { | ||
| next(err); | ||
| } else { | ||
| const entryName = pth.normalize(canonical(entry.entryName)); | ||
| const filePath = sanitize(targetPath, entryName); | ||
| entry.getDataAsync(function (content, err_1) { | ||
| if (err_1) { | ||
| next(err_1); | ||
| } else if (!content) { | ||
| next(Utils.Errors.CANT_EXTRACT_FILE()); | ||
| } else { | ||
| // The reverse operation for attr depend on method addFile() | ||
| const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; | ||
| filetools.writeFileToAsync(filePath, content, overwrite, fileAttr, function (succ) { | ||
| if (!succ) { | ||
| next(getError("Unable to write file", filePath)); | ||
| } | ||
| filetools.fs.utimes(filePath, entry.header.time, entry.header.time, function (err_2) { | ||
| if (err_2) { | ||
| next(getError("Unable to set times", filePath)); | ||
| } else { | ||
| next(); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
| }, callback)(); | ||
| }, | ||
| /** | ||
| * Writes the newly created zip file to disk at the specified location or if a zip was opened and no ``targetFileName`` is provided, it will overwrite the opened zip | ||
| * | ||
| * @param {string} targetFileName | ||
| * @param {function} callback | ||
| */ | ||
| writeZip: function (targetFileName, callback) { | ||
| if (arguments.length === 1) { | ||
| if (typeof targetFileName === "function") { | ||
| callback = targetFileName; | ||
| targetFileName = ""; | ||
| } | ||
| } | ||
| if (!targetFileName && opts.filename) { | ||
| targetFileName = opts.filename; | ||
| } | ||
| if (!targetFileName) return; | ||
| var zipData = _zip.compressToBuffer(); | ||
| if (zipData) { | ||
| var ok = filetools.writeFileTo(targetFileName, zipData, true); | ||
| if (typeof callback === "function") callback(!ok ? new Error("failed") : null, ""); | ||
| } | ||
| }, | ||
| /** | ||
| * | ||
| * @param {string} targetFileName | ||
| * @param {object} [props] | ||
| * @param {boolean} [props.overwrite=true] If the file already exists at the target path, the file will be overwriten if this is true. | ||
| * @param {boolean} [props.perm] The file will be set as the permission from the entry if this is true. | ||
| * @returns {Promise<void>} | ||
| */ | ||
| writeZipPromise: function (/**String*/ targetFileName, /* object */ props) { | ||
| const { overwrite, perm } = Object.assign({ overwrite: true }, props); | ||
| return new Promise((resolve, reject) => { | ||
| // find file name | ||
| if (!targetFileName && opts.filename) targetFileName = opts.filename; | ||
| if (!targetFileName) reject("ADM-ZIP: ZIP File Name Missing"); | ||
| this.toBufferPromise().then((zipData) => { | ||
| const ret = (done) => (done ? resolve(done) : reject("ADM-ZIP: Wasn't able to write zip file")); | ||
| filetools.writeFileToAsync(targetFileName, zipData, overwrite, perm, ret); | ||
| }, reject); | ||
| }); | ||
| }, | ||
| /** | ||
| * @returns {Promise<Buffer>} A promise to the Buffer. | ||
| */ | ||
| toBufferPromise: function () { | ||
| return new Promise((resolve, reject) => { | ||
| _zip.toAsyncBuffer(resolve, reject); | ||
| }); | ||
| }, | ||
| /** | ||
| * Returns the content of the entire zip file as a Buffer object | ||
| * | ||
| * @prop {function} [onSuccess] | ||
| * @prop {function} [onFail] | ||
| * @prop {function} [onItemStart] | ||
| * @prop {function} [onItemEnd] | ||
| * @returns {Buffer} | ||
| */ | ||
| toBuffer: function (onSuccess, onFail, onItemStart, onItemEnd) { | ||
| if (typeof onSuccess === "function") { | ||
| _zip.toAsyncBuffer(onSuccess, onFail, onItemStart, onItemEnd); | ||
| return null; | ||
| } | ||
| return _zip.compressToBuffer(); | ||
| } | ||
| }; | ||
| }; |
| var Utils = require("../util"), | ||
| Constants = Utils.Constants; | ||
| /* The central directory file header */ | ||
| module.exports = function () { | ||
| var _verMade = 20, // v2.0 | ||
| _version = 10, // v1.0 | ||
| _flags = 0, | ||
| _method = 0, | ||
| _time = 0, | ||
| _crc = 0, | ||
| _compressedSize = 0, | ||
| _size = 0, | ||
| _fnameLen = 0, | ||
| _extraLen = 0, | ||
| _comLen = 0, | ||
| _diskStart = 0, | ||
| _inattr = 0, | ||
| _attr = 0, | ||
| _offset = 0; | ||
| _verMade |= Utils.isWin ? 0x0a00 : 0x0300; | ||
| // Set EFS flag since filename and comment fields are all by default encoded using UTF-8. | ||
| // Without it file names may be corrupted for other apps when file names use unicode chars | ||
| _flags |= Constants.FLG_EFS; | ||
| const _localHeader = { | ||
| extraLen: 0 | ||
| }; | ||
| // casting | ||
| const uint32 = (val) => Math.max(0, val) >>> 0; | ||
| const uint16 = (val) => Math.max(0, val) & 0xffff; | ||
| const uint8 = (val) => Math.max(0, val) & 0xff; | ||
| _time = Utils.fromDate2DOS(new Date()); | ||
| return { | ||
| get made() { | ||
| return _verMade; | ||
| }, | ||
| set made(val) { | ||
| _verMade = val; | ||
| }, | ||
| get version() { | ||
| return _version; | ||
| }, | ||
| set version(val) { | ||
| _version = val; | ||
| }, | ||
| get flags() { | ||
| return _flags; | ||
| }, | ||
| set flags(val) { | ||
| _flags = val; | ||
| }, | ||
| get flags_efs() { | ||
| return (_flags & Constants.FLG_EFS) > 0; | ||
| }, | ||
| set flags_efs(val) { | ||
| if (val) { | ||
| _flags |= Constants.FLG_EFS; | ||
| } else { | ||
| _flags &= ~Constants.FLG_EFS; | ||
| } | ||
| }, | ||
| get flags_desc() { | ||
| return (_flags & Constants.FLG_DESC) > 0; | ||
| }, | ||
| set flags_desc(val) { | ||
| if (val) { | ||
| _flags |= Constants.FLG_DESC; | ||
| } else { | ||
| _flags &= ~Constants.FLG_DESC; | ||
| } | ||
| }, | ||
| get method() { | ||
| return _method; | ||
| }, | ||
| set method(val) { | ||
| switch (val) { | ||
| case Constants.STORED: | ||
| this.version = 10; | ||
| case Constants.DEFLATED: | ||
| default: | ||
| this.version = 20; | ||
| } | ||
| _method = val; | ||
| }, | ||
| get time() { | ||
| return Utils.fromDOS2Date(this.timeval); | ||
| }, | ||
| set time(val) { | ||
| val = new Date(val); | ||
| this.timeval = Utils.fromDate2DOS(val); | ||
| }, | ||
| get timeval() { | ||
| return _time; | ||
| }, | ||
| set timeval(val) { | ||
| _time = uint32(val); | ||
| }, | ||
| get timeHighByte() { | ||
| return uint8(_time >>> 8); | ||
| }, | ||
| get crc() { | ||
| return _crc; | ||
| }, | ||
| set crc(val) { | ||
| _crc = uint32(val); | ||
| }, | ||
| get compressedSize() { | ||
| return _compressedSize; | ||
| }, | ||
| set compressedSize(val) { | ||
| _compressedSize = uint32(val); | ||
| }, | ||
| get size() { | ||
| return _size; | ||
| }, | ||
| set size(val) { | ||
| _size = uint32(val); | ||
| }, | ||
| get fileNameLength() { | ||
| return _fnameLen; | ||
| }, | ||
| set fileNameLength(val) { | ||
| _fnameLen = val; | ||
| }, | ||
| get extraLength() { | ||
| return _extraLen; | ||
| }, | ||
| set extraLength(val) { | ||
| _extraLen = val; | ||
| }, | ||
| get extraLocalLength() { | ||
| return _localHeader.extraLen; | ||
| }, | ||
| set extraLocalLength(val) { | ||
| _localHeader.extraLen = val; | ||
| }, | ||
| get commentLength() { | ||
| return _comLen; | ||
| }, | ||
| set commentLength(val) { | ||
| _comLen = val; | ||
| }, | ||
| get diskNumStart() { | ||
| return _diskStart; | ||
| }, | ||
| set diskNumStart(val) { | ||
| _diskStart = uint32(val); | ||
| }, | ||
| get inAttr() { | ||
| return _inattr; | ||
| }, | ||
| set inAttr(val) { | ||
| _inattr = uint32(val); | ||
| }, | ||
| get attr() { | ||
| return _attr; | ||
| }, | ||
| set attr(val) { | ||
| _attr = uint32(val); | ||
| }, | ||
| // get Unix file permissions | ||
| get fileAttr() { | ||
| return (_attr || 0) >> 16 & 0xfff; | ||
| }, | ||
| get offset() { | ||
| return _offset; | ||
| }, | ||
| set offset(val) { | ||
| _offset = uint32(val); | ||
| }, | ||
| get encrypted() { | ||
| return (_flags & Constants.FLG_ENC) === Constants.FLG_ENC; | ||
| }, | ||
| get centralHeaderSize() { | ||
| return Constants.CENHDR + _fnameLen + _extraLen + _comLen; | ||
| }, | ||
| get realDataOffset() { | ||
| return _offset + Constants.LOCHDR + _localHeader.fnameLen + _localHeader.extraLen; | ||
| }, | ||
| get localHeader() { | ||
| return _localHeader; | ||
| }, | ||
| loadLocalHeaderFromBinary: function (/*Buffer*/ input) { | ||
| var data = input.slice(_offset, _offset + Constants.LOCHDR); | ||
| // 30 bytes and should start with "PK\003\004" | ||
| if (data.readUInt32LE(0) !== Constants.LOCSIG) { | ||
| throw Utils.Errors.INVALID_LOC(); | ||
| } | ||
| // version needed to extract | ||
| _localHeader.version = data.readUInt16LE(Constants.LOCVER); | ||
| // general purpose bit flag | ||
| _localHeader.flags = data.readUInt16LE(Constants.LOCFLG); | ||
| // desc flag | ||
| _localHeader.flags_desc = (_localHeader.flags & Constants.FLG_DESC) > 0; | ||
| // compression method | ||
| _localHeader.method = data.readUInt16LE(Constants.LOCHOW); | ||
| // modification time (2 bytes time, 2 bytes date) | ||
| _localHeader.time = data.readUInt32LE(Constants.LOCTIM); | ||
| // uncompressed file crc-32 valu | ||
| _localHeader.crc = data.readUInt32LE(Constants.LOCCRC); | ||
| // compressed size | ||
| _localHeader.compressedSize = data.readUInt32LE(Constants.LOCSIZ); | ||
| // uncompressed size | ||
| _localHeader.size = data.readUInt32LE(Constants.LOCLEN); | ||
| // filename length | ||
| _localHeader.fnameLen = data.readUInt16LE(Constants.LOCNAM); | ||
| // extra field length | ||
| _localHeader.extraLen = data.readUInt16LE(Constants.LOCEXT); | ||
| // read extra data | ||
| const extraStart = _offset + Constants.LOCHDR + _localHeader.fnameLen; | ||
| const extraEnd = extraStart + _localHeader.extraLen; | ||
| return input.slice(extraStart, extraEnd); | ||
| }, | ||
| loadFromBinary: function (/*Buffer*/ data) { | ||
| // data should be 46 bytes and start with "PK 01 02" | ||
| if (data.length !== Constants.CENHDR || data.readUInt32LE(0) !== Constants.CENSIG) { | ||
| throw Utils.Errors.INVALID_CEN(); | ||
| } | ||
| // version made by | ||
| _verMade = data.readUInt16LE(Constants.CENVEM); | ||
| // version needed to extract | ||
| _version = data.readUInt16LE(Constants.CENVER); | ||
| // encrypt, decrypt flags | ||
| _flags = data.readUInt16LE(Constants.CENFLG); | ||
| // compression method | ||
| _method = data.readUInt16LE(Constants.CENHOW); | ||
| // modification time (2 bytes time, 2 bytes date) | ||
| _time = data.readUInt32LE(Constants.CENTIM); | ||
| // uncompressed file crc-32 value | ||
| _crc = data.readUInt32LE(Constants.CENCRC); | ||
| // compressed size | ||
| _compressedSize = data.readUInt32LE(Constants.CENSIZ); | ||
| // uncompressed size | ||
| _size = data.readUInt32LE(Constants.CENLEN); | ||
| // filename length | ||
| _fnameLen = data.readUInt16LE(Constants.CENNAM); | ||
| // extra field length | ||
| _extraLen = data.readUInt16LE(Constants.CENEXT); | ||
| // file comment length | ||
| _comLen = data.readUInt16LE(Constants.CENCOM); | ||
| // volume number start | ||
| _diskStart = data.readUInt16LE(Constants.CENDSK); | ||
| // internal file attributes | ||
| _inattr = data.readUInt16LE(Constants.CENATT); | ||
| // external file attributes | ||
| _attr = data.readUInt32LE(Constants.CENATX); | ||
| // LOC header offset | ||
| _offset = data.readUInt32LE(Constants.CENOFF); | ||
| }, | ||
| localHeaderToBinary: function () { | ||
| // LOC header size (30 bytes) | ||
| var data = Buffer.alloc(Constants.LOCHDR); | ||
| // "PK\003\004" | ||
| data.writeUInt32LE(Constants.LOCSIG, 0); | ||
| // version needed to extract | ||
| data.writeUInt16LE(_version, Constants.LOCVER); | ||
| // general purpose bit flag | ||
| data.writeUInt16LE(_flags, Constants.LOCFLG); | ||
| // compression method | ||
| data.writeUInt16LE(_method, Constants.LOCHOW); | ||
| // modification time (2 bytes time, 2 bytes date) | ||
| data.writeUInt32LE(_time, Constants.LOCTIM); | ||
| // uncompressed file crc-32 value | ||
| data.writeUInt32LE(_crc, Constants.LOCCRC); | ||
| // compressed size | ||
| data.writeUInt32LE(_compressedSize, Constants.LOCSIZ); | ||
| // uncompressed size | ||
| data.writeUInt32LE(_size, Constants.LOCLEN); | ||
| // filename length | ||
| data.writeUInt16LE(_fnameLen, Constants.LOCNAM); | ||
| // extra field length | ||
| data.writeUInt16LE(_localHeader.extraLen, Constants.LOCEXT); | ||
| return data; | ||
| }, | ||
| centralHeaderToBinary: function () { | ||
| // CEN header size (46 bytes) | ||
| var data = Buffer.alloc(Constants.CENHDR + _fnameLen + _extraLen + _comLen); | ||
| // "PK\001\002" | ||
| data.writeUInt32LE(Constants.CENSIG, 0); | ||
| // version made by | ||
| data.writeUInt16LE(_verMade, Constants.CENVEM); | ||
| // version needed to extract | ||
| data.writeUInt16LE(_version, Constants.CENVER); | ||
| // encrypt, decrypt flags | ||
| data.writeUInt16LE(_flags, Constants.CENFLG); | ||
| // compression method | ||
| data.writeUInt16LE(_method, Constants.CENHOW); | ||
| // modification time (2 bytes time, 2 bytes date) | ||
| data.writeUInt32LE(_time, Constants.CENTIM); | ||
| // uncompressed file crc-32 value | ||
| data.writeUInt32LE(_crc, Constants.CENCRC); | ||
| // compressed size | ||
| data.writeUInt32LE(_compressedSize, Constants.CENSIZ); | ||
| // uncompressed size | ||
| data.writeUInt32LE(_size, Constants.CENLEN); | ||
| // filename length | ||
| data.writeUInt16LE(_fnameLen, Constants.CENNAM); | ||
| // extra field length | ||
| data.writeUInt16LE(_extraLen, Constants.CENEXT); | ||
| // file comment length | ||
| data.writeUInt16LE(_comLen, Constants.CENCOM); | ||
| // volume number start | ||
| data.writeUInt16LE(_diskStart, Constants.CENDSK); | ||
| // internal file attributes | ||
| data.writeUInt16LE(_inattr, Constants.CENATT); | ||
| // external file attributes | ||
| data.writeUInt32LE(_attr, Constants.CENATX); | ||
| // LOC header offset | ||
| data.writeUInt32LE(_offset, Constants.CENOFF); | ||
| return data; | ||
| }, | ||
| toJSON: function () { | ||
| const bytes = function (nr) { | ||
| return nr + " bytes"; | ||
| }; | ||
| return { | ||
| made: _verMade, | ||
| version: _version, | ||
| flags: _flags, | ||
| method: Utils.methodToString(_method), | ||
| time: this.time, | ||
| crc: "0x" + _crc.toString(16).toUpperCase(), | ||
| compressedSize: bytes(_compressedSize), | ||
| size: bytes(_size), | ||
| fileNameLength: bytes(_fnameLen), | ||
| extraLength: bytes(_extraLen), | ||
| commentLength: bytes(_comLen), | ||
| diskNumStart: _diskStart, | ||
| inAttr: _inattr, | ||
| attr: _attr, | ||
| offset: _offset, | ||
| centralHeaderSize: bytes(Constants.CENHDR + _fnameLen + _extraLen + _comLen) | ||
| }; | ||
| }, | ||
| toString: function () { | ||
| return JSON.stringify(this.toJSON(), null, "\t"); | ||
| } | ||
| }; | ||
| }; |
| exports.EntryHeader = require("./entryHeader"); | ||
| exports.MainHeader = require("./mainHeader"); |
| var Utils = require("../util"), | ||
| Constants = Utils.Constants; | ||
| /* The entries in the end of central directory */ | ||
| module.exports = function () { | ||
| var _volumeEntries = 0, | ||
| _totalEntries = 0, | ||
| _size = 0, | ||
| _offset = 0, | ||
| _commentLength = 0; | ||
| return { | ||
| get diskEntries() { | ||
| return _volumeEntries; | ||
| }, | ||
| set diskEntries(/*Number*/ val) { | ||
| _volumeEntries = _totalEntries = val; | ||
| }, | ||
| get totalEntries() { | ||
| return _totalEntries; | ||
| }, | ||
| set totalEntries(/*Number*/ val) { | ||
| _totalEntries = _volumeEntries = val; | ||
| }, | ||
| get size() { | ||
| return _size; | ||
| }, | ||
| set size(/*Number*/ val) { | ||
| _size = val; | ||
| }, | ||
| get offset() { | ||
| return _offset; | ||
| }, | ||
| set offset(/*Number*/ val) { | ||
| _offset = val; | ||
| }, | ||
| get commentLength() { | ||
| return _commentLength; | ||
| }, | ||
| set commentLength(/*Number*/ val) { | ||
| _commentLength = val; | ||
| }, | ||
| get mainHeaderSize() { | ||
| return Constants.ENDHDR + _commentLength; | ||
| }, | ||
| loadFromBinary: function (/*Buffer*/ data) { | ||
| // data should be 22 bytes and start with "PK 05 06" | ||
| // or be 56+ bytes and start with "PK 06 06" for Zip64 | ||
| if ( | ||
| (data.length !== Constants.ENDHDR || data.readUInt32LE(0) !== Constants.ENDSIG) && | ||
| (data.length < Constants.ZIP64HDR || data.readUInt32LE(0) !== Constants.ZIP64SIG) | ||
| ) { | ||
| throw Utils.Errors.INVALID_END(); | ||
| } | ||
| if (data.readUInt32LE(0) === Constants.ENDSIG) { | ||
| // number of entries on this volume | ||
| _volumeEntries = data.readUInt16LE(Constants.ENDSUB); | ||
| // total number of entries | ||
| _totalEntries = data.readUInt16LE(Constants.ENDTOT); | ||
| // central directory size in bytes | ||
| _size = data.readUInt32LE(Constants.ENDSIZ); | ||
| // offset of first CEN header | ||
| _offset = data.readUInt32LE(Constants.ENDOFF); | ||
| // zip file comment length | ||
| _commentLength = data.readUInt16LE(Constants.ENDCOM); | ||
| } else { | ||
| // number of entries on this volume | ||
| _volumeEntries = Utils.readBigUInt64LE(data, Constants.ZIP64SUB); | ||
| // total number of entries | ||
| _totalEntries = Utils.readBigUInt64LE(data, Constants.ZIP64TOT); | ||
| // central directory size in bytes | ||
| _size = Utils.readBigUInt64LE(data, Constants.ZIP64SIZE); | ||
| // offset of first CEN header | ||
| _offset = Utils.readBigUInt64LE(data, Constants.ZIP64OFF); | ||
| _commentLength = 0; | ||
| } | ||
| }, | ||
| toBinary: function () { | ||
| var b = Buffer.alloc(Constants.ENDHDR + _commentLength); | ||
| // "PK 05 06" signature | ||
| b.writeUInt32LE(Constants.ENDSIG, 0); | ||
| b.writeUInt32LE(0, 4); | ||
| // number of entries on this volume | ||
| b.writeUInt16LE(_volumeEntries, Constants.ENDSUB); | ||
| // total number of entries | ||
| b.writeUInt16LE(_totalEntries, Constants.ENDTOT); | ||
| // central directory size in bytes | ||
| b.writeUInt32LE(_size, Constants.ENDSIZ); | ||
| // offset of first CEN header | ||
| b.writeUInt32LE(_offset, Constants.ENDOFF); | ||
| // zip file comment length | ||
| b.writeUInt16LE(_commentLength, Constants.ENDCOM); | ||
| // fill comment memory with spaces so no garbage is left there | ||
| b.fill(" ", Constants.ENDHDR); | ||
| return b; | ||
| }, | ||
| toJSON: function () { | ||
| // creates 0x0000 style output | ||
| const offset = function (nr, len) { | ||
| let offs = nr.toString(16).toUpperCase(); | ||
| while (offs.length < len) offs = "0" + offs; | ||
| return "0x" + offs; | ||
| }; | ||
| return { | ||
| diskEntries: _volumeEntries, | ||
| totalEntries: _totalEntries, | ||
| size: _size + " bytes", | ||
| offset: offset(_offset, 4), | ||
| commentLength: _commentLength | ||
| }; | ||
| }, | ||
| toString: function () { | ||
| return JSON.stringify(this.toJSON(), null, "\t"); | ||
| } | ||
| }; | ||
| }; | ||
| // Misspelled |
| MIT License | ||
| Copyright (c) 2012 Another-D-Mention Software and other contributors | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
| module.exports = function (/*Buffer*/ inbuf) { | ||
| var zlib = require("zlib"); | ||
| var opts = { chunkSize: (parseInt(inbuf.length / 1024) + 1) * 1024 }; | ||
| return { | ||
| deflate: function () { | ||
| return zlib.deflateRawSync(inbuf, opts); | ||
| }, | ||
| deflateAsync: function (/*Function*/ callback) { | ||
| var tmp = zlib.createDeflateRaw(opts), | ||
| parts = [], | ||
| total = 0; | ||
| tmp.on("data", function (data) { | ||
| parts.push(data); | ||
| total += data.length; | ||
| }); | ||
| tmp.on("end", function () { | ||
| var buf = Buffer.alloc(total), | ||
| written = 0; | ||
| buf.fill(0); | ||
| for (var i = 0; i < parts.length; i++) { | ||
| var part = parts[i]; | ||
| part.copy(buf, written); | ||
| written += part.length; | ||
| } | ||
| callback && callback(buf); | ||
| }); | ||
| tmp.end(inbuf); | ||
| } | ||
| }; | ||
| }; |
| exports.Deflater = require("./deflater"); | ||
| exports.Inflater = require("./inflater"); | ||
| exports.ZipCrypto = require("./zipcrypto"); |
| const version = +(process.versions ? process.versions.node : "").split(".")[0] || 0; | ||
| module.exports = function (/*Buffer*/ inbuf, /*number*/ expectedLength) { | ||
| var zlib = require("zlib"); | ||
| const option = version >= 15 && expectedLength > 0 ? { maxOutputLength: expectedLength } : {}; | ||
| return { | ||
| inflate: function () { | ||
| return zlib.inflateRawSync(inbuf, option); | ||
| }, | ||
| inflateAsync: function (/*Function*/ callback) { | ||
| var tmp = zlib.createInflateRaw(option), | ||
| parts = [], | ||
| total = 0; | ||
| tmp.on("data", function (data) { | ||
| parts.push(data); | ||
| total += data.length; | ||
| }); | ||
| tmp.on("end", function () { | ||
| var buf = Buffer.alloc(total), | ||
| written = 0; | ||
| buf.fill(0); | ||
| for (var i = 0; i < parts.length; i++) { | ||
| var part = parts[i]; | ||
| part.copy(buf, written); | ||
| written += part.length; | ||
| } | ||
| callback && callback(buf); | ||
| }); | ||
| tmp.end(inbuf); | ||
| } | ||
| }; | ||
| }; |
| "use strict"; | ||
| // node crypt, we use it for generate salt | ||
| // eslint-disable-next-line node/no-unsupported-features/node-builtins | ||
| const { randomFillSync } = require("crypto"); | ||
| const Errors = require("../util/errors"); | ||
| // generate CRC32 lookup table | ||
| const crctable = new Uint32Array(256).map((t, crc) => { | ||
| for (let j = 0; j < 8; j++) { | ||
| if (0 !== (crc & 1)) { | ||
| crc = (crc >>> 1) ^ 0xedb88320; | ||
| } else { | ||
| crc >>>= 1; | ||
| } | ||
| } | ||
| return crc >>> 0; | ||
| }); | ||
| // C-style uInt32 Multiply (discards higher bits, when JS multiply discards lower bits) | ||
| const uMul = (a, b) => Math.imul(a, b) >>> 0; | ||
| // crc32 byte single update (actually same function is part of utils.crc32 function :) ) | ||
| const crc32update = (pCrc32, bval) => { | ||
| return crctable[(pCrc32 ^ bval) & 0xff] ^ (pCrc32 >>> 8); | ||
| }; | ||
| // function for generating salt for encrytion header | ||
| const genSalt = () => { | ||
| if ("function" === typeof randomFillSync) { | ||
| return randomFillSync(Buffer.alloc(12)); | ||
| } else { | ||
| // fallback if function is not defined | ||
| return genSalt.node(); | ||
| } | ||
| }; | ||
| // salt generation with node random function (mainly as fallback) | ||
| genSalt.node = () => { | ||
| const salt = Buffer.alloc(12); | ||
| const len = salt.length; | ||
| for (let i = 0; i < len; i++) salt[i] = (Math.random() * 256) & 0xff; | ||
| return salt; | ||
| }; | ||
| // general config | ||
| const config = { | ||
| genSalt | ||
| }; | ||
| // Class Initkeys handles same basic ops with keys | ||
| function Initkeys(pw) { | ||
| const pass = Buffer.isBuffer(pw) ? pw : Buffer.from(pw); | ||
| this.keys = new Uint32Array([0x12345678, 0x23456789, 0x34567890]); | ||
| for (let i = 0; i < pass.length; i++) { | ||
| this.updateKeys(pass[i]); | ||
| } | ||
| } | ||
| Initkeys.prototype.updateKeys = function (byteValue) { | ||
| const keys = this.keys; | ||
| keys[0] = crc32update(keys[0], byteValue); | ||
| keys[1] += keys[0] & 0xff; | ||
| keys[1] = uMul(keys[1], 134775813) + 1; | ||
| keys[2] = crc32update(keys[2], keys[1] >>> 24); | ||
| return byteValue; | ||
| }; | ||
| Initkeys.prototype.next = function () { | ||
| const k = (this.keys[2] | 2) >>> 0; // key | ||
| return (uMul(k, k ^ 1) >> 8) & 0xff; // decode | ||
| }; | ||
| function make_decrypter(/*Buffer*/ pwd) { | ||
| // 1. Stage initialize key | ||
| const keys = new Initkeys(pwd); | ||
| // return decrypter function | ||
| return function (/*Buffer*/ data) { | ||
| // result - we create new Buffer for results | ||
| const result = Buffer.alloc(data.length); | ||
| let pos = 0; | ||
| // process input data | ||
| for (let c of data) { | ||
| //c ^= keys.next(); | ||
| //result[pos++] = c; // decode & Save Value | ||
| result[pos++] = keys.updateKeys(c ^ keys.next()); // update keys with decoded byte | ||
| } | ||
| return result; | ||
| }; | ||
| } | ||
| function make_encrypter(/*Buffer*/ pwd) { | ||
| // 1. Stage initialize key | ||
| const keys = new Initkeys(pwd); | ||
| // return encrypting function, result and pos is here so we dont have to merge buffers later | ||
| return function (/*Buffer*/ data, /*Buffer*/ result, /* Number */ pos = 0) { | ||
| // result - we create new Buffer for results | ||
| if (!result) result = Buffer.alloc(data.length); | ||
| // process input data | ||
| for (let c of data) { | ||
| const k = keys.next(); // save key byte | ||
| result[pos++] = c ^ k; // save val | ||
| keys.updateKeys(c); // update keys with decoded byte | ||
| } | ||
| return result; | ||
| }; | ||
| } | ||
| function decrypt(/*Buffer*/ data, /*Object*/ header, /*String, Buffer*/ pwd) { | ||
| if (!data || !Buffer.isBuffer(data) || data.length < 12) { | ||
| return Buffer.alloc(0); | ||
| } | ||
| // 1. We Initialize and generate decrypting function | ||
| const decrypter = make_decrypter(pwd); | ||
| // 2. decrypt salt what is always 12 bytes and is a part of file content | ||
| const salt = decrypter(data.slice(0, 12)); | ||
| // if bit 3 (0x08) of the general-purpose flags field is set, check salt[11] with the high byte of the header time | ||
| // 2 byte data block (as per Info-Zip spec), otherwise check with the high byte of the header entry | ||
| const verifyByte = (header.flags & 0x8) === 0x8 ? header.timeHighByte : header.crc >>> 24; | ||
| //3. does password meet expectations | ||
| if (salt[11] !== verifyByte) { | ||
| throw Errors.WRONG_PASSWORD(); | ||
| } | ||
| // 4. decode content | ||
| return decrypter(data.slice(12)); | ||
| } | ||
| // lets add way to populate salt, NOT RECOMMENDED for production but maybe useful for testing general functionality | ||
| function _salter(data) { | ||
| if (Buffer.isBuffer(data) && data.length >= 12) { | ||
| // be aware - currently salting buffer data is modified | ||
| config.genSalt = function () { | ||
| return data.slice(0, 12); | ||
| }; | ||
| } else if (data === "node") { | ||
| // test salt generation with node random function | ||
| config.genSalt = genSalt.node; | ||
| } else { | ||
| // if value is not acceptable config gets reset. | ||
| config.genSalt = genSalt; | ||
| } | ||
| } | ||
| function encrypt(/*Buffer*/ data, /*Object*/ header, /*String, Buffer*/ pwd, /*Boolean*/ oldlike = false) { | ||
| // 1. test data if data is not Buffer we make buffer from it | ||
| if (data == null) data = Buffer.alloc(0); | ||
| // if data is not buffer be make buffer from it | ||
| if (!Buffer.isBuffer(data)) data = Buffer.from(data.toString()); | ||
| // 2. We Initialize and generate encrypting function | ||
| const encrypter = make_encrypter(pwd); | ||
| // 3. generate salt (12-bytes of random data) | ||
| const salt = config.genSalt(); | ||
| salt[11] = (header.crc >>> 24) & 0xff; | ||
| // old implementations (before PKZip 2.04g) used two byte check | ||
| if (oldlike) salt[10] = (header.crc >>> 16) & 0xff; | ||
| // 4. create output | ||
| const result = Buffer.alloc(data.length + 12); | ||
| encrypter(salt, result); | ||
| // finally encode content | ||
| return encrypter(data, result, 12); | ||
| } | ||
| module.exports = { decrypt, encrypt, _salter }; |
| { | ||
| "name": "adm-zip", | ||
| "version": "0.5.17", | ||
| "description": "Javascript implementation of zip for nodejs with support for electron original-fs. Allows user to create or extract zip files both in memory or to/from disk", | ||
| "scripts": { | ||
| "test": "mocha -R spec", | ||
| "test:format": "npm run format:prettier:raw -- --check", | ||
| "format": "npm run format:prettier", | ||
| "format:prettier": "npm run format:prettier:raw -- --write", | ||
| "format:prettier:raw": "prettier \"**/*.{js,yml,json}\"" | ||
| }, | ||
| "keywords": [ | ||
| "zip", | ||
| "methods", | ||
| "archive", | ||
| "unzip" | ||
| ], | ||
| "homepage": "https://github.com/cthackers/adm-zip", | ||
| "author": "Nasca Iacob <sy@another-d-mention.ro> (https://github.com/cthackers)", | ||
| "bugs": { | ||
| "email": "sy@another-d-mention.ro", | ||
| "url": "https://github.com/cthackers/adm-zip/issues" | ||
| }, | ||
| "license": "MIT", | ||
| "files": [ | ||
| "adm-zip.js", | ||
| "headers", | ||
| "methods", | ||
| "util", | ||
| "zipEntry.js", | ||
| "zipFile.js", | ||
| "LICENSE" | ||
| ], | ||
| "main": "adm-zip.js", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/cthackers/adm-zip.git" | ||
| }, | ||
| "engines": { | ||
| "node": ">=12.0" | ||
| }, | ||
| "overrides": { | ||
| "mocha": { | ||
| "chokidar": "^4.0.3" | ||
| } | ||
| }, | ||
| "devDependencies": { | ||
| "chai": "^6.2.2", | ||
| "iconv-lite": "^0.7.2", | ||
| "mocha": "12.0.0-beta-10", | ||
| "prettier": "^3.8.1", | ||
| "rimraf": "^3.0.2" | ||
| } | ||
| } |
| # ADM-ZIP for NodeJS | ||
| ADM-ZIP is a pure JavaScript implementation for zip data compression for [NodeJS](https://nodejs.org/). | ||
| <a href="https://github.com/cthackers/adm-zip/actions/workflows/ci.yml"> | ||
| <img src="https://github.com/cthackers/adm-zip/actions/workflows/ci.yml/badge.svg" alt="Build Status"> | ||
| </a> | ||
| # Installation | ||
| With [npm](https://www.npmjs.com/) do: | ||
| $ npm install adm-zip | ||
| **Electron** file system support described below. | ||
| ## What is it good for? | ||
| The library allows you to: | ||
| - decompress zip files directly to disk or in memory buffers | ||
| - compress files and store them to disk in .zip format or in compressed buffers | ||
| - update content of/add new/delete files from an existing .zip | ||
| # Dependencies | ||
| There are no other nodeJS libraries that ADM-ZIP is dependent of | ||
| # Examples | ||
| ## Basic usage | ||
| ```javascript | ||
| var AdmZip = require("adm-zip"); | ||
| // reading archives | ||
| var zip = new AdmZip("./my_file.zip"); | ||
| var password = "1234567890"; | ||
| var zipEntries = zip.getEntries(); // an array of ZipEntry records - add password parameter if entries are password protected | ||
| zipEntries.forEach(function (zipEntry) { | ||
| console.log(zipEntry.toString()); // outputs zip entries information | ||
| if (zipEntry.entryName == "my_file.txt") { | ||
| console.log(zipEntry.getData().toString("utf8")); | ||
| } | ||
| }); | ||
| // outputs the content of some_folder/my_file.txt | ||
| console.log(zip.readAsText("some_folder/my_file.txt")); | ||
| // extracts the specified file to the specified location | ||
| zip.extractEntryTo(/*entry name*/ "some_folder/my_file.txt", /*target path*/ "/home/me/tempfolder", /*maintainEntryPath*/ false, /*overwrite*/ true); | ||
| // extracts everything | ||
| zip.extractAllTo(/*target path*/ "/home/me/zipcontent/", /*overwrite*/ true); | ||
| // creating archives | ||
| var zip = new AdmZip(); | ||
| // add file directly | ||
| var content = "inner content of the file"; | ||
| zip.addFile("test.txt", Buffer.from(content, "utf8"), "entry comment goes here"); | ||
| // add local file | ||
| zip.addLocalFile("/home/me/some_picture.png"); | ||
| // get everything as a buffer | ||
| var willSendthis = zip.toBuffer(); | ||
| // or write everything to disk | ||
| zip.writeZip(/*target file name*/ "/home/me/files.zip"); | ||
| // ... more examples in the wiki | ||
| ``` | ||
| For more detailed information please check out the [wiki](https://github.com/cthackers/adm-zip/wiki). | ||
| ## Electron original-fs | ||
| ADM-ZIP has supported electron **original-fs** for years without any user interractions but it causes problem with bundlers like rollup etc. For continuing support **original-fs** or any other custom file system module. There is possible specify your module by **fs** option in ADM-ZIP constructor. | ||
| Example: | ||
| ```javascript | ||
| const AdmZip = require("adm-zip"); | ||
| const OriginalFs = require("original-fs"); | ||
| // reading archives | ||
| const zip = new AdmZip("./my_file.zip", { fs: OriginalFs }); | ||
| . | ||
| . | ||
| . | ||
| ``` |
| module.exports = { | ||
| /* The local file header */ | ||
| LOCHDR : 30, // LOC header size | ||
| LOCSIG : 0x04034b50, // "PK\003\004" | ||
| LOCVER : 4, // version needed to extract | ||
| LOCFLG : 6, // general purpose bit flag | ||
| LOCHOW : 8, // compression method | ||
| LOCTIM : 10, // modification time (2 bytes time, 2 bytes date) | ||
| LOCCRC : 14, // uncompressed file crc-32 value | ||
| LOCSIZ : 18, // compressed size | ||
| LOCLEN : 22, // uncompressed size | ||
| LOCNAM : 26, // filename length | ||
| LOCEXT : 28, // extra field length | ||
| /* The Data descriptor */ | ||
| EXTSIG : 0x08074b50, // "PK\007\008" | ||
| EXTHDR : 16, // EXT header size | ||
| EXTCRC : 4, // uncompressed file crc-32 value | ||
| EXTSIZ : 8, // compressed size | ||
| EXTLEN : 12, // uncompressed size | ||
| /* The central directory file header */ | ||
| CENHDR : 46, // CEN header size | ||
| CENSIG : 0x02014b50, // "PK\001\002" | ||
| CENVEM : 4, // version made by | ||
| CENVER : 6, // version needed to extract | ||
| CENFLG : 8, // encrypt, decrypt flags | ||
| CENHOW : 10, // compression method | ||
| CENTIM : 12, // modification time (2 bytes time, 2 bytes date) | ||
| CENCRC : 16, // uncompressed file crc-32 value | ||
| CENSIZ : 20, // compressed size | ||
| CENLEN : 24, // uncompressed size | ||
| CENNAM : 28, // filename length | ||
| CENEXT : 30, // extra field length | ||
| CENCOM : 32, // file comment length | ||
| CENDSK : 34, // volume number start | ||
| CENATT : 36, // internal file attributes | ||
| CENATX : 38, // external file attributes (host system dependent) | ||
| CENOFF : 42, // LOC header offset | ||
| /* The entries in the end of central directory */ | ||
| ENDHDR : 22, // END header size | ||
| ENDSIG : 0x06054b50, // "PK\005\006" | ||
| ENDSUB : 8, // number of entries on this disk | ||
| ENDTOT : 10, // total number of entries | ||
| ENDSIZ : 12, // central directory size in bytes | ||
| ENDOFF : 16, // offset of first CEN header | ||
| ENDCOM : 20, // zip file comment length | ||
| END64HDR : 20, // zip64 END header size | ||
| END64SIG : 0x07064b50, // zip64 Locator signature, "PK\006\007" | ||
| END64START : 4, // number of the disk with the start of the zip64 | ||
| END64OFF : 8, // relative offset of the zip64 end of central directory | ||
| END64NUMDISKS : 16, // total number of disks | ||
| ZIP64SIG : 0x06064b50, // zip64 signature, "PK\006\006" | ||
| ZIP64HDR : 56, // zip64 record minimum size | ||
| ZIP64LEAD : 12, // leading bytes at the start of the record, not counted by the value stored in ZIP64SIZE | ||
| ZIP64SIZE : 4, // zip64 size of the central directory record | ||
| ZIP64VEM : 12, // zip64 version made by | ||
| ZIP64VER : 14, // zip64 version needed to extract | ||
| ZIP64DSK : 16, // zip64 number of this disk | ||
| ZIP64DSKDIR : 20, // number of the disk with the start of the record directory | ||
| ZIP64SUB : 24, // number of entries on this disk | ||
| ZIP64TOT : 32, // total number of entries | ||
| ZIP64SIZB : 40, // zip64 central directory size in bytes | ||
| ZIP64OFF : 48, // offset of start of central directory with respect to the starting disk number | ||
| ZIP64EXTRA : 56, // extensible data sector | ||
| /* Compression methods */ | ||
| STORED : 0, // no compression | ||
| SHRUNK : 1, // shrunk | ||
| REDUCED1 : 2, // reduced with compression factor 1 | ||
| REDUCED2 : 3, // reduced with compression factor 2 | ||
| REDUCED3 : 4, // reduced with compression factor 3 | ||
| REDUCED4 : 5, // reduced with compression factor 4 | ||
| IMPLODED : 6, // imploded | ||
| // 7 reserved for Tokenizing compression algorithm | ||
| DEFLATED : 8, // deflated | ||
| ENHANCED_DEFLATED: 9, // enhanced deflated | ||
| PKWARE : 10,// PKWare DCL imploded | ||
| // 11 reserved by PKWARE | ||
| BZIP2 : 12, // compressed using BZIP2 | ||
| // 13 reserved by PKWARE | ||
| LZMA : 14, // LZMA | ||
| // 15-17 reserved by PKWARE | ||
| IBM_TERSE : 18, // compressed using IBM TERSE | ||
| IBM_LZ77 : 19, // IBM LZ77 z | ||
| AES_ENCRYPT : 99, // WinZIP AES encryption method | ||
| /* General purpose bit flag */ | ||
| // values can obtained with expression 2**bitnr | ||
| FLG_ENC : 1, // Bit 0: encrypted file | ||
| FLG_COMP1 : 2, // Bit 1, compression option | ||
| FLG_COMP2 : 4, // Bit 2, compression option | ||
| FLG_DESC : 8, // Bit 3, data descriptor | ||
| FLG_ENH : 16, // Bit 4, enhanced deflating | ||
| FLG_PATCH : 32, // Bit 5, indicates that the file is compressed patched data. | ||
| FLG_STR : 64, // Bit 6, strong encryption (patented) | ||
| // Bits 7-10: Currently unused. | ||
| FLG_EFS : 2048, // Bit 11: Language encoding flag (EFS) | ||
| // Bit 12: Reserved by PKWARE for enhanced compression. | ||
| // Bit 13: encrypted the Central Directory (patented). | ||
| // Bits 14-15: Reserved by PKWARE. | ||
| FLG_MSK : 4096, // mask header values | ||
| /* Load type */ | ||
| FILE : 2, | ||
| BUFFER : 1, | ||
| NONE : 0, | ||
| /* 4.5 Extensible data fields */ | ||
| EF_ID : 0, | ||
| EF_SIZE : 2, | ||
| /* Header IDs */ | ||
| ID_ZIP64 : 0x0001, | ||
| ID_AVINFO : 0x0007, | ||
| ID_PFS : 0x0008, | ||
| ID_OS2 : 0x0009, | ||
| ID_NTFS : 0x000a, | ||
| ID_OPENVMS : 0x000c, | ||
| ID_UNIX : 0x000d, | ||
| ID_FORK : 0x000e, | ||
| ID_PATCH : 0x000f, | ||
| ID_X509_PKCS7 : 0x0014, | ||
| ID_X509_CERTID_F : 0x0015, | ||
| ID_X509_CERTID_C : 0x0016, | ||
| ID_STRONGENC : 0x0017, | ||
| ID_RECORD_MGT : 0x0018, | ||
| ID_X509_PKCS7_RL : 0x0019, | ||
| ID_IBM1 : 0x0065, | ||
| ID_IBM2 : 0x0066, | ||
| ID_POSZIP : 0x4690, | ||
| EF_ZIP64_OR_32 : 0xffffffff, | ||
| EF_ZIP64_OR_16 : 0xffff, | ||
| EF_ZIP64_SUNCOMP : 0, | ||
| EF_ZIP64_SCOMP : 8, | ||
| EF_ZIP64_RHO : 16, | ||
| EF_ZIP64_DSN : 24 | ||
| }; |
| module.exports = { | ||
| efs: true, | ||
| encode: (data) => Buffer.from(data, "utf8"), | ||
| decode: (data) => data.toString("utf8") | ||
| }; |
| const errors = { | ||
| /* Header error messages */ | ||
| INVALID_LOC: "Invalid LOC header (bad signature)", | ||
| INVALID_CEN: "Invalid CEN header (bad signature)", | ||
| INVALID_END: "Invalid END header (bad signature)", | ||
| /* Descriptor */ | ||
| DESCRIPTOR_NOT_EXIST: "No descriptor present", | ||
| DESCRIPTOR_UNKNOWN: "Unknown descriptor format", | ||
| DESCRIPTOR_FAULTY: "Descriptor data is malformed", | ||
| /* ZipEntry error messages*/ | ||
| NO_DATA: "Nothing to decompress", | ||
| BAD_CRC: "CRC32 checksum failed {0}", | ||
| FILE_IN_THE_WAY: "There is a file in the way: {0}", | ||
| UNKNOWN_METHOD: "Invalid/unsupported compression method", | ||
| /* Inflater error messages */ | ||
| AVAIL_DATA: "inflate::Available inflate data did not terminate", | ||
| INVALID_DISTANCE: "inflate::Invalid literal/length or distance code in fixed or dynamic block", | ||
| TO_MANY_CODES: "inflate::Dynamic block code description: too many length or distance codes", | ||
| INVALID_REPEAT_LEN: "inflate::Dynamic block code description: repeat more than specified lengths", | ||
| INVALID_REPEAT_FIRST: "inflate::Dynamic block code description: repeat lengths with no first length", | ||
| INCOMPLETE_CODES: "inflate::Dynamic block code description: code lengths codes incomplete", | ||
| INVALID_DYN_DISTANCE: "inflate::Dynamic block code description: invalid distance code lengths", | ||
| INVALID_CODES_LEN: "inflate::Dynamic block code description: invalid literal/length code lengths", | ||
| INVALID_STORE_BLOCK: "inflate::Stored block length did not match one's complement", | ||
| INVALID_BLOCK_TYPE: "inflate::Invalid block type (type == 3)", | ||
| /* ADM-ZIP error messages */ | ||
| CANT_EXTRACT_FILE: "Could not extract the file", | ||
| CANT_OVERRIDE: "Target file already exists", | ||
| DISK_ENTRY_TOO_LARGE: "Number of disk entries is too large", | ||
| NO_ZIP: "No zip file was loaded", | ||
| NO_ENTRY: "Entry doesn't exist", | ||
| DIRECTORY_CONTENT_ERROR: "A directory cannot have content", | ||
| FILE_NOT_FOUND: 'File not found: "{0}"', | ||
| NOT_IMPLEMENTED: "Not implemented", | ||
| INVALID_FILENAME: "Invalid filename", | ||
| INVALID_FORMAT: "Invalid or unsupported zip format. No END header found", | ||
| INVALID_PASS_PARAM: "Incompatible password parameter", | ||
| WRONG_PASSWORD: "Wrong Password", | ||
| /* ADM-ZIP */ | ||
| COMMENT_TOO_LONG: "Comment is too long", // Comment can be max 65535 bytes long (NOTE: some non-US characters may take more space) | ||
| EXTRA_FIELD_PARSE_ERROR: "Extra field parsing error" | ||
| }; | ||
| // template | ||
| function E(message) { | ||
| return function (...args) { | ||
| if (args.length) { // Allow {0} .. {9} arguments in error message, based on argument number | ||
| message = message.replace(/\{(\d)\}/g, (_, n) => args[n] || ''); | ||
| } | ||
| return new Error('ADM-ZIP: ' + message); | ||
| }; | ||
| } | ||
| // Init errors with template | ||
| for (const msg of Object.keys(errors)) { | ||
| exports[msg] = E(errors[msg]); | ||
| } |
| const pth = require("path"); | ||
| module.exports = function (/*String*/ path, /*Utils object*/ { fs }) { | ||
| var _path = path || "", | ||
| _obj = newAttr(), | ||
| _stat = null; | ||
| function newAttr() { | ||
| return { | ||
| directory: false, | ||
| readonly: false, | ||
| hidden: false, | ||
| executable: false, | ||
| mtime: 0, | ||
| atime: 0 | ||
| }; | ||
| } | ||
| if (_path && fs.existsSync(_path)) { | ||
| _stat = fs.statSync(_path); | ||
| _obj.directory = _stat.isDirectory(); | ||
| _obj.mtime = _stat.mtime; | ||
| _obj.atime = _stat.atime; | ||
| _obj.executable = (0o111 & _stat.mode) !== 0; // file is executable who ever har right not just owner | ||
| _obj.readonly = (0o200 & _stat.mode) === 0; // readonly if owner has no write right | ||
| _obj.hidden = pth.basename(_path)[0] === "."; | ||
| } else { | ||
| console.warn("Invalid path: " + _path); | ||
| } | ||
| return { | ||
| get directory() { | ||
| return _obj.directory; | ||
| }, | ||
| get readOnly() { | ||
| return _obj.readonly; | ||
| }, | ||
| get hidden() { | ||
| return _obj.hidden; | ||
| }, | ||
| get mtime() { | ||
| return _obj.mtime; | ||
| }, | ||
| get atime() { | ||
| return _obj.atime; | ||
| }, | ||
| get executable() { | ||
| return _obj.executable; | ||
| }, | ||
| decodeAttributes: function () {}, | ||
| encodeAttributes: function () {}, | ||
| toJSON: function () { | ||
| return { | ||
| path: _path, | ||
| isDirectory: _obj.directory, | ||
| isReadOnly: _obj.readonly, | ||
| isHidden: _obj.hidden, | ||
| isExecutable: _obj.executable, | ||
| mTime: _obj.mtime, | ||
| aTime: _obj.atime | ||
| }; | ||
| }, | ||
| toString: function () { | ||
| return JSON.stringify(this.toJSON(), null, "\t"); | ||
| } | ||
| }; | ||
| }; |
| module.exports = require("./utils"); | ||
| module.exports.Constants = require("./constants"); | ||
| module.exports.Errors = require("./errors"); | ||
| module.exports.FileAttr = require("./fattr"); | ||
| module.exports.decoder = require("./decoder"); |
| const fsystem = require("fs"); | ||
| const pth = require("path"); | ||
| const Constants = require("./constants"); | ||
| const Errors = require("./errors"); | ||
| const isWin = typeof process === "object" && "win32" === process.platform; | ||
| const is_Obj = (obj) => typeof obj === "object" && obj !== null; | ||
| // generate CRC32 lookup table | ||
| const crcTable = new Uint32Array(256).map((t, c) => { | ||
| for (let k = 0; k < 8; k++) { | ||
| if ((c & 1) !== 0) { | ||
| c = 0xedb88320 ^ (c >>> 1); | ||
| } else { | ||
| c >>>= 1; | ||
| } | ||
| } | ||
| return c >>> 0; | ||
| }); | ||
| // UTILS functions | ||
| function Utils(opts) { | ||
| this.sep = pth.sep; | ||
| this.fs = fsystem; | ||
| if (is_Obj(opts)) { | ||
| // custom filesystem | ||
| if (is_Obj(opts.fs) && typeof opts.fs.statSync === "function") { | ||
| this.fs = opts.fs; | ||
| } | ||
| } | ||
| } | ||
| module.exports = Utils; | ||
| // INSTANTIABLE functions | ||
| Utils.prototype.makeDir = function (/*String*/ folder) { | ||
| const self = this; | ||
| // Sync - make directories tree | ||
| function mkdirSync(/*String*/ fpath) { | ||
| let resolvedPath = fpath.split(self.sep)[0]; | ||
| fpath.split(self.sep).forEach(function (name) { | ||
| if (!name || name.substr(-1, 1) === ":") return; | ||
| resolvedPath += self.sep + name; | ||
| var stat; | ||
| try { | ||
| stat = self.fs.statSync(resolvedPath); | ||
| } catch (e) { | ||
| if (e.message && e.message.startsWith('ENOENT')) { | ||
| self.fs.mkdirSync(resolvedPath); | ||
| } else { | ||
| throw e; | ||
| } | ||
| } | ||
| if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`); | ||
| }); | ||
| } | ||
| mkdirSync(folder); | ||
| }; | ||
| Utils.prototype.writeFileTo = function (/*String*/ path, /*Buffer*/ content, /*Boolean*/ overwrite, /*Number*/ attr) { | ||
| const self = this; | ||
| if (self.fs.existsSync(path)) { | ||
| if (!overwrite) return false; // cannot overwrite | ||
| var stat = self.fs.statSync(path); | ||
| if (stat.isDirectory()) { | ||
| return false; | ||
| } | ||
| } | ||
| var folder = pth.dirname(path); | ||
| if (!self.fs.existsSync(folder)) { | ||
| self.makeDir(folder); | ||
| } | ||
| var fd; | ||
| try { | ||
| fd = self.fs.openSync(path, "w", 0o666); // 0666 | ||
| } catch (e) { | ||
| self.fs.chmodSync(path, 0o666); | ||
| fd = self.fs.openSync(path, "w", 0o666); | ||
| } | ||
| if (fd) { | ||
| try { | ||
| self.fs.writeSync(fd, content, 0, content.length, 0); | ||
| } finally { | ||
| self.fs.closeSync(fd); | ||
| } | ||
| } | ||
| self.fs.chmodSync(path, attr || 0o666); | ||
| return true; | ||
| }; | ||
| Utils.prototype.writeFileToAsync = function (/*String*/ path, /*Buffer*/ content, /*Boolean*/ overwrite, /*Number*/ attr, /*Function*/ callback) { | ||
| if (typeof attr === "function") { | ||
| callback = attr; | ||
| attr = undefined; | ||
| } | ||
| const self = this; | ||
| self.fs.exists(path, function (exist) { | ||
| if (exist && !overwrite) return callback(false); | ||
| self.fs.stat(path, function (err, stat) { | ||
| if (exist && stat.isDirectory()) { | ||
| return callback(false); | ||
| } | ||
| var folder = pth.dirname(path); | ||
| self.fs.exists(folder, function (exists) { | ||
| if (!exists) self.makeDir(folder); | ||
| self.fs.open(path, "w", 0o666, function (err, fd) { | ||
| if (err) { | ||
| self.fs.chmod(path, 0o666, function () { | ||
| self.fs.open(path, "w", 0o666, function (err, fd) { | ||
| self.fs.write(fd, content, 0, content.length, 0, function () { | ||
| self.fs.close(fd, function () { | ||
| self.fs.chmod(path, attr || 0o666, function () { | ||
| callback(true); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| } else if (fd) { | ||
| self.fs.write(fd, content, 0, content.length, 0, function () { | ||
| self.fs.close(fd, function () { | ||
| self.fs.chmod(path, attr || 0o666, function () { | ||
| callback(true); | ||
| }); | ||
| }); | ||
| }); | ||
| } else { | ||
| self.fs.chmod(path, attr || 0o666, function () { | ||
| callback(true); | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| }; | ||
| Utils.prototype.findFiles = function (/*String*/ path) { | ||
| const self = this; | ||
| function findSync(/*String*/ dir, /*RegExp*/ pattern, /*Boolean*/ recursive) { | ||
| if (typeof pattern === "boolean") { | ||
| recursive = pattern; | ||
| pattern = undefined; | ||
| } | ||
| let files = []; | ||
| self.fs.readdirSync(dir).forEach(function (file) { | ||
| const path = pth.join(dir, file); | ||
| const stat = self.fs.statSync(path); | ||
| if (!pattern || pattern.test(path)) { | ||
| files.push(pth.normalize(path) + (stat.isDirectory() ? self.sep : "")); | ||
| } | ||
| if (stat.isDirectory() && recursive) files = files.concat(findSync(path, pattern, recursive)); | ||
| }); | ||
| return files; | ||
| } | ||
| return findSync(path, undefined, true); | ||
| }; | ||
| /** | ||
| * Callback for showing if everything was done. | ||
| * | ||
| * @callback filelistCallback | ||
| * @param {Error} err - Error object | ||
| * @param {string[]} list - was request fully completed | ||
| */ | ||
| /** | ||
| * | ||
| * @param {string} dir | ||
| * @param {filelistCallback} cb | ||
| */ | ||
| Utils.prototype.findFilesAsync = function (dir, cb) { | ||
| const self = this; | ||
| let results = []; | ||
| self.fs.readdir(dir, function (err, list) { | ||
| if (err) return cb(err); | ||
| let list_length = list.length; | ||
| if (!list_length) return cb(null, results); | ||
| list.forEach(function (file) { | ||
| file = pth.join(dir, file); | ||
| self.fs.stat(file, function (err, stat) { | ||
| if (err) return cb(err); | ||
| if (stat) { | ||
| results.push(pth.normalize(file) + (stat.isDirectory() ? self.sep : "")); | ||
| if (stat.isDirectory()) { | ||
| self.findFilesAsync(file, function (err, res) { | ||
| if (err) return cb(err); | ||
| results = results.concat(res); | ||
| if (!--list_length) cb(null, results); | ||
| }); | ||
| } else { | ||
| if (!--list_length) cb(null, results); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| }; | ||
| Utils.prototype.getAttributes = function () {}; | ||
| Utils.prototype.setAttributes = function () {}; | ||
| // STATIC functions | ||
| // crc32 single update (it is part of crc32) | ||
| Utils.crc32update = function (crc, byte) { | ||
| return crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8); | ||
| }; | ||
| Utils.crc32 = function (buf) { | ||
| if (typeof buf === "string") { | ||
| buf = Buffer.from(buf, "utf8"); | ||
| } | ||
| let len = buf.length; | ||
| let crc = ~0; | ||
| for (let off = 0; off < len; ) crc = Utils.crc32update(crc, buf[off++]); | ||
| // xor and cast as uint32 number | ||
| return ~crc >>> 0; | ||
| }; | ||
| Utils.methodToString = function (/*Number*/ method) { | ||
| switch (method) { | ||
| case Constants.STORED: | ||
| return "STORED (" + method + ")"; | ||
| case Constants.DEFLATED: | ||
| return "DEFLATED (" + method + ")"; | ||
| default: | ||
| return "UNSUPPORTED (" + method + ")"; | ||
| } | ||
| }; | ||
| /** | ||
| * removes ".." style path elements | ||
| * @param {string} path - fixable path | ||
| * @returns string - fixed filepath | ||
| */ | ||
| Utils.canonical = function (/*string*/ path) { | ||
| if (!path) return ""; | ||
| // trick normalize think path is absolute | ||
| const safeSuffix = pth.posix.normalize("/" + path.split("\\").join("/")); | ||
| return pth.join(".", safeSuffix); | ||
| }; | ||
| /** | ||
| * fix file names in achive | ||
| * @param {string} path - fixable path | ||
| * @returns string - fixed filepath | ||
| */ | ||
| Utils.zipnamefix = function (path) { | ||
| if (!path) return ""; | ||
| // trick normalize think path is absolute | ||
| const safeSuffix = pth.posix.normalize("/" + path.split("\\").join("/")); | ||
| return pth.posix.join(".", safeSuffix); | ||
| }; | ||
| /** | ||
| * | ||
| * @param {Array} arr | ||
| * @param {function} callback | ||
| * @returns | ||
| */ | ||
| Utils.findLast = function (arr, callback) { | ||
| if (!Array.isArray(arr)) throw new TypeError("arr is not array"); | ||
| const len = arr.length >>> 0; | ||
| for (let i = len - 1; i >= 0; i--) { | ||
| if (callback(arr[i], i, arr)) { | ||
| return arr[i]; | ||
| } | ||
| } | ||
| return void 0; | ||
| }; | ||
| // make abolute paths taking prefix as root folder | ||
| Utils.sanitize = function (/*string*/ prefix, /*string*/ name) { | ||
| prefix = pth.resolve(pth.normalize(prefix)); | ||
| var parts = name.split("/"); | ||
| for (var i = 0, l = parts.length; i < l; i++) { | ||
| var path = pth.normalize(pth.join(prefix, parts.slice(i, l).join(pth.sep))); | ||
| if (path.indexOf(prefix) === 0) { | ||
| return path; | ||
| } | ||
| } | ||
| return pth.normalize(pth.join(prefix, pth.basename(name))); | ||
| }; | ||
| // converts buffer, Uint8Array, string types to buffer | ||
| Utils.toBuffer = function toBuffer(/*buffer, Uint8Array, string*/ input, /* function */ encoder) { | ||
| if (Buffer.isBuffer(input)) { | ||
| return input; | ||
| } else if (input instanceof Uint8Array) { | ||
| return Buffer.from(input); | ||
| } else { | ||
| // expect string all other values are invalid and return empty buffer | ||
| return typeof input === "string" ? encoder(input) : Buffer.alloc(0); | ||
| } | ||
| }; | ||
| Utils.readBigUInt64LE = function (/*Buffer*/ buffer, /*int*/ index) { | ||
| const lo = buffer.readUInt32LE(index); | ||
| const hi = buffer.readUInt32LE(index + 4); | ||
| return hi * 0x100000000 + lo; | ||
| }; | ||
| Utils.fromDOS2Date = function (val) { | ||
| return new Date(((val >> 25) & 0x7f) + 1980, Math.max(((val >> 21) & 0x0f) - 1, 0), Math.max((val >> 16) & 0x1f, 1), (val >> 11) & 0x1f, (val >> 5) & 0x3f, (val & 0x1f) << 1); | ||
| }; | ||
| Utils.fromDate2DOS = function (val) { | ||
| let date = 0; | ||
| let time = 0; | ||
| if (val.getFullYear() > 1979) { | ||
| date = (((val.getFullYear() - 1980) & 0x7f) << 9) | ((val.getMonth() + 1) << 5) | val.getDate(); | ||
| time = (val.getHours() << 11) | (val.getMinutes() << 5) | (val.getSeconds() >> 1); | ||
| } | ||
| return (date << 16) | time; | ||
| }; | ||
| Utils.isWin = isWin; // Do we have windows system | ||
| Utils.crcTable = crcTable; |
| var Utils = require("./util"), | ||
| Headers = require("./headers"), | ||
| Constants = Utils.Constants, | ||
| Methods = require("./methods"); | ||
| module.exports = function (/** object */ options, /*Buffer*/ input) { | ||
| var _centralHeader = new Headers.EntryHeader(), | ||
| _entryName = Buffer.alloc(0), | ||
| _comment = Buffer.alloc(0), | ||
| _isDirectory = false, | ||
| uncompressedData = null, | ||
| _extra = Buffer.alloc(0), | ||
| _extralocal = Buffer.alloc(0), | ||
| _efs = true; | ||
| // assign options | ||
| const opts = options; | ||
| const decoder = typeof opts.decoder === "object" ? opts.decoder : Utils.decoder; | ||
| _efs = decoder.hasOwnProperty("efs") ? decoder.efs : false; | ||
| function getCompressedDataFromZip() { | ||
| //if (!input || !Buffer.isBuffer(input)) { | ||
| if (!input || !(input instanceof Uint8Array)) { | ||
| return Buffer.alloc(0); | ||
| } | ||
| _extralocal = _centralHeader.loadLocalHeaderFromBinary(input); | ||
| return input.slice(_centralHeader.realDataOffset, _centralHeader.realDataOffset + _centralHeader.compressedSize); | ||
| } | ||
| function crc32OK(data) { | ||
| // if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the local header is written | ||
| if (!_centralHeader.flags_desc && !_centralHeader.localHeader.flags_desc) { | ||
| if (Utils.crc32(data) !== _centralHeader.localHeader.crc) { | ||
| return false; | ||
| } | ||
| } else { | ||
| const descriptor = {}; | ||
| const dataEndOffset = _centralHeader.realDataOffset + _centralHeader.compressedSize; | ||
| // no descriptor after compressed data, instead new local header | ||
| if (input.readUInt32LE(dataEndOffset) == Constants.LOCSIG || input.readUInt32LE(dataEndOffset) == Constants.CENSIG) { | ||
| throw Utils.Errors.DESCRIPTOR_NOT_EXIST(); | ||
| } | ||
| // get decriptor data | ||
| if (input.readUInt32LE(dataEndOffset) == Constants.EXTSIG) { | ||
| // descriptor with signature | ||
| descriptor.crc = input.readUInt32LE(dataEndOffset + Constants.EXTCRC); | ||
| descriptor.compressedSize = input.readUInt32LE(dataEndOffset + Constants.EXTSIZ); | ||
| descriptor.size = input.readUInt32LE(dataEndOffset + Constants.EXTLEN); | ||
| } else if (input.readUInt16LE(dataEndOffset + 12) === 0x4b50) { | ||
| // descriptor without signature (we check is new header starting where we expect) | ||
| descriptor.crc = input.readUInt32LE(dataEndOffset + Constants.EXTCRC - 4); | ||
| descriptor.compressedSize = input.readUInt32LE(dataEndOffset + Constants.EXTSIZ - 4); | ||
| descriptor.size = input.readUInt32LE(dataEndOffset + Constants.EXTLEN - 4); | ||
| } else { | ||
| throw Utils.Errors.DESCRIPTOR_UNKNOWN(); | ||
| } | ||
| // check data integrity | ||
| if (descriptor.compressedSize !== _centralHeader.compressedSize || descriptor.size !== _centralHeader.size || descriptor.crc !== _centralHeader.crc) { | ||
| throw Utils.Errors.DESCRIPTOR_FAULTY(); | ||
| } | ||
| if (Utils.crc32(data) !== descriptor.crc) { | ||
| return false; | ||
| } | ||
| // @TODO: zip64 bit descriptor fields | ||
| // if bit 3 is set and any value in local header "zip64 Extended information" extra field are set 0 (place holder) | ||
| // then 64-bit descriptor format is used instead of 32-bit | ||
| // central header - "zip64 Extended information" extra field should store real values and not place holders | ||
| } | ||
| return true; | ||
| } | ||
| function decompress(/*Boolean*/ async, /*Function*/ callback, /*String, Buffer*/ pass) { | ||
| if (typeof callback === "undefined" && typeof async === "string") { | ||
| pass = async; | ||
| async = void 0; | ||
| } | ||
| if (_isDirectory) { | ||
| if (async && callback) { | ||
| callback(Buffer.alloc(0), Utils.Errors.DIRECTORY_CONTENT_ERROR()); //si added error. | ||
| } | ||
| return Buffer.alloc(0); | ||
| } | ||
| var compressedData = getCompressedDataFromZip(); | ||
| if (compressedData.length === 0) { | ||
| // File is empty, nothing to decompress. | ||
| if (async && callback) callback(compressedData); | ||
| return compressedData; | ||
| } | ||
| if (_centralHeader.encrypted) { | ||
| if ("string" !== typeof pass && !Buffer.isBuffer(pass)) { | ||
| throw Utils.Errors.INVALID_PASS_PARAM(); | ||
| } | ||
| compressedData = Methods.ZipCrypto.decrypt(compressedData, _centralHeader, pass); | ||
| } | ||
| var data = Buffer.alloc(_centralHeader.size); | ||
| switch (_centralHeader.method) { | ||
| case Utils.Constants.STORED: | ||
| compressedData.copy(data); | ||
| if (!crc32OK(data)) { | ||
| if (async && callback) callback(data, Utils.Errors.BAD_CRC()); //si added error | ||
| throw Utils.Errors.BAD_CRC(); | ||
| } else { | ||
| //si added otherwise did not seem to return data. | ||
| if (async && callback) callback(data); | ||
| return data; | ||
| } | ||
| case Utils.Constants.DEFLATED: | ||
| var inflater = new Methods.Inflater(compressedData, _centralHeader.size); | ||
| if (!async) { | ||
| const result = inflater.inflate(data); | ||
| result.copy(data, 0); | ||
| if (!crc32OK(data)) { | ||
| throw Utils.Errors.BAD_CRC(`"${decoder.decode(_entryName)}"`); | ||
| } | ||
| return data; | ||
| } else { | ||
| inflater.inflateAsync(function (result) { | ||
| result.copy(result, 0); | ||
| if (callback) { | ||
| if (!crc32OK(result)) { | ||
| callback(result, Utils.Errors.BAD_CRC()); //si added error | ||
| } else { | ||
| callback(result); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| break; | ||
| default: | ||
| if (async && callback) callback(Buffer.alloc(0), Utils.Errors.UNKNOWN_METHOD()); | ||
| throw Utils.Errors.UNKNOWN_METHOD(); | ||
| } | ||
| } | ||
| function compress(/*Boolean*/ async, /*Function*/ callback) { | ||
| if ((!uncompressedData || !uncompressedData.length) && Buffer.isBuffer(input)) { | ||
| // no data set or the data wasn't changed to require recompression | ||
| if (async && callback) callback(getCompressedDataFromZip()); | ||
| return getCompressedDataFromZip(); | ||
| } | ||
| if (uncompressedData.length && !_isDirectory) { | ||
| var compressedData; | ||
| // Local file header | ||
| switch (_centralHeader.method) { | ||
| case Utils.Constants.STORED: | ||
| _centralHeader.compressedSize = _centralHeader.size; | ||
| compressedData = Buffer.alloc(uncompressedData.length); | ||
| uncompressedData.copy(compressedData); | ||
| if (async && callback) callback(compressedData); | ||
| return compressedData; | ||
| default: | ||
| case Utils.Constants.DEFLATED: | ||
| var deflater = new Methods.Deflater(uncompressedData); | ||
| if (!async) { | ||
| var deflated = deflater.deflate(); | ||
| _centralHeader.compressedSize = deflated.length; | ||
| return deflated; | ||
| } else { | ||
| deflater.deflateAsync(function (data) { | ||
| compressedData = Buffer.alloc(data.length); | ||
| _centralHeader.compressedSize = data.length; | ||
| data.copy(compressedData); | ||
| callback && callback(compressedData); | ||
| }); | ||
| } | ||
| deflater = null; | ||
| break; | ||
| } | ||
| } else if (async && callback) { | ||
| callback(Buffer.alloc(0)); | ||
| } else { | ||
| return Buffer.alloc(0); | ||
| } | ||
| } | ||
| function readUInt64LE(buffer, offset) { | ||
| return Utils.readBigUInt64LE(buffer, offset); | ||
| } | ||
| function parseExtra(data) { | ||
| try { | ||
| var offset = 0; | ||
| var signature, size, part; | ||
| while (offset + 4 < data.length) { | ||
| signature = data.readUInt16LE(offset); | ||
| offset += 2; | ||
| size = data.readUInt16LE(offset); | ||
| offset += 2; | ||
| part = data.slice(offset, offset + size); | ||
| offset += size; | ||
| if (Constants.ID_ZIP64 === signature) { | ||
| parseZip64ExtendedInformation(part); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| throw Utils.Errors.EXTRA_FIELD_PARSE_ERROR(); | ||
| } | ||
| } | ||
| //Override header field values with values from the ZIP64 extra field | ||
| function parseZip64ExtendedInformation(data) { | ||
| var size, compressedSize, offset, diskNumStart; | ||
| if (data.length >= Constants.EF_ZIP64_SCOMP) { | ||
| size = readUInt64LE(data, Constants.EF_ZIP64_SUNCOMP); | ||
| if (_centralHeader.size === Constants.EF_ZIP64_OR_32) { | ||
| _centralHeader.size = size; | ||
| } | ||
| } | ||
| if (data.length >= Constants.EF_ZIP64_RHO) { | ||
| compressedSize = readUInt64LE(data, Constants.EF_ZIP64_SCOMP); | ||
| if (_centralHeader.compressedSize === Constants.EF_ZIP64_OR_32) { | ||
| _centralHeader.compressedSize = compressedSize; | ||
| } | ||
| } | ||
| if (data.length >= Constants.EF_ZIP64_DSN) { | ||
| offset = readUInt64LE(data, Constants.EF_ZIP64_RHO); | ||
| if (_centralHeader.offset === Constants.EF_ZIP64_OR_32) { | ||
| _centralHeader.offset = offset; | ||
| } | ||
| } | ||
| if (data.length >= Constants.EF_ZIP64_DSN + 4) { | ||
| diskNumStart = data.readUInt32LE(Constants.EF_ZIP64_DSN); | ||
| if (_centralHeader.diskNumStart === Constants.EF_ZIP64_OR_16) { | ||
| _centralHeader.diskNumStart = diskNumStart; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| get entryName() { | ||
| return decoder.decode(_entryName); | ||
| }, | ||
| get rawEntryName() { | ||
| return _entryName; | ||
| }, | ||
| set entryName(val) { | ||
| _entryName = Utils.toBuffer(val, decoder.encode); | ||
| var lastChar = _entryName[_entryName.length - 1]; | ||
| _isDirectory = lastChar === 47 || lastChar === 92; | ||
| _centralHeader.fileNameLength = _entryName.length; | ||
| }, | ||
| get efs() { | ||
| if (typeof _efs === "function") { | ||
| return _efs(this.entryName); | ||
| } else { | ||
| return _efs; | ||
| } | ||
| }, | ||
| get extra() { | ||
| return _extra; | ||
| }, | ||
| set extra(val) { | ||
| _extra = val; | ||
| _centralHeader.extraLength = val.length; | ||
| parseExtra(val); | ||
| }, | ||
| get comment() { | ||
| return decoder.decode(_comment); | ||
| }, | ||
| set comment(val) { | ||
| _comment = Utils.toBuffer(val, decoder.encode); | ||
| _centralHeader.commentLength = _comment.length; | ||
| if (_comment.length > 0xffff) throw Utils.Errors.COMMENT_TOO_LONG(); | ||
| }, | ||
| get name() { | ||
| var n = decoder.decode(_entryName); | ||
| return _isDirectory | ||
| ? n | ||
| .substr(n.length - 1) | ||
| .split("/") | ||
| .pop() | ||
| : n.split("/").pop(); | ||
| }, | ||
| get isDirectory() { | ||
| return _isDirectory; | ||
| }, | ||
| getCompressedData: function () { | ||
| return compress(false, null); | ||
| }, | ||
| getCompressedDataAsync: function (/*Function*/ callback) { | ||
| compress(true, callback); | ||
| }, | ||
| setData: function (value) { | ||
| uncompressedData = Utils.toBuffer(value, Utils.decoder.encode); | ||
| if (!_isDirectory && uncompressedData.length) { | ||
| _centralHeader.size = uncompressedData.length; | ||
| _centralHeader.method = Utils.Constants.DEFLATED; | ||
| _centralHeader.crc = Utils.crc32(value); | ||
| _centralHeader.changed = true; | ||
| } else { | ||
| // folders and blank files should be stored | ||
| _centralHeader.method = Utils.Constants.STORED; | ||
| } | ||
| }, | ||
| getData: function (pass) { | ||
| if (_centralHeader.changed) { | ||
| return uncompressedData; | ||
| } else { | ||
| return decompress(false, null, pass); | ||
| } | ||
| }, | ||
| getDataAsync: function (/*Function*/ callback, pass) { | ||
| if (_centralHeader.changed) { | ||
| callback(uncompressedData); | ||
| } else { | ||
| decompress(true, callback, pass); | ||
| } | ||
| }, | ||
| set attr(attr) { | ||
| _centralHeader.attr = attr; | ||
| }, | ||
| get attr() { | ||
| return _centralHeader.attr; | ||
| }, | ||
| set header(/*Buffer*/ data) { | ||
| _centralHeader.loadFromBinary(data); | ||
| }, | ||
| get header() { | ||
| return _centralHeader; | ||
| }, | ||
| packCentralHeader: function () { | ||
| _centralHeader.flags_efs = this.efs; | ||
| _centralHeader.extraLength = _extra.length; | ||
| // 1. create header (buffer) | ||
| var header = _centralHeader.centralHeaderToBinary(); | ||
| var addpos = Utils.Constants.CENHDR; | ||
| // 2. add file name | ||
| _entryName.copy(header, addpos); | ||
| addpos += _entryName.length; | ||
| // 3. add extra data | ||
| _extra.copy(header, addpos); | ||
| addpos += _centralHeader.extraLength; | ||
| // 4. add file comment | ||
| _comment.copy(header, addpos); | ||
| return header; | ||
| }, | ||
| packLocalHeader: function () { | ||
| let addpos = 0; | ||
| _centralHeader.flags_efs = this.efs; | ||
| _centralHeader.extraLocalLength = _extralocal.length; | ||
| // 1. construct local header Buffer | ||
| const localHeaderBuf = _centralHeader.localHeaderToBinary(); | ||
| // 2. localHeader - crate header buffer | ||
| const localHeader = Buffer.alloc(localHeaderBuf.length + _entryName.length + _centralHeader.extraLocalLength); | ||
| // 2.1 add localheader | ||
| localHeaderBuf.copy(localHeader, addpos); | ||
| addpos += localHeaderBuf.length; | ||
| // 2.2 add file name | ||
| _entryName.copy(localHeader, addpos); | ||
| addpos += _entryName.length; | ||
| // 2.3 add extra field | ||
| _extralocal.copy(localHeader, addpos); | ||
| addpos += _extralocal.length; | ||
| return localHeader; | ||
| }, | ||
| toJSON: function () { | ||
| const bytes = function (nr) { | ||
| return "<" + ((nr && nr.length + " bytes buffer") || "null") + ">"; | ||
| }; | ||
| return { | ||
| entryName: this.entryName, | ||
| name: this.name, | ||
| comment: this.comment, | ||
| isDirectory: this.isDirectory, | ||
| header: _centralHeader.toJSON(), | ||
| compressedData: bytes(input), | ||
| data: bytes(uncompressedData) | ||
| }; | ||
| }, | ||
| toString: function () { | ||
| return JSON.stringify(this.toJSON(), null, "\t"); | ||
| } | ||
| }; | ||
| }; |
| const ZipEntry = require("./zipEntry"); | ||
| const Headers = require("./headers"); | ||
| const Utils = require("./util"); | ||
| module.exports = function (/*Buffer|null*/ inBuffer, /** object */ options) { | ||
| var entryList = [], | ||
| entryTable = {}, | ||
| _comment = Buffer.alloc(0), | ||
| mainHeader = new Headers.MainHeader(), | ||
| loadedEntries = false; | ||
| var password = null; | ||
| const temporary = new Set(); | ||
| // assign options | ||
| const opts = options; | ||
| const { noSort, decoder } = opts; | ||
| if (inBuffer) { | ||
| // is a memory buffer | ||
| readMainHeader(opts.readEntries); | ||
| } else { | ||
| // none. is a new file | ||
| loadedEntries = true; | ||
| } | ||
| function makeTemporaryFolders() { | ||
| const foldersList = new Set(); | ||
| // Make list of all folders in file | ||
| for (const elem of Object.keys(entryTable)) { | ||
| const elements = elem.split("/"); | ||
| elements.pop(); // filename | ||
| if (!elements.length) continue; // no folders | ||
| for (let i = 0; i < elements.length; i++) { | ||
| const sub = elements.slice(0, i + 1).join("/") + "/"; | ||
| foldersList.add(sub); | ||
| } | ||
| } | ||
| // create missing folders as temporary | ||
| for (const elem of foldersList) { | ||
| if (!(elem in entryTable)) { | ||
| const tempfolder = new ZipEntry(opts); | ||
| tempfolder.entryName = elem; | ||
| tempfolder.attr = 0x10; | ||
| tempfolder.temporary = true; | ||
| entryList.push(tempfolder); | ||
| entryTable[tempfolder.entryName] = tempfolder; | ||
| temporary.add(tempfolder); | ||
| } | ||
| } | ||
| } | ||
| function readEntries() { | ||
| loadedEntries = true; | ||
| entryTable = {}; | ||
| if (mainHeader.diskEntries > (inBuffer.length - mainHeader.offset) / Utils.Constants.CENHDR) { | ||
| throw Utils.Errors.DISK_ENTRY_TOO_LARGE(); | ||
| } | ||
| entryList = new Array(mainHeader.diskEntries); // total number of entries | ||
| var index = mainHeader.offset; // offset of first CEN header | ||
| for (var i = 0; i < entryList.length; i++) { | ||
| var tmp = index, | ||
| entry = new ZipEntry(opts, inBuffer); | ||
| entry.header = inBuffer.slice(tmp, (tmp += Utils.Constants.CENHDR)); | ||
| entry.entryName = inBuffer.slice(tmp, (tmp += entry.header.fileNameLength)); | ||
| if (entry.header.extraLength) { | ||
| entry.extra = inBuffer.slice(tmp, (tmp += entry.header.extraLength)); | ||
| } | ||
| if (entry.header.commentLength) entry.comment = inBuffer.slice(tmp, tmp + entry.header.commentLength); | ||
| index += entry.header.centralHeaderSize; | ||
| entryList[i] = entry; | ||
| entryTable[entry.entryName] = entry; | ||
| } | ||
| temporary.clear(); | ||
| makeTemporaryFolders(); | ||
| } | ||
| function readMainHeader(/*Boolean*/ readNow) { | ||
| var i = inBuffer.length - Utils.Constants.ENDHDR, // END header size | ||
| max = Math.max(0, i - 0xffff), // 0xFFFF is the max zip file comment length | ||
| n = max, | ||
| endStart = inBuffer.length, | ||
| endOffset = -1, // Start offset of the END header | ||
| commentEnd = 0; | ||
| // option to search header form entire file | ||
| const trailingSpace = typeof opts.trailingSpace === "boolean" ? opts.trailingSpace : false; | ||
| if (trailingSpace) max = 0; | ||
| for (i; i >= n; i--) { | ||
| if (inBuffer[i] !== 0x50) continue; // quick check that the byte is 'P' | ||
| if (inBuffer.readUInt32LE(i) === Utils.Constants.ENDSIG) { | ||
| // "PK\005\006" | ||
| endOffset = i; | ||
| commentEnd = i; | ||
| endStart = i + Utils.Constants.ENDHDR; | ||
| // We already found a regular signature, let's look just a bit further to check if there's any zip64 signature | ||
| n = i - Utils.Constants.END64HDR; | ||
| continue; | ||
| } | ||
| if (inBuffer.readUInt32LE(i) === Utils.Constants.END64SIG) { | ||
| // Found a zip64 signature, let's continue reading the whole zip64 record | ||
| n = max; | ||
| continue; | ||
| } | ||
| if (inBuffer.readUInt32LE(i) === Utils.Constants.ZIP64SIG) { | ||
| // Found the zip64 record, let's determine it's size | ||
| endOffset = i; | ||
| endStart = i + Utils.readBigUInt64LE(inBuffer, i + Utils.Constants.ZIP64SIZE) + Utils.Constants.ZIP64LEAD; | ||
| break; | ||
| } | ||
| } | ||
| if (endOffset == -1) throw Utils.Errors.INVALID_FORMAT(); | ||
| mainHeader.loadFromBinary(inBuffer.slice(endOffset, endStart)); | ||
| if (mainHeader.commentLength) { | ||
| _comment = inBuffer.slice(commentEnd + Utils.Constants.ENDHDR); | ||
| } | ||
| if (readNow) readEntries(); | ||
| } | ||
| function sortEntries() { | ||
| if (entryList.length > 1 && !noSort) { | ||
| entryList.sort((a, b) => a.entryName.toLowerCase().localeCompare(b.entryName.toLowerCase())); | ||
| } | ||
| } | ||
| return { | ||
| /** | ||
| * Returns an array of ZipEntry objects existent in the current opened archive | ||
| * @return Array | ||
| */ | ||
| get entries() { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| return entryList.filter((e) => !temporary.has(e)); | ||
| }, | ||
| /** | ||
| * Archive comment | ||
| * @return {String} | ||
| */ | ||
| get comment() { | ||
| return decoder.decode(_comment); | ||
| }, | ||
| set comment(val) { | ||
| _comment = Utils.toBuffer(val, decoder.encode); | ||
| mainHeader.commentLength = _comment.length; | ||
| }, | ||
| getEntryCount: function () { | ||
| if (!loadedEntries) { | ||
| return mainHeader.diskEntries; | ||
| } | ||
| return entryList.length; | ||
| }, | ||
| forEach: function (callback) { | ||
| this.entries.forEach(callback); | ||
| }, | ||
| /** | ||
| * Returns a reference to the entry with the given name or null if entry is inexistent | ||
| * | ||
| * @param entryName | ||
| * @return ZipEntry | ||
| */ | ||
| getEntry: function (/*String*/ entryName) { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| return entryTable[entryName] || null; | ||
| }, | ||
| /** | ||
| * Adds the given entry to the entry list | ||
| * | ||
| * @param entry | ||
| */ | ||
| setEntry: function (/*ZipEntry*/ entry) { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| entryList.push(entry); | ||
| entryTable[entry.entryName] = entry; | ||
| mainHeader.totalEntries = entryList.length; | ||
| }, | ||
| /** | ||
| * Removes the file with the given name from the entry list. | ||
| * | ||
| * If the entry is a directory, then all nested files and directories will be removed | ||
| * @param entryName | ||
| * @returns {void} | ||
| */ | ||
| deleteFile: function (/*String*/ entryName, withsubfolders = true) { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| const entry = entryTable[entryName]; | ||
| const list = this.getEntryChildren(entry, withsubfolders).map((child) => child.entryName); | ||
| list.forEach(this.deleteEntry); | ||
| }, | ||
| /** | ||
| * Removes the entry with the given name from the entry list. | ||
| * | ||
| * @param {string} entryName | ||
| * @returns {void} | ||
| */ | ||
| deleteEntry: function (/*String*/ entryName) { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| const entry = entryTable[entryName]; | ||
| const index = entryList.indexOf(entry); | ||
| if (index >= 0) { | ||
| entryList.splice(index, 1); | ||
| delete entryTable[entryName]; | ||
| mainHeader.totalEntries = entryList.length; | ||
| } | ||
| }, | ||
| /** | ||
| * Iterates and returns all nested files and directories of the given entry | ||
| * | ||
| * @param entry | ||
| * @return Array | ||
| */ | ||
| getEntryChildren: function (/*ZipEntry*/ entry, subfolders = true) { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| if (typeof entry === "object") { | ||
| if (entry.isDirectory && subfolders) { | ||
| const list = []; | ||
| const name = entry.entryName; | ||
| for (const zipEntry of entryList) { | ||
| if (zipEntry.entryName.startsWith(name)) { | ||
| list.push(zipEntry); | ||
| } | ||
| } | ||
| return list; | ||
| } else { | ||
| return [entry]; | ||
| } | ||
| } | ||
| return []; | ||
| }, | ||
| /** | ||
| * How many child elements entry has | ||
| * | ||
| * @param {ZipEntry} entry | ||
| * @return {integer} | ||
| */ | ||
| getChildCount: function (entry) { | ||
| if (entry && entry.isDirectory) { | ||
| const list = this.getEntryChildren(entry); | ||
| return list.includes(entry) ? list.length - 1 : list.length; | ||
| } | ||
| return 0; | ||
| }, | ||
| /** | ||
| * Returns the zip file | ||
| * | ||
| * @return Buffer | ||
| */ | ||
| compressToBuffer: function () { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| sortEntries(); | ||
| const dataBlock = []; | ||
| const headerBlocks = []; | ||
| let totalSize = 0; | ||
| let dindex = 0; | ||
| mainHeader.size = 0; | ||
| mainHeader.offset = 0; | ||
| let totalEntries = 0; | ||
| for (const entry of this.entries) { | ||
| // compress data and set local and entry header accordingly. Reason why is called first | ||
| const compressedData = entry.getCompressedData(); | ||
| entry.header.offset = dindex; | ||
| // 1. construct local header | ||
| const localHeader = entry.packLocalHeader(); | ||
| // 2. offsets | ||
| const dataLength = localHeader.length + compressedData.length; | ||
| dindex += dataLength; | ||
| // 3. store values in sequence | ||
| dataBlock.push(localHeader); | ||
| dataBlock.push(compressedData); | ||
| // 4. construct central header | ||
| const centralHeader = entry.packCentralHeader(); | ||
| headerBlocks.push(centralHeader); | ||
| // 5. update main header | ||
| mainHeader.size += centralHeader.length; | ||
| totalSize += dataLength + centralHeader.length; | ||
| totalEntries++; | ||
| } | ||
| totalSize += mainHeader.mainHeaderSize; // also includes zip file comment length | ||
| // point to end of data and beginning of central directory first record | ||
| mainHeader.offset = dindex; | ||
| mainHeader.totalEntries = totalEntries; | ||
| dindex = 0; | ||
| const outBuffer = Buffer.alloc(totalSize); | ||
| // write data blocks | ||
| for (const content of dataBlock) { | ||
| content.copy(outBuffer, dindex); | ||
| dindex += content.length; | ||
| } | ||
| // write central directory entries | ||
| for (const content of headerBlocks) { | ||
| content.copy(outBuffer, dindex); | ||
| dindex += content.length; | ||
| } | ||
| // write main header | ||
| const mh = mainHeader.toBinary(); | ||
| if (_comment) { | ||
| _comment.copy(mh, Utils.Constants.ENDHDR); // add zip file comment | ||
| } | ||
| mh.copy(outBuffer, dindex); | ||
| // Since we update entry and main header offsets, | ||
| // they are no longer valid and we have to reset content | ||
| // (Issue 64) | ||
| inBuffer = outBuffer; | ||
| loadedEntries = false; | ||
| return outBuffer; | ||
| }, | ||
| toAsyncBuffer: function (/*Function*/ onSuccess, /*Function*/ onFail, /*Function*/ onItemStart, /*Function*/ onItemEnd) { | ||
| try { | ||
| if (!loadedEntries) { | ||
| readEntries(); | ||
| } | ||
| sortEntries(); | ||
| const dataBlock = []; | ||
| const centralHeaders = []; | ||
| let totalSize = 0; | ||
| let dindex = 0; | ||
| let totalEntries = 0; | ||
| mainHeader.size = 0; | ||
| mainHeader.offset = 0; | ||
| const compress2Buffer = function (entryLists) { | ||
| if (entryLists.length > 0) { | ||
| const entry = entryLists.shift(); | ||
| const name = entry.entryName + entry.extra.toString(); | ||
| if (onItemStart) onItemStart(name); | ||
| entry.getCompressedDataAsync(function (compressedData) { | ||
| if (onItemEnd) onItemEnd(name); | ||
| entry.header.offset = dindex; | ||
| // 1. construct local header | ||
| const localHeader = entry.packLocalHeader(); | ||
| // 2. offsets | ||
| const dataLength = localHeader.length + compressedData.length; | ||
| dindex += dataLength; | ||
| // 3. store values in sequence | ||
| dataBlock.push(localHeader); | ||
| dataBlock.push(compressedData); | ||
| // central header | ||
| const centalHeader = entry.packCentralHeader(); | ||
| centralHeaders.push(centalHeader); | ||
| mainHeader.size += centalHeader.length; | ||
| totalSize += dataLength + centalHeader.length; | ||
| totalEntries++; | ||
| compress2Buffer(entryLists); | ||
| }); | ||
| } else { | ||
| totalSize += mainHeader.mainHeaderSize; // also includes zip file comment length | ||
| // point to end of data and beginning of central directory first record | ||
| mainHeader.offset = dindex; | ||
| mainHeader.totalEntries = totalEntries; | ||
| dindex = 0; | ||
| const outBuffer = Buffer.alloc(totalSize); | ||
| dataBlock.forEach(function (content) { | ||
| content.copy(outBuffer, dindex); // write data blocks | ||
| dindex += content.length; | ||
| }); | ||
| centralHeaders.forEach(function (content) { | ||
| content.copy(outBuffer, dindex); // write central directory entries | ||
| dindex += content.length; | ||
| }); | ||
| const mh = mainHeader.toBinary(); | ||
| if (_comment) { | ||
| _comment.copy(mh, Utils.Constants.ENDHDR); // add zip file comment | ||
| } | ||
| mh.copy(outBuffer, dindex); // write main header | ||
| // Since we update entry and main header offsets, they are no | ||
| // longer valid and we have to reset content using our new buffer | ||
| // (Issue 64) | ||
| inBuffer = outBuffer; | ||
| loadedEntries = false; | ||
| onSuccess(outBuffer); | ||
| } | ||
| }; | ||
| compress2Buffer(Array.from(this.entries)); | ||
| } catch (e) { | ||
| onFail(e); | ||
| } | ||
| } | ||
| }; | ||
| }; |
| { | ||
| "name": "foundry-local-sdk", | ||
| "version": "1.0.0-dev.202604172003", | ||
| "description": "Foundry Local JavaScript SDK", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "type": "module", | ||
| "files": [ | ||
| "dist", | ||
| "prebuilds", | ||
| "script/install-standard.cjs", | ||
| "script/install-utils.cjs", | ||
| "script/preinstall.cjs", | ||
| "deps_versions.json" | ||
| ], | ||
| "scripts": { | ||
| "build": "tsc -p tsconfig.build.json", | ||
| "build:native": "cd native && node-gyp rebuild && node ../script/copy-addon.cjs", | ||
| "docs": "typedoc", | ||
| "example": "tsx examples/chat-completion.ts", | ||
| "install": "node script/install-standard.cjs", | ||
| "pack": "node script/pack.cjs", | ||
| "pack:winml": "node script/pack.cjs winml", | ||
| "preinstall": "node script/preinstall.cjs", | ||
| "test": "mocha --import=tsx test/**/*.test.ts" | ||
| }, | ||
| "dependencies": { | ||
| "adm-zip": "^0.5.16" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/chai": "^5.2.3", | ||
| "@types/mocha": "^10.0.10", | ||
| "@types/node": "^24.10.1", | ||
| "chai": "^6.2.1", | ||
| "mocha": "^11.7.5", | ||
| "node-api-headers": "^1.8.0", | ||
| "tsx": "^4.7.0", | ||
| "typedoc": "^0.28.15", | ||
| "typedoc-plugin-markdown": "^4.2.0", | ||
| "typescript": "^5.9.3" | ||
| }, | ||
| "directories": { | ||
| "doc": "docs", | ||
| "example": "examples", | ||
| "test": "test" | ||
| }, | ||
| "author": "", | ||
| "license": "ISC" | ||
| } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| # Foundry Local JS SDK | ||
| The Foundry Local JS SDK provides a JavaScript/TypeScript interface for running AI models locally on your machine. Discover, download, load, and run inference — all without cloud dependencies. | ||
| ## Features | ||
| - **Local-first AI** — Run models entirely on your machine with no cloud calls | ||
| - **Model catalog** — Browse and discover available models, check what's cached or loaded | ||
| - **Automatic model management** — Download, load, unload, and remove models from cache | ||
| - **Chat completions** — OpenAI-compatible chat API with both synchronous and streaming responses | ||
| - **Audio transcription** — Transcribe audio files locally with streaming support | ||
| - **Multi-variant models** — Models can have multiple variants (e.g., different quantizations) with automatic selection of the best cached variant | ||
| - **Embedded web service** — Start a local HTTP service for OpenAI-compatible API access | ||
| - **WinML support** — Automatic execution provider download on Windows for NPU/GPU acceleration | ||
| - **Configurable inference** — Control temperature, max tokens, top-k, top-p, frequency penalty, and more | ||
| ## Installation | ||
| ```bash | ||
| npm install foundry-local-sdk | ||
| ``` | ||
| ## WinML: Automatic Hardware Acceleration (Windows) | ||
| On Windows, install the WinML package to enable automatic execution provider management. The SDK will automatically discover, download, and register hardware-specific execution providers (e.g., Qualcomm QNN for NPU acceleration) via the Windows App Runtime — no manual driver or EP setup required. | ||
| > **Note:** `foundry-local-sdk-winml` is a Windows-only package. Its install script downloads WinML artifacts during installation and may fail on macOS or Linux. | ||
| ```bash | ||
| npm install foundry-local-sdk-winml | ||
| ``` | ||
| When WinML is enabled: | ||
| - Execution providers like `QNNExecutionProvider`, `OpenVINOExecutionProvider`, etc. are downloaded and registered on the fly, enabling NPU/GPU acceleration without manual configuration | ||
| - **No code changes needed** — your application code stays the same whether WinML is enabled or not | ||
| ### Explicit EP Management | ||
| You can explicitly discover and download execution providers using the `discoverEps()` and `downloadAndRegisterEps()` methods: | ||
| ```typescript | ||
| // Discover available EPs and their status | ||
| const eps = manager.discoverEps(); | ||
| for (const ep of eps) { | ||
| console.log(`${ep.name} — registered: ${ep.isRegistered}`); | ||
| } | ||
| // Download and register all available EPs | ||
| const result = await manager.downloadAndRegisterEps(); | ||
| console.log(`Success: ${result.success}, Status: ${result.status}`); | ||
| // Download only specific EPs | ||
| const result2 = await manager.downloadAndRegisterEps([eps[0].name]); | ||
| ``` | ||
| #### Per-EP download progress | ||
| Pass an optional `progressCallback` to receive `(epName, percent)` updates as each EP downloads (`percent` is 0–100): | ||
| ```typescript | ||
| let currentEp = ''; | ||
| await manager.downloadAndRegisterEps((epName, percent) => { | ||
| if (epName !== currentEp) { | ||
| if (currentEp !== '') { | ||
| process.stdout.write('\n'); | ||
| } | ||
| currentEp = epName; | ||
| } | ||
| process.stdout.write(`\r ${epName} ${percent.toFixed(1)}%`); | ||
| }); | ||
| process.stdout.write('\n'); | ||
| ``` | ||
| Catalog access does not block on EP downloads. Call `downloadAndRegisterEps()` when you need hardware-accelerated execution providers. | ||
| ## Quick Start | ||
| ```typescript | ||
| import { FoundryLocalManager } from 'foundry-local-sdk'; | ||
| const manager = FoundryLocalManager.create({ | ||
| appName: 'foundry_local_samples', | ||
| logLevel: 'info' | ||
| }); | ||
| // Get the model object | ||
| const modelAlias = 'qwen2.5-0.5b'; | ||
| const model = await manager.catalog.getModel(modelAlias); | ||
| // Download the model | ||
| console.log(`\nDownloading model ${modelAlias}...`); | ||
| await model.download((progress) => { | ||
| process.stdout.write(`\rDownloading... ${progress.toFixed(2)}%`); | ||
| }); | ||
| // Load the model | ||
| await model.load(); | ||
| // Create chat client | ||
| const chatClient = model.createChatClient(); | ||
| // Example chat completion | ||
| console.log('\nTesting chat completion...'); | ||
| const completion = await chatClient.completeChat([ | ||
| { role: 'user', content: 'Why is the sky blue?' } | ||
| ]); | ||
| console.log(completion.choices[0]?.message?.content); | ||
| // Example streaming completion | ||
| console.log('\nTesting streaming completion...'); | ||
| for await (const chunk of chatClient.completeStreamingChat( | ||
| [{ role: 'user', content: 'Write a short poem about programming.' }] | ||
| )) { | ||
| const content = chunk.choices?.[0]?.delta?.content; | ||
| if (content) { | ||
| process.stdout.write(content); | ||
| } | ||
| } | ||
| console.log('\n'); | ||
| // Unload the model | ||
| await model.unload(); | ||
| ``` | ||
| ## Usage | ||
| ### Browsing the Model Catalog | ||
| The `Catalog` lets you discover what models are available, which are already cached locally, and which are currently loaded in memory. | ||
| ```typescript | ||
| const catalog = manager.catalog; | ||
| // List all available models | ||
| const models = await catalog.getModels(); | ||
| models.forEach(model => { | ||
| console.log(`${model.alias} — cached: ${model.isCached}`); | ||
| }); | ||
| // See what's already downloaded | ||
| const cached = await catalog.getCachedModels(); | ||
| // See what's currently loaded in memory | ||
| const loaded = await catalog.getLoadedModels(); | ||
| ``` | ||
| ### Loading and Running Models | ||
| Each model can have multiple variants (different quantizations or formats). The SDK automatically selects the best available variant, preferring cached versions. All models implement the `IModel` interface. | ||
| ```typescript | ||
| const model = await catalog.getModel('qwen2.5-0.5b'); | ||
| // Download if not cached (with optional progress tracking) | ||
| if (!model.isCached) { | ||
| await model.download((progress) => { | ||
| console.log(`Download: ${progress}%`); | ||
| }); | ||
| } | ||
| // Load into memory and run inference | ||
| await model.load(); | ||
| const chatClient = model.createChatClient(); | ||
| ``` | ||
| You can also select a specific variant manually: | ||
| ```typescript | ||
| const variants = model.variants; | ||
| model.selectVariant(variants[0]); | ||
| ``` | ||
| ### Chat Completions | ||
| The `ChatClient` follows the OpenAI Chat Completion API structure. | ||
| ```typescript | ||
| const chatClient = model.createChatClient(); | ||
| // Configure settings | ||
| chatClient.settings.temperature = 0.7; | ||
| chatClient.settings.maxTokens = 800; | ||
| chatClient.settings.topP = 0.9; | ||
| // Synchronous completion | ||
| const response = await chatClient.completeChat([ | ||
| { role: 'system', content: 'You are a helpful assistant.' }, | ||
| { role: 'user', content: 'Explain quantum computing in simple terms.' } | ||
| ]); | ||
| console.log(response.choices[0].message.content); | ||
| ``` | ||
| ### Streaming Responses | ||
| For real-time output, use streaming: | ||
| ```typescript | ||
| for await (const chunk of chatClient.completeStreamingChat( | ||
| [{ role: 'user', content: 'Write a short poem about programming.' }] | ||
| )) { | ||
| const content = chunk.choices?.[0]?.delta?.content; | ||
| if (content) { | ||
| process.stdout.write(content); | ||
| } | ||
| } | ||
| ``` | ||
| ### Audio Transcription | ||
| Transcribe audio files locally using the `AudioClient`: | ||
| ```typescript | ||
| const audioClient = model.createAudioClient(); | ||
| audioClient.settings.language = 'en'; | ||
| // Synchronous transcription | ||
| const result = await audioClient.transcribe('/path/to/audio.wav'); | ||
| // Streaming transcription | ||
| for await (const chunk of audioClient.transcribeStreaming('/path/to/audio.wav')) { | ||
| console.log(chunk); | ||
| } | ||
| ``` | ||
| ### Embedded Web Service | ||
| Start a local HTTP server that exposes an OpenAI-compatible API: | ||
| ```typescript | ||
| manager.startWebService(); | ||
| console.log('Service running at:', manager.urls); | ||
| // Use with any OpenAI-compatible client library | ||
| // ... | ||
| manager.stopWebService(); | ||
| ``` | ||
| ### Configuration | ||
| The SDK is configured via `FoundryLocalConfig` when creating the manager: | ||
| | Option | Description | Default | | ||
| |--------|-------------|---------| | ||
| | `appName` | **Required.** Application name for logs and telemetry. | — | | ||
| | `appDataDir` | Directory where application data should be stored | `~/.{appName}` | | ||
| | `logLevel` | Logging level: `trace`, `debug`, `info`, `warn`, `error`, `fatal` | `warn` | | ||
| | `modelCacheDir` | Directory for downloaded models | `~/.{appName}/cache/models` | | ||
| | `logsDir` | Directory for log files | `~/.{appName}/logs` | | ||
| | `libraryPath` | Path to native Foundry Local Core libraries | Auto-discovered | | ||
| | `serviceEndpoint` | URL of an existing external service to connect to | — | | ||
| | `webServiceUrls` | URL(s) for the embedded web service to bind to | — | | ||
| ## API Reference | ||
| Auto-generated class documentation lives in [`docs/classes/`](docs/classes/): | ||
| - [FoundryLocalManager](docs/classes/FoundryLocalManager.md) — SDK entry point, web service management | ||
| - [Catalog](docs/classes/Catalog.md) — Model discovery and browsing | ||
| - [IModel](docs/README.md#imodel) — Model interface: variant selection, download, load, inference | ||
| - [ChatClient](docs/classes/ChatClient.md) — Chat completions (sync and streaming) | ||
| - [AudioClient](docs/classes/AudioClient.md) — Audio transcription (sync and streaming) | ||
| - [ModelLoadManager](docs/classes/ModelLoadManager.md) — Low-level model loading management | ||
| ## Contributing: Building from Source | ||
| ### Prerequisites | ||
| - **Node.js 20+** | ||
| - **Python 3.x** — required by `node-gyp` for compiling the native addon | ||
| - **C/C++ toolchain**: | ||
| - **Windows**: Visual Studio Build Tools (the "Desktop development with C++" workload) | ||
| - **Linux**: `build-essential` (`apt install build-essential`) | ||
| - **macOS**: Xcode Command Line Tools (`xcode-select --install`) | ||
| ### Build Steps | ||
| ```bash | ||
| # 1. Install JS dependencies (also downloads native core binaries) | ||
| npm install | ||
| # 2. Build the Node-API native addon (compiles C code and copies to prebuilds/) | ||
| npm run build:native | ||
| # 3. Build the TypeScript source | ||
| npm run build | ||
| # 4. Run tests | ||
| npm test | ||
| # 5. Pack the SDK into a .tgz (includes prebuilt addon for your platform) | ||
| npm run pack | ||
| ``` | ||
| > **Note:** `npm run build:native` compiles the addon only for your current platform. The published npm package includes prebuilt addons for all supported platforms (win32-x64, win32-arm64, linux-x64, darwin-arm64), which are compiled in CI. | ||
| ## Running Tests | ||
| ```bash | ||
| npm test | ||
| ``` | ||
| See `test/README.md` for details on prerequisites and setup. | ||
| ## Running Examples | ||
| ```bash | ||
| npm run example | ||
| ``` | ||
| This runs the chat completion example in `examples/chat-completion.ts`. |
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
| // Shared NuGet download and extraction utilities for install scripts. | ||
| 'use strict'; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const os = require('os'); | ||
| const https = require('https'); | ||
| const AdmZip = require('adm-zip'); | ||
| const PLATFORM_MAP = { | ||
| 'win32-x64': 'win-x64', | ||
| 'win32-arm64': 'win-arm64', | ||
| 'linux-x64': 'linux-x64', | ||
| 'darwin-arm64': 'osx-arm64', | ||
| }; | ||
| const platformKey = `${os.platform()}-${os.arch()}`; | ||
| const RID = PLATFORM_MAP[platformKey]; | ||
| // Install binaries into foundry-local-core/<platform> inside the package root. | ||
| const BIN_DIR = path.join(__dirname, '..', 'foundry-local-core', platformKey); | ||
| const EXT = os.platform() === 'win32' ? '.dll' : os.platform() === 'darwin' ? '.dylib' : '.so'; | ||
| const REQUIRED_FILES = [ | ||
| `Microsoft.AI.Foundry.Local.Core${EXT}`, | ||
| `${os.platform() === 'win32' ? '' : 'lib'}onnxruntime${EXT}`, | ||
| `${os.platform() === 'win32' ? '' : 'lib'}onnxruntime-genai${EXT}`, | ||
| ]; | ||
| const NUGET_FEED = 'https://api.nuget.org/v3/index.json'; | ||
| const ORT_NIGHTLY_FEED = 'https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json'; | ||
| // --- Download helpers --- | ||
| async function downloadWithRetryAndRedirects(url, destStream = null) { | ||
| const maxRedirects = 5; | ||
| let currentUrl = url; | ||
| let redirects = 0; | ||
| while (redirects < maxRedirects) { | ||
| const response = await new Promise((resolve, reject) => { | ||
| https.get(currentUrl, (res) => resolve(res)) | ||
| .on('error', reject); | ||
| }); | ||
| if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { | ||
| currentUrl = response.headers.location; | ||
| response.resume(); | ||
| redirects++; | ||
| console.log(` Following redirect to ${new URL(currentUrl).host}...`); | ||
| continue; | ||
| } | ||
| if (response.statusCode !== 200) { | ||
| throw new Error(`Download failed with status ${response.statusCode}: ${currentUrl}`); | ||
| } | ||
| if (destStream) { | ||
| response.pipe(destStream); | ||
| return new Promise((resolve, reject) => { | ||
| destStream.on('finish', resolve); | ||
| destStream.on('error', reject); | ||
| response.on('error', reject); | ||
| }); | ||
| } else { | ||
| let data = ''; | ||
| response.on('data', chunk => data += chunk); | ||
| return new Promise((resolve, reject) => { | ||
| response.on('end', () => resolve(data)); | ||
| response.on('error', reject); | ||
| }); | ||
| } | ||
| } | ||
| throw new Error('Too many redirects'); | ||
| } | ||
| async function downloadJson(url) { | ||
| return JSON.parse(await downloadWithRetryAndRedirects(url)); | ||
| } | ||
| async function downloadFile(url, dest) { | ||
| const file = fs.createWriteStream(dest); | ||
| try { | ||
| await downloadWithRetryAndRedirects(url, file); | ||
| file.close(); | ||
| } catch (e) { | ||
| file.close(); | ||
| if (fs.existsSync(dest)) fs.unlinkSync(dest); | ||
| throw e; | ||
| } | ||
| } | ||
| const serviceIndexCache = new Map(); | ||
| async function getBaseAddress(feedUrl) { | ||
| if (!serviceIndexCache.has(feedUrl)) { | ||
| serviceIndexCache.set(feedUrl, await downloadJson(feedUrl)); | ||
| } | ||
| const resources = serviceIndexCache.get(feedUrl).resources || []; | ||
| const res = resources.find(r => r['@type'] && r['@type'].startsWith('PackageBaseAddress/3.0.0')); | ||
| if (!res) throw new Error('Could not find PackageBaseAddress/3.0.0 in NuGet feed.'); | ||
| const baseAddress = res['@id']; | ||
| return baseAddress.endsWith('/') ? baseAddress : baseAddress + '/'; | ||
| } | ||
| async function installPackage(artifact, tempDir, binDir, skipIfPresent) { | ||
| const pkgName = artifact.name; | ||
| const pkgVer = artifact.version; | ||
| // Skip download if this package's main native binary is already present | ||
| // (e.g. pre-populated by CI from a locally-built artifact). | ||
| // Callers pass skipIfPresent=false when overriding (e.g. WinML over standard). | ||
| if (skipIfPresent) { | ||
| const prefix = os.platform() === 'win32' ? '' : 'lib'; | ||
| let expectedFile; | ||
| if (pkgName.includes('Foundry.Local.Core')) { | ||
| expectedFile = `Microsoft.AI.Foundry.Local.Core${EXT}`; | ||
| } else if (pkgName.includes('OnnxRuntimeGenAI')) { | ||
| expectedFile = `${prefix}onnxruntime-genai${EXT}`; | ||
| } else if (pkgName.includes('OnnxRuntime')) { | ||
| expectedFile = `${prefix}onnxruntime${EXT}`; | ||
| } | ||
| if (expectedFile && fs.existsSync(path.join(binDir, expectedFile))) { | ||
| console.log(` ${pkgName}: already present, skipping download.`); | ||
| return; | ||
| } | ||
| } | ||
| const baseAddress = await getBaseAddress(artifact.feed); | ||
| const nameLower = pkgName.toLowerCase(); | ||
| const verLower = pkgVer.toLowerCase(); | ||
| const downloadUrl = `${baseAddress}${nameLower}/${verLower}/${nameLower}.${verLower}.nupkg`; | ||
| const nupkgPath = path.join(tempDir, `${pkgName}.${pkgVer}.nupkg`); | ||
| console.log(` Downloading ${pkgName} ${pkgVer}...`); | ||
| await downloadFile(downloadUrl, nupkgPath); | ||
| console.log(` Extracting...`); | ||
| const zip = new AdmZip(nupkgPath); | ||
| const targetPathPrefix = `runtimes/${RID}/native/`.toLowerCase(); | ||
| const entries = zip.getEntries().filter(e => { | ||
| const p = e.entryName.toLowerCase(); | ||
| return p.includes(targetPathPrefix) && p.endsWith(EXT); | ||
| }); | ||
| if (entries.length > 0) { | ||
| entries.forEach(entry => { | ||
| zip.extractEntryTo(entry, binDir, false, true); | ||
| console.log(` Extracted ${entry.name}`); | ||
| }); | ||
| } else { | ||
| console.warn(` No files found for RID ${RID} in ${pkgName}.`); | ||
| } | ||
| // Write a metadata package.json with version info for diagnostics | ||
| if (pkgName.startsWith('Microsoft.AI.Foundry.Local.Core')) { | ||
| const pkgJsonPath = path.join(binDir, 'package.json'); | ||
| const pkgContent = { | ||
| name: `@foundry-local-core/${platformKey}`, | ||
| version: pkgVer, | ||
| description: `Native binaries for Foundry Local SDK (${platformKey})`, | ||
| private: true | ||
| }; | ||
| fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2)); | ||
| } | ||
| } | ||
| async function runInstall(artifacts, options) { | ||
| if (!RID) { | ||
| console.warn(`[foundry-local] Unsupported platform: ${platformKey}. Skipping.`); | ||
| return; | ||
| } | ||
| const binDir = (options && options.binDir) || BIN_DIR; | ||
| // When a custom binDir is provided (e.g. WinML overriding standard), | ||
| // don't skip packages whose output files already exist — we need to | ||
| // overwrite them with the variant's binaries. | ||
| const skipIfPresent = !(options && options.binDir); | ||
| console.log(`[foundry-local] Installing native libraries for ${RID}...`); | ||
| fs.mkdirSync(binDir, { recursive: true }); | ||
| const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foundry-install-')); | ||
| try { | ||
| for (const artifact of artifacts) { | ||
| await installPackage(artifact, tempDir, binDir, skipIfPresent); | ||
| } | ||
| console.log('[foundry-local] Installation complete.'); | ||
| } finally { | ||
| try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {} | ||
| } | ||
| } | ||
| module.exports = { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall }; |
| // | ||
| // Copyright 2021-2022 Picovoice Inc. | ||
| // | ||
| // You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" | ||
| // file accompanying this source. | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations under the License. | ||
| // | ||
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const pv_recorder_status_t_1 = require("./pv_recorder_status_t"); | ||
| class PvRecorderStatusOutOfMemoryError extends Error { | ||
| } | ||
| class PvRecorderStatusInvalidArgumentError extends Error { | ||
| } | ||
| class PvRecorderStatusInvalidStateError extends Error { | ||
| } | ||
| class PvRecorderStatusBackendError extends Error { | ||
| } | ||
| class PvRecorderStatusDeviceAlreadyInitializedError extends Error { | ||
| } | ||
| class PvRecorderStatusDeviceNotInitializedError extends Error { | ||
| } | ||
| class PvRecorderStatusIOError extends Error { | ||
| } | ||
| class PvRecorderStatusRuntimeError extends Error { | ||
| } | ||
| function pvRecorderStatusToException(status, errorMessage) { | ||
| switch (status) { | ||
| case pv_recorder_status_t_1.default.OUT_OF_MEMORY: | ||
| return new PvRecorderStatusOutOfMemoryError(errorMessage); | ||
| case pv_recorder_status_t_1.default.INVALID_ARGUMENT: | ||
| return new PvRecorderStatusInvalidArgumentError(errorMessage); | ||
| case pv_recorder_status_t_1.default.INVALID_STATE: | ||
| return new PvRecorderStatusInvalidStateError(errorMessage); | ||
| case pv_recorder_status_t_1.default.BACKEND_ERROR: | ||
| return new PvRecorderStatusBackendError(errorMessage); | ||
| case pv_recorder_status_t_1.default.DEVICE_ALREADY_INITIALIZED: | ||
| return new PvRecorderStatusDeviceAlreadyInitializedError(errorMessage); | ||
| case pv_recorder_status_t_1.default.DEVICE_NOT_INITIALIZED: | ||
| return new PvRecorderStatusDeviceNotInitializedError(errorMessage); | ||
| case pv_recorder_status_t_1.default.IO_ERROR: | ||
| return new PvRecorderStatusIOError(errorMessage); | ||
| case pv_recorder_status_t_1.default.RUNTIME_ERROR: | ||
| return new PvRecorderStatusRuntimeError(errorMessage); | ||
| default: | ||
| // eslint-disable-next-line | ||
| console.warn(`Unknown error code: ${status}`); | ||
| return new Error(errorMessage); | ||
| } | ||
| } | ||
| exports.default = pvRecorderStatusToException; | ||
| //# sourceMappingURL=errors.js.map |
| {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,qCAAqC;AACrC,EAAE;AACF,qHAAqH;AACrH,iCAAiC;AACjC,EAAE;AACF,sHAAsH;AACtH,qHAAqH;AACrH,6EAA6E;AAC7E,EAAE;AACF,YAAY,CAAC;;AAEb,iEAAsD;AAEtD,MAAM,gCAAiC,SAAQ,KAAK;CAAG;AACvD,MAAM,oCAAqC,SAAQ,KAAK;CAAG;AAC3D,MAAM,iCAAkC,SAAQ,KAAK;CAAG;AACxD,MAAM,4BAA6B,SAAQ,KAAK;CAAG;AACnD,MAAM,6CAA8C,SAAQ,KAAK;CAAG;AACpE,MAAM,yCAA0C,SAAQ,KAAK;CAAG;AAChE,MAAM,uBAAwB,SAAQ,KAAK;CAAG;AAC9C,MAAM,4BAA6B,SAAQ,KAAK;CAAG;AAEnD,SAAS,2BAA2B,CAAC,MAAwB,EAAE,YAAoB;IACjF,QAAQ,MAAM,EAAE;QACd,KAAK,8BAAgB,CAAC,aAAa;YACjC,OAAO,IAAI,gCAAgC,CAAC,YAAY,CAAC,CAAC;QAC5D,KAAK,8BAAgB,CAAC,gBAAgB;YACpC,OAAO,IAAI,oCAAoC,CAAC,YAAY,CAAC,CAAC;QAChE,KAAK,8BAAgB,CAAC,aAAa;YACjC,OAAO,IAAI,iCAAiC,CAAC,YAAY,CAAC,CAAC;QAC7D,KAAK,8BAAgB,CAAC,aAAa;YACjC,OAAO,IAAI,4BAA4B,CAAC,YAAY,CAAC,CAAC;QACxD,KAAK,8BAAgB,CAAC,0BAA0B;YAC9C,OAAO,IAAI,6CAA6C,CAAC,YAAY,CAAC,CAAC;QACzE,KAAK,8BAAgB,CAAC,sBAAsB;YAC1C,OAAO,IAAI,yCAAyC,CAAC,YAAY,CAAC,CAAC;QACrE,KAAK,8BAAgB,CAAC,QAAQ;YAC5B,OAAO,IAAI,uBAAuB,CAAC,YAAY,CAAC,CAAC;QACnD,KAAK,8BAAgB,CAAC,aAAa;YACjC,OAAO,IAAI,4BAA4B,CAAC,YAAY,CAAC,CAAC;QACxD;YACE,2BAA2B;YAC3B,OAAO,CAAC,IAAI,CAAC,uBAAuB,MAAM,EAAE,CAAC,CAAC;YAC9C,OAAO,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;KAClC;AACH,CAAC;AAED,kBAAe,2BAA2B,CAAC"} |
| // | ||
| // Copyright 2021-2022 Picovoice Inc. | ||
| // | ||
| // You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" | ||
| // file accompanying this source. | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations under the License. | ||
| // | ||
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.PvRecorder = void 0; | ||
| const pv_recorder_1 = require("./pv_recorder"); | ||
| exports.PvRecorder = pv_recorder_1.default; | ||
| //# sourceMappingURL=index.js.map |
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,qCAAqC;AACrC,EAAE;AACF,qHAAqH;AACrH,iCAAiC;AACjC,EAAE;AACF,sHAAsH;AACtH,qHAAqH;AACrH,6EAA6E;AAC7E,EAAE;AACF,YAAY,CAAC;;;AAEb,+CAAuC;AAE9B,qBAFF,qBAAU,CAEE"} |
| // | ||
| // Copyright 2025 Picovoice Inc. | ||
| // | ||
| // You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" | ||
| // file accompanying this source. | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations under the License. | ||
| // | ||
| 'use strict'; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.getSystemLibraryPath = exports.getPlatform = void 0; | ||
| const fs = require("fs"); | ||
| const os = require("os"); | ||
| const path = require("path"); | ||
| const SYSTEM_LINUX = 'linux'; | ||
| const SYSTEM_MAC = 'darwin'; | ||
| const SYSTEM_WINDOWS = 'win32'; | ||
| const X86_64 = 'x64'; | ||
| const ARM_32 = 'arm'; | ||
| const ARM_64 = 'arm64'; | ||
| const PLATFORM_LINUX = 'linux'; | ||
| const PLATFORM_MAC = 'mac'; | ||
| const PLATFORM_RASPBERRY_PI = 'raspberry-pi'; | ||
| const PLATFORM_WINDOWS = 'windows'; | ||
| const ARM_CPU_64 = '-aarch64'; | ||
| const ARM_CPU_CORTEX_A53 = 'cortex-a53'; | ||
| const ARM_CPU_CORTEX_A72 = 'cortex-a72'; | ||
| const ARM_CPU_CORTEX_A76 = 'cortex-a76'; | ||
| const SUPPORTED_NODEJS_SYSTEMS = new Set([ | ||
| SYSTEM_LINUX, | ||
| SYSTEM_MAC, | ||
| SYSTEM_WINDOWS, | ||
| ]); | ||
| const LIBRARY_PATH_PREFIX = '../lib/'; | ||
| const SYSTEM_TO_LIBRARY_PATH = new Map(); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_MAC}/${X86_64}`, `${PLATFORM_MAC}/x86_64/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_MAC}/${ARM_64}`, `${PLATFORM_MAC}/arm64/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${X86_64}`, `${PLATFORM_LINUX}/x86_64/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${ARM_CPU_CORTEX_A53}`, `${PLATFORM_RASPBERRY_PI}/${ARM_CPU_CORTEX_A53}/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${ARM_CPU_CORTEX_A53}${ARM_CPU_64}`, `${PLATFORM_RASPBERRY_PI}/${ARM_CPU_CORTEX_A53}${ARM_CPU_64}/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${ARM_CPU_CORTEX_A72}`, `${PLATFORM_RASPBERRY_PI}/${ARM_CPU_CORTEX_A72}/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${ARM_CPU_CORTEX_A72}${ARM_CPU_64}`, `${PLATFORM_RASPBERRY_PI}/${ARM_CPU_CORTEX_A72}${ARM_CPU_64}/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${ARM_CPU_CORTEX_A76}`, `${PLATFORM_RASPBERRY_PI}/${ARM_CPU_CORTEX_A76}/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_LINUX}/${ARM_CPU_CORTEX_A76}${ARM_CPU_64}`, `${PLATFORM_RASPBERRY_PI}/${ARM_CPU_CORTEX_A76}${ARM_CPU_64}/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_WINDOWS}/${X86_64}`, `${PLATFORM_WINDOWS}/amd64/pv_recorder.node`); | ||
| SYSTEM_TO_LIBRARY_PATH.set(`${SYSTEM_WINDOWS}/${ARM_64}`, `${PLATFORM_WINDOWS}/arm64/pv_recorder.node`); | ||
| function absoluteLibraryPath(libraryPath) { | ||
| return path.resolve(__dirname, LIBRARY_PATH_PREFIX, libraryPath); | ||
| } | ||
| function getCpuPart() { | ||
| const cpuInfo = fs.readFileSync('/proc/cpuinfo', 'ascii'); | ||
| for (const infoLine of cpuInfo.split('\n')) { | ||
| if (infoLine.includes('CPU part')) { | ||
| const infoLineSplit = infoLine.split(' '); | ||
| return infoLineSplit[infoLineSplit.length - 1].toLowerCase(); | ||
| } | ||
| } | ||
| throw new Error(`Unsupported CPU.`); | ||
| } | ||
| function getLinuxPlatform() { | ||
| const cpuPart = getCpuPart(); | ||
| switch (cpuPart) { | ||
| case '0xd03': | ||
| case '0xd08': | ||
| case '0xd0b': | ||
| return PLATFORM_RASPBERRY_PI; | ||
| default: | ||
| throw new Error(`Unsupported CPU: '${cpuPart}'`); | ||
| } | ||
| } | ||
| function getLinuxMachine(arch) { | ||
| let archInfo = ''; | ||
| if (arch === ARM_64) { | ||
| archInfo = ARM_CPU_64; | ||
| } | ||
| const cpuPart = getCpuPart(); | ||
| switch (cpuPart) { | ||
| case '0xd03': | ||
| return ARM_CPU_CORTEX_A53 + archInfo; | ||
| case '0xd08': | ||
| return ARM_CPU_CORTEX_A72 + archInfo; | ||
| case '0xd0b': | ||
| return ARM_CPU_CORTEX_A76 + archInfo; | ||
| default: | ||
| throw new Error(`Unsupported CPU: '${cpuPart}'`); | ||
| } | ||
| } | ||
| function getPlatform() { | ||
| const system = os.platform(); | ||
| const arch = os.arch(); | ||
| if (system === SYSTEM_MAC && (arch === X86_64 || arch === ARM_64)) { | ||
| return PLATFORM_MAC; | ||
| } | ||
| if (system === SYSTEM_WINDOWS && (arch === X86_64 || arch === ARM_64)) { | ||
| return PLATFORM_WINDOWS; | ||
| } | ||
| if (system === SYSTEM_LINUX) { | ||
| if (arch === X86_64) { | ||
| return PLATFORM_LINUX; | ||
| } | ||
| return getLinuxPlatform(); | ||
| } | ||
| throw `System ${system}/${arch} is not supported by this library.`; | ||
| } | ||
| exports.getPlatform = getPlatform; | ||
| function getSystemLibraryPath() { | ||
| const system = os.platform(); | ||
| const arch = os.arch(); | ||
| if (SUPPORTED_NODEJS_SYSTEMS.has(system)) { | ||
| switch (system) { | ||
| case SYSTEM_MAC: { | ||
| if (arch === X86_64) { | ||
| return absoluteLibraryPath(SYSTEM_TO_LIBRARY_PATH.get(`${SYSTEM_MAC}/${X86_64}`)); | ||
| } | ||
| else if (arch === ARM_64) { | ||
| return absoluteLibraryPath(SYSTEM_TO_LIBRARY_PATH.get(`${SYSTEM_MAC}/${ARM_64}`)); | ||
| } | ||
| break; | ||
| } | ||
| case SYSTEM_LINUX: { | ||
| if (arch === X86_64) { | ||
| return absoluteLibraryPath(SYSTEM_TO_LIBRARY_PATH.get(`${SYSTEM_LINUX}/${X86_64}`)); | ||
| } | ||
| else if (arch === ARM_32 || arch === ARM_64) { | ||
| const linuxMachine = getLinuxMachine(arch); | ||
| if (linuxMachine !== null) { | ||
| return absoluteLibraryPath(SYSTEM_TO_LIBRARY_PATH.get(`${SYSTEM_LINUX}/${linuxMachine}`)); | ||
| } | ||
| throw new Error(`System ${system}/${arch} is not supported by this library for this CPU.`); | ||
| } | ||
| break; | ||
| } | ||
| case SYSTEM_WINDOWS: { | ||
| if (arch === X86_64 || arch === ARM_64) { | ||
| return absoluteLibraryPath(SYSTEM_TO_LIBRARY_PATH.get(`${SYSTEM_WINDOWS}/${arch}`)); | ||
| } | ||
| break; | ||
| } | ||
| default: { | ||
| throw new Error(`System ${system}/${arch} is not supported by this library.`); | ||
| } | ||
| } | ||
| } | ||
| throw new Error(`System ${system}/${arch} is not supported by this library.`); | ||
| } | ||
| exports.getSystemLibraryPath = getSystemLibraryPath; | ||
| //# sourceMappingURL=platforms.js.map |
| {"version":3,"file":"platforms.js","sourceRoot":"","sources":["../src/platforms.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,gCAAgC;AAChC,EAAE;AACF,qHAAqH;AACrH,iCAAiC;AACjC,EAAE;AACF,sHAAsH;AACtH,qHAAqH;AACrH,6EAA6E;AAC7E,EAAE;AACF,YAAY,CAAC;;;AAEb,yBAAyB;AACzB,yBAAyB;AACzB,6BAA6B;AAE7B,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,UAAU,GAAG,QAAQ,CAAC;AAC5B,MAAM,cAAc,GAAG,OAAO,CAAC;AAE/B,MAAM,MAAM,GAAG,KAAK,CAAC;AACrB,MAAM,MAAM,GAAG,KAAK,CAAC;AACrB,MAAM,MAAM,GAAG,OAAO,CAAC;AAEvB,MAAM,cAAc,GAAG,OAAO,CAAC;AAC/B,MAAM,YAAY,GAAG,KAAK,CAAC;AAC3B,MAAM,qBAAqB,GAAG,cAAc,CAAC;AAC7C,MAAM,gBAAgB,GAAG,SAAS,CAAC;AAEnC,MAAM,UAAU,GAAG,UAAU,CAAC;AAC9B,MAAM,kBAAkB,GAAG,YAAY,CAAC;AACxC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AACxC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAExC,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC;IACvC,YAAY;IACZ,UAAU;IACV,cAAc;CACf,CAAC,CAAC;AAEH,MAAM,mBAAmB,GAAG,SAAS,CAAC;AACtC,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAAE,CAAC;AACzC,sBAAsB,CAAC,GAAG,CACxB,GAAG,UAAU,IAAI,MAAM,EAAE,EACzB,GAAG,YAAY,0BAA0B,CAC1C,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,UAAU,IAAI,MAAM,EAAE,EACzB,GAAG,YAAY,yBAAyB,CACzC,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,MAAM,EAAE,EAC3B,GAAG,cAAc,0BAA0B,CAC5C,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,kBAAkB,EAAE,EACvC,GAAG,qBAAqB,IAAI,kBAAkB,mBAAmB,CAClE,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,kBAAkB,GAAG,UAAU,EAAE,EACpD,GAAG,qBAAqB,IAAI,kBAAkB,GAAG,UAAU,mBAAmB,CAC/E,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,kBAAkB,EAAE,EACvC,GAAG,qBAAqB,IAAI,kBAAkB,mBAAmB,CAClE,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,kBAAkB,GAAG,UAAU,EAAE,EACpD,GAAG,qBAAqB,IAAI,kBAAkB,GAAG,UAAU,mBAAmB,CAC/E,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,kBAAkB,EAAE,EACvC,GAAG,qBAAqB,IAAI,kBAAkB,mBAAmB,CAClE,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,YAAY,IAAI,kBAAkB,GAAG,UAAU,EAAE,EACpD,GAAG,qBAAqB,IAAI,kBAAkB,GAAG,UAAU,mBAAmB,CAC/E,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,cAAc,IAAI,MAAM,EAAE,EAC7B,GAAG,gBAAgB,yBAAyB,CAC7C,CAAC;AACF,sBAAsB,CAAC,GAAG,CACxB,GAAG,cAAc,IAAI,MAAM,EAAE,EAC7B,GAAG,gBAAgB,yBAAyB,CAC7C,CAAC;AAEF,SAAS,mBAAmB,CAAC,WAAmB;IAC9C,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,mBAAmB,EAAE,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAC1D,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;QAC1C,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE;YACjC,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC1C,OAAO,aAAa,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SAC9D;KACF;IACD,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,gBAAgB;IACvB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,QAAQ,OAAO,EAAE;QACf,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;QACb,KAAK,OAAO;YACV,OAAO,qBAAqB,CAAC;QAC/B;YACE,MAAM,IAAI,KAAK,CAAC,qBAAqB,OAAO,GAAG,CAAC,CAAC;KACpD;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,IAAI,KAAK,MAAM,EAAE;QACnB,QAAQ,GAAG,UAAU,CAAC;KACvB;IAED,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,QAAQ,OAAO,EAAE;QACf,KAAK,OAAO;YACV,OAAO,kBAAkB,GAAG,QAAQ,CAAC;QACvC,KAAK,OAAO;YACV,OAAO,kBAAkB,GAAG,QAAQ,CAAC;QACvC,KAAK,OAAO;YACV,OAAO,kBAAkB,GAAG,QAAQ,CAAC;QACvC;YACE,MAAM,IAAI,KAAK,CAAC,qBAAqB,OAAO,GAAG,CAAC,CAAC;KACpD;AACH,CAAC;AAED,SAAgB,WAAW;IACzB,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;IAEvB,IAAI,MAAM,KAAK,UAAU,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,CAAC,EAAE;QACjE,OAAO,YAAY,CAAC;KACrB;IAED,IAAI,MAAM,KAAK,cAAc,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,CAAC,EAAE;QACrE,OAAO,gBAAgB,CAAC;KACzB;IAED,IAAI,MAAM,KAAK,YAAY,EAAE;QAC3B,IAAI,IAAI,KAAK,MAAM,EAAE;YACnB,OAAO,cAAc,CAAC;SACvB;QACD,OAAO,gBAAgB,EAAE,CAAC;KAC3B;IAED,MAAM,UAAU,MAAM,IAAI,IAAI,oCAAoC,CAAC;AACrE,CAAC;AApBD,kCAoBC;AAED,SAAgB,oBAAoB;IAClC,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;IAEvB,IAAI,wBAAwB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QACxC,QAAQ,MAAM,EAAE;YACd,KAAK,UAAU,CAAC,CAAC;gBACf,IAAI,IAAI,KAAK,MAAM,EAAE;oBACnB,OAAO,mBAAmB,CACxB,sBAAsB,CAAC,GAAG,CAAC,GAAG,UAAU,IAAI,MAAM,EAAE,CAAC,CACtD,CAAC;iBACH;qBAAM,IAAI,IAAI,KAAK,MAAM,EAAE;oBAC1B,OAAO,mBAAmB,CACxB,sBAAsB,CAAC,GAAG,CAAC,GAAG,UAAU,IAAI,MAAM,EAAE,CAAC,CACtD,CAAC;iBACH;gBACD,MAAM;aACP;YACD,KAAK,YAAY,CAAC,CAAC;gBACjB,IAAI,IAAI,KAAK,MAAM,EAAE;oBACnB,OAAO,mBAAmB,CACxB,sBAAsB,CAAC,GAAG,CAAC,GAAG,YAAY,IAAI,MAAM,EAAE,CAAC,CACxD,CAAC;iBACH;qBAAM,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,EAAE;oBAC7C,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;oBAC3C,IAAI,YAAY,KAAK,IAAI,EAAE;wBACzB,OAAO,mBAAmB,CACxB,sBAAsB,CAAC,GAAG,CAAC,GAAG,YAAY,IAAI,YAAY,EAAE,CAAC,CAC9D,CAAC;qBACH;oBACD,MAAM,IAAI,KAAK,CACb,UAAU,MAAM,IAAI,IAAI,iDAAiD,CAC1E,CAAC;iBACH;gBACD,MAAM;aACP;YACD,KAAK,cAAc,CAAC,CAAC;gBACnB,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,EAAE;oBACtC,OAAO,mBAAmB,CACxB,sBAAsB,CAAC,GAAG,CAAC,GAAG,cAAc,IAAI,IAAI,EAAE,CAAC,CACxD,CAAC;iBACH;gBACD,MAAM;aACP;YACD,OAAO,CAAC,CAAC;gBACP,MAAM,IAAI,KAAK,CACb,UAAU,MAAM,IAAI,IAAI,oCAAoC,CAC7D,CAAC;aACH;SACF;KACF;IAED,MAAM,IAAI,KAAK,CACb,UAAU,MAAM,IAAI,IAAI,oCAAoC,CAC7D,CAAC;AACJ,CAAC;AAvDD,oDAuDC"} |
| // | ||
| // Copyright 2021-2022 Picovoice Inc. | ||
| // | ||
| // You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" | ||
| // file accompanying this source. | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations under the License. | ||
| // | ||
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| var PvRecorderStatus; | ||
| (function (PvRecorderStatus) { | ||
| PvRecorderStatus[PvRecorderStatus["SUCCESS"] = 0] = "SUCCESS"; | ||
| PvRecorderStatus[PvRecorderStatus["OUT_OF_MEMORY"] = 1] = "OUT_OF_MEMORY"; | ||
| PvRecorderStatus[PvRecorderStatus["INVALID_ARGUMENT"] = 2] = "INVALID_ARGUMENT"; | ||
| PvRecorderStatus[PvRecorderStatus["INVALID_STATE"] = 3] = "INVALID_STATE"; | ||
| PvRecorderStatus[PvRecorderStatus["BACKEND_ERROR"] = 4] = "BACKEND_ERROR"; | ||
| PvRecorderStatus[PvRecorderStatus["DEVICE_ALREADY_INITIALIZED"] = 5] = "DEVICE_ALREADY_INITIALIZED"; | ||
| PvRecorderStatus[PvRecorderStatus["DEVICE_NOT_INITIALIZED"] = 6] = "DEVICE_NOT_INITIALIZED"; | ||
| PvRecorderStatus[PvRecorderStatus["IO_ERROR"] = 7] = "IO_ERROR"; | ||
| PvRecorderStatus[PvRecorderStatus["RUNTIME_ERROR"] = 8] = "RUNTIME_ERROR"; | ||
| })(PvRecorderStatus || (PvRecorderStatus = {})); | ||
| exports.default = PvRecorderStatus; | ||
| //# sourceMappingURL=pv_recorder_status_t.js.map |
| {"version":3,"file":"pv_recorder_status_t.js","sourceRoot":"","sources":["../src/pv_recorder_status_t.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,qCAAqC;AACrC,EAAE;AACF,qHAAqH;AACrH,iCAAiC;AACjC,EAAE;AACF,sHAAsH;AACtH,qHAAqH;AACrH,6EAA6E;AAC7E,EAAE;AACF,YAAY,CAAC;;AAEb,IAAK,gBAUJ;AAVD,WAAK,gBAAgB;IACnB,6DAAW,CAAA;IACX,yEAAa,CAAA;IACb,+EAAgB,CAAA;IAChB,yEAAa,CAAA;IACb,yEAAa,CAAA;IACb,mGAA0B,CAAA;IAC1B,2FAAsB,CAAA;IACtB,+DAAQ,CAAA;IACR,yEAAa,CAAA;AACf,CAAC,EAVI,gBAAgB,KAAhB,gBAAgB,QAUpB;AAED,kBAAe,gBAAgB,CAAC"} |
| // | ||
| // Copyright 2022-2023 Picovoice Inc. | ||
| // | ||
| // You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" | ||
| // file accompanying this source. | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations under the License. | ||
| // | ||
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const pv_recorder_status_t_1 = require("./pv_recorder_status_t"); | ||
| const errors_1 = require("./errors"); | ||
| const platforms_1 = require("./platforms"); | ||
| /** | ||
| * PvRecorder class for recording audio. | ||
| */ | ||
| class PvRecorder { | ||
| // eslint-disable-next-line | ||
| static _pvRecorder = require((0, platforms_1.getSystemLibraryPath)()); | ||
| _handle; | ||
| _frameLength; | ||
| _sampleRate; | ||
| _version; | ||
| /** | ||
| * PvRecorder constructor. | ||
| * | ||
| * @param frameLength Length of the audio frames to receive per read call. | ||
| * @param deviceIndex The audio device index to use to record audio. A value of (-1) will use machine's default audio device. | ||
| * @param bufferedFramesCount The number of audio frames buffered internally for reading - i.e. internal circular buffer | ||
| * will be of size `frameLength` * `bufferedFramesCount`. If this value is too low, buffer overflows could occur | ||
| * and audio frames could be dropped. A higher value will increase memory usage. | ||
| */ | ||
| constructor(frameLength, deviceIndex = -1, bufferedFramesCount = 50) { | ||
| let pvRecorderHandleAndStatus; | ||
| try { | ||
| pvRecorderHandleAndStatus = PvRecorder._pvRecorder.init(frameLength, deviceIndex, bufferedFramesCount); | ||
| } | ||
| catch (err) { | ||
| (0, errors_1.default)(err.code, err); | ||
| } | ||
| const status = pvRecorderHandleAndStatus.status; | ||
| if (status !== pv_recorder_status_t_1.default.SUCCESS) { | ||
| throw (0, errors_1.default)(status, "PvRecorder failed to initialize."); | ||
| } | ||
| this._handle = pvRecorderHandleAndStatus.handle; | ||
| this._frameLength = frameLength; | ||
| this._sampleRate = PvRecorder._pvRecorder.sample_rate(); | ||
| this._version = PvRecorder._pvRecorder.version(); | ||
| } | ||
| /** | ||
| * @returns Length of the audio frames to receive per read call. | ||
| */ | ||
| get frameLength() { | ||
| return this._frameLength; | ||
| } | ||
| /** | ||
| * @returns Audio sample rate used by PvRecorder. | ||
| */ | ||
| get sampleRate() { | ||
| return this._sampleRate; | ||
| } | ||
| /** | ||
| * @returns the version of the PvRecorder | ||
| */ | ||
| get version() { | ||
| return this._version; | ||
| } | ||
| /** | ||
| * @returns Whether PvRecorder is currently recording audio or not. | ||
| */ | ||
| get isRecording() { | ||
| return PvRecorder._pvRecorder.get_is_recording(this._handle); | ||
| } | ||
| /** | ||
| * Starts recording audio. | ||
| */ | ||
| start() { | ||
| const status = PvRecorder._pvRecorder.start(this._handle); | ||
| if (status !== pv_recorder_status_t_1.default.SUCCESS) { | ||
| throw (0, errors_1.default)(status, "PvRecorder failed to start."); | ||
| } | ||
| } | ||
| /** | ||
| * Stops recording audio. | ||
| */ | ||
| stop() { | ||
| const status = PvRecorder._pvRecorder.stop(this._handle); | ||
| if (status !== pv_recorder_status_t_1.default.SUCCESS) { | ||
| throw (0, errors_1.default)(status, "PvRecorder failed to stop."); | ||
| } | ||
| } | ||
| /** | ||
| * Asynchronous call to read a frame of audio data. | ||
| * | ||
| * @returns {Promise<Int16Array>} Audio data frame. | ||
| */ | ||
| async read() { | ||
| return new Promise((resolve, reject) => { | ||
| setTimeout(() => { | ||
| const pcm = new Int16Array(this._frameLength); | ||
| const status = PvRecorder._pvRecorder.read(this._handle, pcm); | ||
| if (status !== pv_recorder_status_t_1.default.SUCCESS) { | ||
| reject((0, errors_1.default)(status, "PvRecorder failed to read audio data frame.")); | ||
| } | ||
| resolve(pcm); | ||
| }); | ||
| }); | ||
| } | ||
| /** | ||
| * Synchronous call to read a frame of audio data. | ||
| * | ||
| * @returns {Int16Array} Audio data frame. | ||
| */ | ||
| readSync() { | ||
| const pcm = new Int16Array(this._frameLength); | ||
| const status = PvRecorder._pvRecorder.read(this._handle, pcm); | ||
| if (status !== pv_recorder_status_t_1.default.SUCCESS) { | ||
| throw (0, errors_1.default)(status, "PvRecorder failed to read audio data frame."); | ||
| } | ||
| return pcm; | ||
| } | ||
| /** | ||
| * Enable or disable debug logging for PvRecorder. Debug logs will indicate when there are overflows in the internal | ||
| * frame buffer and when an audio source is generating frames of silence. | ||
| * | ||
| * @param isDebugLoggingEnabled Boolean indicating whether the debug logging is enabled or disabled. | ||
| */ | ||
| setDebugLogging(isDebugLoggingEnabled) { | ||
| PvRecorder._pvRecorder.set_debug_logging(this._handle, isDebugLoggingEnabled); | ||
| } | ||
| /** | ||
| * Returns the name of the selected device used to capture audio. | ||
| * | ||
| * @returns {string} Name of the selected audio device. | ||
| */ | ||
| getSelectedDevice() { | ||
| const device = PvRecorder._pvRecorder.get_selected_device(this._handle); | ||
| if ((device === undefined) || (device === null)) { | ||
| throw new Error("Failed to get selected device."); | ||
| } | ||
| return device; | ||
| } | ||
| /** | ||
| * Destructor. Releases resources acquired by PvRecorder. | ||
| */ | ||
| release() { | ||
| PvRecorder._pvRecorder.delete(this._handle); | ||
| } | ||
| /** | ||
| * Helper function to get the list of available audio devices. | ||
| * | ||
| * @returns {Array<string>} An array of the available device names. | ||
| */ | ||
| static getAvailableDevices() { | ||
| const devices = PvRecorder._pvRecorder.get_available_devices(); | ||
| if ((devices === undefined) || (devices === null)) { | ||
| throw new Error("Failed to get audio devices."); | ||
| } | ||
| return devices; | ||
| } | ||
| } | ||
| exports.default = PvRecorder; | ||
| //# sourceMappingURL=pv_recorder.js.map |
| {"version":3,"file":"pv_recorder.js","sourceRoot":"","sources":["../src/pv_recorder.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,qCAAqC;AACrC,EAAE;AACF,qHAAqH;AACrH,iCAAiC;AACjC,EAAE;AACF,sHAAsH;AACtH,qHAAqH;AACrH,6EAA6E;AAC7E,EAAE;AACF,YAAY,CAAC;;AAMb,iEAAsD;AACtD,qCAAmD;AACnD,2CAAmD;AAEnD;;GAEG;AACH,MAAM,UAAU;IACd,2BAA2B;IACnB,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAA,gCAAoB,GAAE,CAAC,CAAC;IAE5C,OAAO,CAAS;IAChB,YAAY,CAAS;IACrB,WAAW,CAAS;IACpB,QAAQ,CAAS;IAElC;;;;;;;;OAQG;IACH,YACE,WAAmB,EACnB,cAAsB,CAAC,CAAC,EACxB,mBAAmB,GAAG,EAAE;QAExB,IAAI,yBAAyB,CAAC;QAC9B,IAAI;YACF,yBAAyB,GAAG,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,mBAAmB,CAAC,CAAC;SACxG;QAAC,OAAO,GAAQ,EAAE;YACjB,IAAA,gBAA2B,EAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;SAC5C;QACD,MAAM,MAAM,GAAG,yBAAyB,CAAC,MAAM,CAAC;QAChD,IAAI,MAAM,KAAK,8BAAgB,CAAC,OAAO,EAAE;YACvC,MAAM,IAAA,gBAA2B,EAAC,MAAM,EAAE,kCAAkC,CAAC,CAAC;SAC/E;QACD,IAAI,CAAC,OAAO,GAAG,yBAAyB,CAAC,MAAM,CAAC;QAChD,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;QAChC,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC;QACxD,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;IACnD,CAAC;IAED;;OAEG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED;;OAEG;IACH,IAAI,WAAW;QACb,OAAO,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED;;OAEG;IACI,KAAK;QACV,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1D,IAAI,MAAM,KAAK,8BAAgB,CAAC,OAAO,EAAE;YACvC,MAAM,IAAA,gBAA2B,EAAC,MAAM,EAAE,6BAA6B,CAAC,CAAC;SAC1E;IACH,CAAC;IAED;;OAEG;IACI,IAAI;QACT,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzD,IAAI,MAAM,KAAK,8BAAgB,CAAC,OAAO,EAAE;YACvC,MAAM,IAAA,gBAA2B,EAAC,MAAM,EAAE,4BAA4B,CAAC,CAAC;SACzE;IACH,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,IAAI;QACf,OAAO,IAAI,OAAO,CAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACjD,UAAU,CAAC,GAAG,EAAE;gBACd,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBAC9D,IAAI,MAAM,KAAK,8BAAgB,CAAC,OAAO,EAAE;oBACvC,MAAM,CAAC,IAAA,gBAA2B,EAAC,MAAM,EAAE,6CAA6C,CAAC,CAAC,CAAC;iBAC5F;gBACD,OAAO,CAAC,GAAG,CAAC,CAAC;YACf,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACI,QAAQ;QACb,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC9D,IAAI,MAAM,KAAK,8BAAgB,CAAC,OAAO,EAAE;YACvC,MAAM,IAAA,gBAA2B,EAAC,MAAM,EAAE,6CAA6C,CAAC,CAAC;SAC1F;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED;;;;;OAKG;IACI,eAAe,CAAC,qBAA8B;QACnD,UAAU,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IAChF,CAAC;IAED;;;;OAIG;IACI,iBAAiB;QACtB,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,EAAE;YAC/C,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;SACnD;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACI,OAAO;QACZ,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,mBAAmB;QAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,WAAW,CAAC,qBAAqB,EAAE,CAAC;QAC/D,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,EAAE;YACjD,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;SACjD;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;;AAGH,kBAAe,UAAU,CAAC"} |
| import PvRecorderStatus from "./pv_recorder_status_t"; | ||
| declare function pvRecorderStatusToException(status: PvRecorderStatus, errorMessage: string): Error; | ||
| export default pvRecorderStatusToException; | ||
| //# sourceMappingURL=errors.d.ts.map |
| {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAYA,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AAWtD,iBAAS,2BAA2B,CAAC,MAAM,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,GAAG,KAAK,CAuB1F;AAED,eAAe,2BAA2B,CAAC"} |
| import PvRecorder from "./pv_recorder"; | ||
| export { PvRecorder }; | ||
| //# sourceMappingURL=index.d.ts.map |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAYA,OAAO,UAAU,MAAM,eAAe,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,CAAC"} |
| export declare function getPlatform(): string; | ||
| export declare function getSystemLibraryPath(): string; | ||
| //# sourceMappingURL=platforms.d.ts.map |
| {"version":3,"file":"platforms.d.ts","sourceRoot":"","sources":["../../src/platforms.ts"],"names":[],"mappings":"AAqIA,wBAAgB,WAAW,IAAI,MAAM,CAoBpC;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAuD7C"} |
| declare enum PvRecorderStatus { | ||
| SUCCESS = 0, | ||
| OUT_OF_MEMORY = 1, | ||
| INVALID_ARGUMENT = 2, | ||
| INVALID_STATE = 3, | ||
| BACKEND_ERROR = 4, | ||
| DEVICE_ALREADY_INITIALIZED = 5, | ||
| DEVICE_NOT_INITIALIZED = 6, | ||
| IO_ERROR = 7, | ||
| RUNTIME_ERROR = 8 | ||
| } | ||
| export default PvRecorderStatus; | ||
| //# sourceMappingURL=pv_recorder_status_t.d.ts.map |
| {"version":3,"file":"pv_recorder_status_t.d.ts","sourceRoot":"","sources":["../../src/pv_recorder_status_t.ts"],"names":[],"mappings":"AAYA,aAAK,gBAAgB;IACnB,OAAO,IAAI;IACX,aAAa,IAAA;IACb,gBAAgB,IAAA;IAChB,aAAa,IAAA;IACb,aAAa,IAAA;IACb,0BAA0B,IAAA;IAC1B,sBAAsB,IAAA;IACtB,QAAQ,IAAA;IACR,aAAa,IAAA;CACd;AAED,eAAe,gBAAgB,CAAC"} |
| /** | ||
| * PvRecorder class for recording audio. | ||
| */ | ||
| declare class PvRecorder { | ||
| private static _pvRecorder; | ||
| private readonly _handle; | ||
| private readonly _frameLength; | ||
| private readonly _sampleRate; | ||
| private readonly _version; | ||
| /** | ||
| * PvRecorder constructor. | ||
| * | ||
| * @param frameLength Length of the audio frames to receive per read call. | ||
| * @param deviceIndex The audio device index to use to record audio. A value of (-1) will use machine's default audio device. | ||
| * @param bufferedFramesCount The number of audio frames buffered internally for reading - i.e. internal circular buffer | ||
| * will be of size `frameLength` * `bufferedFramesCount`. If this value is too low, buffer overflows could occur | ||
| * and audio frames could be dropped. A higher value will increase memory usage. | ||
| */ | ||
| constructor(frameLength: number, deviceIndex?: number, bufferedFramesCount?: number); | ||
| /** | ||
| * @returns Length of the audio frames to receive per read call. | ||
| */ | ||
| get frameLength(): number; | ||
| /** | ||
| * @returns Audio sample rate used by PvRecorder. | ||
| */ | ||
| get sampleRate(): number; | ||
| /** | ||
| * @returns the version of the PvRecorder | ||
| */ | ||
| get version(): string; | ||
| /** | ||
| * @returns Whether PvRecorder is currently recording audio or not. | ||
| */ | ||
| get isRecording(): boolean; | ||
| /** | ||
| * Starts recording audio. | ||
| */ | ||
| start(): void; | ||
| /** | ||
| * Stops recording audio. | ||
| */ | ||
| stop(): void; | ||
| /** | ||
| * Asynchronous call to read a frame of audio data. | ||
| * | ||
| * @returns {Promise<Int16Array>} Audio data frame. | ||
| */ | ||
| read(): Promise<Int16Array>; | ||
| /** | ||
| * Synchronous call to read a frame of audio data. | ||
| * | ||
| * @returns {Int16Array} Audio data frame. | ||
| */ | ||
| readSync(): Int16Array; | ||
| /** | ||
| * Enable or disable debug logging for PvRecorder. Debug logs will indicate when there are overflows in the internal | ||
| * frame buffer and when an audio source is generating frames of silence. | ||
| * | ||
| * @param isDebugLoggingEnabled Boolean indicating whether the debug logging is enabled or disabled. | ||
| */ | ||
| setDebugLogging(isDebugLoggingEnabled: boolean): void; | ||
| /** | ||
| * Returns the name of the selected device used to capture audio. | ||
| * | ||
| * @returns {string} Name of the selected audio device. | ||
| */ | ||
| getSelectedDevice(): string; | ||
| /** | ||
| * Destructor. Releases resources acquired by PvRecorder. | ||
| */ | ||
| release(): void; | ||
| /** | ||
| * Helper function to get the list of available audio devices. | ||
| * | ||
| * @returns {Array<string>} An array of the available device names. | ||
| */ | ||
| static getAvailableDevices(): string[]; | ||
| } | ||
| export default PvRecorder; | ||
| //# sourceMappingURL=pv_recorder.d.ts.map |
| {"version":3,"file":"pv_recorder.d.ts","sourceRoot":"","sources":["../../src/pv_recorder.ts"],"names":[],"mappings":"AAoBA;;GAEG;AACH,cAAM,UAAU;IAEd,OAAO,CAAC,MAAM,CAAC,WAAW,CAAmC;IAE7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC;;;;;;;;OAQG;gBAED,WAAW,EAAE,MAAM,EACnB,WAAW,GAAE,MAAW,EACxB,mBAAmB,SAAK;IAkB1B;;OAEG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;OAEG;IACI,KAAK,IAAI,IAAI;IAOpB;;OAEG;IACI,IAAI,IAAI,IAAI;IAOnB;;;;OAIG;IACU,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IAaxC;;;;OAIG;IACI,QAAQ,IAAI,UAAU;IAS7B;;;;;OAKG;IACI,eAAe,CAAC,qBAAqB,EAAE,OAAO,GAAG,IAAI;IAI5D;;;;OAIG;IACI,iBAAiB,IAAI,MAAM;IAQlC;;OAEG;IACI,OAAO,IAAI,IAAI;IAItB;;;;OAIG;WACW,mBAAmB,IAAI,MAAM,EAAE;CAO9C;AAED,eAAe,UAAU,CAAC"} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| { | ||
| "name": "@picovoice/pvrecorder-node", | ||
| "version": "1.2.8", | ||
| "description": "Audio recorder sdk for Nodejs.", | ||
| "main": "dist/index.js", | ||
| "types": "dist/types", | ||
| "keywords": [ | ||
| "audio, audio recorder" | ||
| ], | ||
| "author": "Picovoice Inc.", | ||
| "license": "Apache-2.0", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/Picovoice/pvrecorder.git", | ||
| "directory": "binding/nodejs" | ||
| }, | ||
| "scripts": { | ||
| "build": "npm-run-all --parallel build:**", | ||
| "build:all": "tsc", | ||
| "build:types": "tsc --declaration --declarationMap --emitDeclarationOnly --outDir ./dist/types", | ||
| "prepack": "npm run build", | ||
| "prepare": "node copy.js", | ||
| "test": "jest --no-cache", | ||
| "lint": "eslint . --ext .js,.ts" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/jest": "^27.4.1", | ||
| "@types/node": "^17.0.21", | ||
| "@typescript-eslint/eslint-plugin": "^5.19.0", | ||
| "@typescript-eslint/parser": "^5.19.0", | ||
| "eslint": "^8.13.0", | ||
| "eslint-plugin-jest": "^27.1.6", | ||
| "jest": "^27.5.1", | ||
| "mkdirp": "^1.0.4", | ||
| "ncp": "^2.0.0", | ||
| "npm-run-all": "^4.1.5", | ||
| "prettier": "^2.6.2", | ||
| "ts-jest": "^27.1.3", | ||
| "typescript": "^4.6.2" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| } | ||
| } |
| # PvRecorder Binding for Node.js | ||
| ## PvRecorder | ||
| PvRecorder is an easy-to-use, cross-platform audio recorder designed for real-time speech audio processing. It allows developers access to an audio device's input stream, broken up into data frames of a given size. | ||
| ## Compatibility | ||
| - Node.js 18+ | ||
| - Runs on Linux (x86_64), macOS (x86_64 and arm64), Windows (x86_64 and arm64), and Raspberry Pi (3, 4, 5). | ||
| ## Installation | ||
| ```console | ||
| yarn add @picovoice/pvrecorder-node | ||
| ``` | ||
| ## Usage | ||
| Initialize and begin recording: | ||
| ```javascript | ||
| const { PvRecorder } = require("@picovoice/pvrecorder-node"); | ||
| const recorder = new PvRecorder(/*frameLength*/ 512); | ||
| recorder.start() | ||
| ``` | ||
| (or) | ||
| Use `get_available_devices()` to get a list of available devices and then initialize the instance based on the index of a device: | ||
| ```javascript | ||
| const { PvRecorder } = require("@picovoice/pvrecorder-node"); | ||
| const devices = PvRecorder.getAvailableDevices() | ||
| const recorder = new PvRecorder(512, /*device index*/0); | ||
| recorder.start() | ||
| ``` | ||
| Read frames of audio: | ||
| ```javascript | ||
| while (recorder.isRecording) { | ||
| // const frame = recorder.readSync(), for synchronous calls | ||
| const frame = await recorder.read(); | ||
| // process audio frame | ||
| } | ||
| ``` | ||
| To stop recording, call `stop()` on the instance: | ||
| ```javascript | ||
| recorder.stop(); | ||
| ``` | ||
| Once you are done, free the resources acquired by PvRecorder. You do not have to call `stop()` before `release()`: | ||
| ```javascript | ||
| recorder.release(); | ||
| ``` | ||
| ## Demos | ||
| [@picovoice/pvrecorder-node-demo](https://www.npmjs.com/package/@picovoice/pvrecorder-node-demo) provides command-line utilities for recording audio to a file. |
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
| import __module from "module"; | ||
| import __path from "path"; | ||
| import __fs from "fs"; | ||
| const __rootRequire = __module.createRequire(import.meta.url); | ||
| const __appPath = __fs.realpathSync(import.meta.dirname); | ||
| const __sharpEntrypoint = __path.join(__appPath, "sharp", "index.js"); | ||
| const __clipboardEntrypoint = __path.join(__appPath, "clipboard", "index.js"); | ||
| const __foundryEntrypoint = __path.join(__appPath, "foundry-local-sdk", "index.js"); | ||
| const __pvRecorderEntrypoint = __path.join(__appPath, "pvrecorder", "index.js"); | ||
| const __sharpRequire = __fs.existsSync(__sharpEntrypoint) | ||
| ? __module.createRequire(__sharpEntrypoint) | ||
| : __rootRequire; | ||
| const __clipboardRequire = __fs.existsSync(__clipboardEntrypoint) | ||
| ? __module.createRequire(__clipboardEntrypoint) | ||
| : __rootRequire; | ||
| const __foundryRequire = __fs.existsSync(__foundryEntrypoint) | ||
| ? __module.createRequire(__foundryEntrypoint) | ||
| : __rootRequire; | ||
| const __pvRecorderRequire = __fs.existsSync(__pvRecorderEntrypoint) | ||
| ? __module.createRequire(__pvRecorderEntrypoint) | ||
| : __rootRequire; | ||
| const __isVendoredNativeModule = (module) => | ||
| typeof module === "string" && | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/") || module === "foundry-local-sdk" || module === "@picovoice/pvrecorder-node"); | ||
| const require = (module) => { | ||
| let req = __rootRequire; | ||
| if (typeof module === "string" && module.startsWith("@img/")) { | ||
| req = __sharpRequire; | ||
| } | ||
| if (typeof module === "string" && module.startsWith("@teddyzhu/")) { | ||
| req = __clipboardRequire; | ||
| } | ||
| if (module === "foundry-local-sdk") { | ||
| req = __foundryRequire; | ||
| } | ||
| if (module === "@picovoice/pvrecorder-node") { | ||
| req = __pvRecorderRequire; | ||
| } | ||
| if (typeof module === "string" && (__module.isBuiltin(module) || __isVendoredNativeModule(module))) { | ||
| return req(module); | ||
| } | ||
| const modulePath = __fs.realpathSync(req.resolve(module)); | ||
| const relativePath = __path.relative(__appPath, modulePath); | ||
| if (relativePath.startsWith("..")) { | ||
| throw new Error("Requiring module outside of application is a security concern; module: " + modulePath + ", app: " + __appPath); | ||
| } | ||
| return req(module); | ||
| };import __url from "url"; | ||
| const __filename = __url.fileURLToPath(import.meta.url); | ||
| const __dirname = __path.dirname(__filename); | ||
| var U=(s=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(s,{get:(e,n)=>(typeof require<"u"?require:e)[n]}):s)(function(s){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+s+'" is not supported')});import{parentPort as v,workerData as re}from"node:worker_threads";var a=class extends Error{constructor(n,t,o){super(n,o);this.code=t;this.name="VoiceBackendError"}},W=16;function S(s){return E(s,new WeakSet,0)}function E(s,e,n){if(n>=W)return{name:"Error",message:"<cause chain truncated>"};if(typeof s=="object"&&s!==null){if(e.has(s))return{name:"Error",message:"<cyclic cause>"};e.add(s)}let t;if(s instanceof a)t={name:s.name,message:s.message,stack:s.stack,code:s.code};else if(s instanceof Error)t={name:s.name,message:s.message,stack:s.stack};else return{name:"Error",message:String(s)};return s instanceof Error&&s.cause!==void 0&&(t.cause=E(s.cause,e,n+1)),t}function L(s){return s instanceof Error?s:new Error(String(s))}function k(s,e){s.on("message",n=>{if(n==null||typeof n!="object")return;let t=n;t.kind==="request"&&j(s,e,t).catch(()=>{})})}async function j(s,e,n){try{let t=n.method,o=n.params,i=await e.call(t,o),r={kind:"response",id:n.id,ok:!0,result:i};s.postMessage(r)}catch(t){let o={kind:"response",id:n.id,ok:!1,error:S(t)};s.postMessage(o)}}var p=class{initialQueue=[];initialQueueResolvers=Promise.withResolvers();logWriter=null;writePromise=this.initialQueueResolvers.promise;setLogWriter(e){this.logWriter=e;for(let n of this.initialQueue)this.writePromise=this.logWriter.writeLog(n.method,n.message);this.initialQueue=[],this.initialQueueResolvers.resolve()}async flush(){await this.writePromise}async dispose(){await this.flush()}outputPath(){return this.logWriter?.outputPath()}logToLevel(e,n){this.logWriter?this.writePromise=this.logWriter.writeLog(e,n):this.initialQueue.push({method:e,message:n})}info(e){this.logToLevel("info",e)}debug(e){this.logToLevel("debug",e)}warning(e){this.logToLevel("warning",e)}error(e){this.logToLevel("error",e instanceof Error?e.message:e)}log(e){this.error(e)}isDebug(){return!1}shouldLog(e){return!0}notice(e){this.info(e instanceof Error?e.message:e)}startGroup(e,n){this.info(`--- Start of group: ${e} ---`)}endGroup(e){this.info("--- End of group ---")}},P=new p;var f=16*1024,m=class{constructor(e){this.port=e}writeLog(e,n){let t={kind:"log",level:e,message:H(n)};try{this.port.postMessage(t)}catch{}return Promise.resolve()}outputPath(){return"<voice-worker>"}};function I(s,e=P){e.setLogWriter(new m(s))}function H(s){return s.length<=f?s:`${s.slice(0,f)}\u2026 [truncated, ${s.length-f} more chars]`}import*as _ from"node:fs/promises";import*as C from"node:os";import*as G from"node:path";function $(s){if(s.includes("<!DOCTYPE")||s.includes("<html")){let e=Math.min(s.indexOf("<!DOCTYPE")!==-1?s.indexOf("<!DOCTYPE"):1/0,s.indexOf("<html")!==-1?s.indexOf("<html"):1/0),n=s.substring(0,e).trim();return n?`${n} [HTML error page omitted]`:"[HTML error page omitted]"}return s}function T(s){let e;if(s instanceof Error)e=String(s);else if(typeof s=="object"&&s!==null)try{e=JSON.stringify(s)??"[object]"}catch{return"[object with circular reference]"}else e=String(s);return $(e)}var u=class{listeners=new Map;on(e,n){let t=this.listeners.get(e);t||(t=new Set,this.listeners.set(e,t));let o=n,i=t;return i.add(o),()=>{i.delete(o)}}emit(e,n){let t=this.listeners.get(e);if(!t)return;let o=[...t];for(let i of o)try{i(n)}catch{}}clear(){this.listeners.clear()}};var z={listModels:!0,downloadModel:!0,deleteModel:!0,loadModel:!0,openSession:!0,appendSession:!0,stopSession:!0,cancelSession:!0,shutdown:!0},me=Object.keys(z),b="automatic-speech-recognition",Y={modelDownloadProgress:!0,sessionPreview:!0},ye=Object.keys(Y);import*as R from"node:fs/promises";import{open as Q}from"node:fs/promises";var x=16e3,y=1,w=16,A=44,g=class{constructor(e){this.path=e}handle;dataSize=0;writeTail=Promise.resolve();async open(){this.handle=await Q(this.path,"w"),await this.handle.write(F(0))}append(e){let n=this.writeTail.then(async()=>{if(!this.handle)throw new Error("WavStreamWriter not opened");await this.handle.write(e),this.dataSize+=e.byteLength});return this.writeTail=n.catch(()=>{}),n}async finalize(){if(!this.handle||(await this.writeTail,!this.handle))return;let e=this.handle;this.handle=void 0;try{await e.write(F(this.dataSize),0,A,0)}finally{await e.close()}}async discard(){this.handle&&(await this.handle.close().catch(()=>{}),this.handle=void 0),await R.unlink(this.path).catch(()=>{})}};function F(s){let e=x*y*w/8,n=y*w/8,t=Buffer.alloc(A);return t.write("RIFF",0,"ascii"),t.writeUInt32LE(36+s,4),t.write("WAVE",8,"ascii"),t.write("fmt ",12,"ascii"),t.writeUInt32LE(16,16),t.writeUInt16LE(1,20),t.writeUInt16LE(y,22),t.writeUInt32LE(x,24),t.writeUInt32LE(e,28),t.writeUInt16LE(n,32),t.writeUInt16LE(w,34),t.write("data",36,"ascii"),t.writeUInt32LE(s,40),t}var q="github-copilot-cli",J={AzureCatalogFilter:"'',test"},K=1e4,X=2e3,Z="voice-foundry-batch-";function ee(s){switch(s.tag){case"opening":return;case"active":case"stopping":case"cancelling":return s.session;default:return s}}var h=class{managerPromise;state={tag:"unloaded"};lastModelGeneration=0;downloads=new Map;shutdownPromise;events=new u;foundryAdditionalSettings;nativeLocation;tempDir;managerFactory;handlers={listModels:()=>this.handleListModels(),downloadModel:e=>this.handleDownloadModel(e),deleteModel:e=>this.handleDeleteModel(e),loadModel:e=>this.handleLoadModel(e),openSession:e=>this.handleOpenSession(e),appendSession:e=>this.handleAppendSession(e),stopSession:e=>this.handleStopSession(e),cancelSession:e=>this.handleCancelSession(e),shutdown:()=>this.handleShutdown()};constructor(e={}){this.foundryAdditionalSettings=e.foundryAdditionalSettings??J,this.nativeLocation=e.nativeLocation,this.tempDir=e.tempDir??C.tmpdir(),this.managerFactory=e.managerFactory}call(e,n,t){if(this.shutdownPromise&&e!=="shutdown")return Promise.reject(this.disposedError());let o=this.handlers[e];return o(n)}on(e,n){return this.events.on(e,n)}onFatalError(e){return()=>{}}shutdown(e){return this.handleShutdown()}async handleListModels(){return O(async()=>{let n=await(await this.getManager()).catalog.getModels(),t=[];for(let o of n)for(let i of o.variants)se(i)&&t.push(await ie(i));return t})}async handleDownloadModel(e){let{variantId:n,downloadId:t}=e,o=this.downloads.get(n);if(o){if(o.downloadId===t)return o.promise;throw new a(`A different download for '${n}' is in flight.`,"io")}let i=(async()=>{await(await this.getVariant(n)).download(d=>{this.events.emit("modelDownloadProgress",{downloadId:t,variantId:n,percent:d})})})(),r={downloadId:t,promise:i};this.downloads.set(n,r);try{await i}finally{this.downloads.get(n)===r&&this.downloads.delete(n)}}async handleDeleteModel(e){let{variantId:n}=e;if(this.downloads.has(n))throw new a(`Cannot delete '${n}' while a download is in flight.`,"io");let t=this.state;switch(t.tag){case"unloaded":break;case"loading":t.selection.variantId===n&&await this.unloadSelected();break;case"ready":if(t.selection.variantId===n){if(t.session!==void 0)throw new a(`Cannot delete '${n}' while a session is active.`,"session-active");await this.unloadSelected()}break;default:}(await this.getVariant(n)).removeFromCache()}async handleLoadModel(e){let{variantId:n}=e;if(this.shutdownPromise)throw this.disposedError();let t=this.state;switch(t.tag){case"unloaded":return this.startLoad(n,void 0);case"loading":if(t.selection.variantId===n){let o=await t.selection.loadedModel;return o.modelGeneration=this.bumpModelGeneration(),{modelGeneration:o.modelGeneration}}return this.startLoad(n,t.selection);case"ready":if(t.session!==void 0&&t.selection.variantId!==n)throw new a(`Cannot load '${n}' while a session is active.`,"session-active");return t.selection.variantId===n?(t.selection.loaded.modelGeneration=this.bumpModelGeneration(),{modelGeneration:t.selection.loaded.modelGeneration}):this.startLoad(n,t.selection);default:return t}}async startLoad(e,n){let t=this.loadVariant(e,n),o={variantId:e,loadedModel:t,previous:n};return this.state={tag:"loading",selection:o},t.then(r=>{this.state.tag==="loading"&&this.state.selection===o&&(this.state={tag:"ready",selection:{variantId:e,loadedModel:t,loaded:r}})},()=>{this.state.tag==="loading"&&this.state.selection===o&&(this.state={tag:"unloaded"})}),{modelGeneration:(await t).modelGeneration}}async loadVariant(e,n){if(n){let o=await n.loadedModel.catch(()=>{});o&&await o.variant.unload().catch(()=>{})}if(this.shutdownPromise)throw this.disposedError();let t=await this.getVariant(e);if(!t.isCached)throw new a(`Voice model '${e}' is not downloaded.`,"model-not-downloaded");if(await t.load(),this.shutdownPromise)throw await t.unload().catch(()=>{}),this.disposedError();return{variant:t,isStreaming:ne(t.alias),modelGeneration:this.bumpModelGeneration()}}bumpModelGeneration(){return this.lastModelGeneration=this.lastModelGeneration+1,this.lastModelGeneration}async unloadSelected(){if(this.state.tag==="unloaded")return;let e=this.state.selection;this.state={tag:"unloaded"};let n=await e.loadedModel.catch(()=>{});n&&await n.variant.unload().catch(()=>{})}async handleOpenSession(e){let n=this.state;switch(n.tag){case"unloaded":throw new a("Loaded model has changed since this session was prepared.","stale-model");case"loading":return await n.selection.loadedModel.catch(()=>{}),this.handleOpenSession(e);case"ready":{let t=n,{sessionId:o,modelGeneration:i}=e;if(t.session!==void 0)throw new a("A voice session is already active.","session-active");if(t.selection.loaded.modelGeneration!==i)throw new a("Loaded model has changed since this session was prepared.","stale-model");let r=t.selection.loaded,c=(async()=>{try{let d=r.isStreaming?await this.openStreamingSession(r,o):await this.openBatchSession(o),l=t.session;this.state===t&&l?.tag==="opening"&&l.sessionId===o?t.session={tag:"active",session:d}:await this.teardownSession(d).catch(()=>{})}catch(d){let l=t.session;throw this.state===t&&l?.tag==="opening"&&l.sessionId===o&&(t.session=void 0),d}})();t.session={tag:"opening",sessionId:o,opening:c},await c;return}default:return n}}async handleAppendSession(e){let n=this.state;switch(n.tag){case"unloaded":case"loading":return;case"ready":{let t=n.session;if(t===void 0)return;switch(t.tag){case"opening":case"stopping":case"cancelling":return;case"active":{if(t.session.sessionId!==e.sessionId)return;let o=t.session;if(o.kind==="streaming"){await o.foundrySdkSession.append(e.pcm);return}await o.wav.append(e.pcm);return}default:return t}}default:return n}}async handleStopSession(e){let n=()=>{throw new a(`No session '${e.sessionId}'.`,"session-not-found")},t=this.state;switch(t.tag){case"unloaded":case"loading":return n();case"ready":{let o=t,i=o.session;if(i===void 0)return n();switch(i.tag){case"opening":case"stopping":case"cancelling":return n();case"active":{if(i.session.sessionId!==e.sessionId)return n();let r=i.session,c=r.error;if(c)throw o.session=void 0,await this.teardownSession(r).catch(()=>{}),c;o.session={tag:"stopping",session:r};try{return{text:r.kind==="streaming"?await this.stopStreaming(r):await this.stopBatch(r)}}catch(d){throw r.error?r.error:d}finally{let d=o.session;this.state===o&&d?.tag==="stopping"&&d.session===r&&(o.session=void 0),await this.teardownSession(r).catch(()=>{})}}default:return i}}default:return t}}async handleCancelSession(e){let n=this.state;switch(n.tag){case"unloaded":case"loading":return;case"ready":{let t=n,o=t.session;if(o===void 0)return;switch(o.tag){case"opening":case"cancelling":return;case"active":{if(o.session.sessionId!==e.sessionId)return;let i=o.session;t.session={tag:"cancelling",session:i};try{await this.teardownSession(i)}finally{let r=t.session;this.state===t&&r?.tag==="cancelling"&&r.session===i&&(t.session=void 0)}return}case"stopping":{if(o.session.sessionId!==e.sessionId)return;let i=o.session;i.error=i.error??new a("Session cancelled.","cancelled"),t.session={tag:"cancelling",session:i},await this.teardownSession(i).catch(()=>{});return}default:return o}}default:return n}}async openStreamingSession(e,n){let t=e.variant.createAudioClient().createLiveTranscriptionSession();try{await t.start()}catch(i){throw await this.disposeSdk(t),i}let o={kind:"streaming",sessionId:n,foundrySdkSession:t,committed:"",tail:"",drainTask:Promise.resolve("")};return o.drainTask=this.runStreamingDrain(o),o.drainTask.catch(()=>{}),o}async openBatchSession(e){let n=new g(G.join(this.tempDir,`${Z}${e}.wav`));try{await n.open()}catch(t){throw await n.discard(),t}return{kind:"batch",sessionId:e,wav:n}}async runStreamingDrain(e){try{for await(let n of e.foundrySdkSession.getTranscriptionStream()){if(this.isCancellingSession(e))break;let t=te(n);t&&(n.is_final?(e.committed+=t,e.tail=""):e.tail+=t,this.events.emit("sessionPreview",{sessionId:e.sessionId,text:e.committed+e.tail}))}return e.committed+e.tail}catch(n){let t=L(n);throw e.error=t,t}}isCancellingSession(e){let n=this.state;switch(n.tag){case"unloaded":case"loading":return!1;case"ready":{let t=n.session;if(t===void 0)return!1;switch(t.tag){case"opening":case"active":case"stopping":return!1;case"cancelling":return t.session===e;default:return t}}default:return n}}async stopStreaming(e){return D((async()=>(await e.foundrySdkSession.stop(),e.drainTask))(),K,"session-timeout","Streaming session drain timed out.")}async stopBatch(e){if(this.state.tag!=="ready")return"";let n=this.state.selection.loaded;await e.wav.finalize();try{return(await n.variant.createAudioClient().transcribe(e.wav.path)).text??""}finally{await _.unlink(e.wav.path).catch(()=>{})}}async teardownSession(e){e.kind==="streaming"?await this.disposeSdk(e.foundrySdkSession):await e.wav.discard()}async disposeSdk(e){await D(e.dispose(),X,"io","SDK dispose timed out.").catch(()=>{})}async handleShutdown(){return this.shutdownPromise?this.shutdownPromise:(this.shutdownPromise=(async()=>{if(this.state.tag==="ready"&&this.state.session?.tag==="opening"&&await this.state.session.opening.catch(()=>{}),this.state.tag==="ready"){let e=this.state,n=e.session;if(n!==void 0){let t=ee(n);t&&(e.session=void 0,await this.teardownSession(t).catch(()=>{}))}}await this.unloadSelected().catch(()=>{}),this.managerPromise=void 0,this.events.clear()})(),this.shutdownPromise)}disposedError(){return new a("Foundry backend has been shut down.","disposed")}async getVariant(e){return(await this.getManager()).catalog.getModelVariant(e)}getManager(){return this.managerPromise||(this.managerPromise=this.initManager().catch(e=>{throw this.managerPromise=void 0,e})),this.managerPromise}async initManager(){if(this.managerFactory)return this.managerFactory();if(!this.nativeLocation)throw new a("Voice runtime is not downloaded. RuntimeInstaller must resolve it before backend construction.","runtime-not-downloaded");let e=this.nativeLocation;return O(async()=>{let n=U("foundry-local-sdk"),t={...this.foundryAdditionalSettings,FoundryLocalCorePath:e.corePath};return e.needsBootstrap&&!("Bootstrap"in t)&&(t.Bootstrap="true"),n.FoundryLocalManager.create({appName:q,additionalSettings:t})})}};function te(s){return s.content?.[0]?.text??""}function ne(s){return s.toLowerCase().includes("streaming")}function se(s){return s.info.task===b}async function O(s,e){try{return await s()}catch(n){if((e?.platform??process.platform)!=="win32")throw n;let o=T(n);if(!/Failed to load (?:dependency|core) library/i.test(o))throw n;let r=(e?.arch??process.arch)==="arm64"?"https://aka.ms/vs/17/release/vc_redist.arm64.exe":"https://aka.ms/vs/17/release/vc_redist.x64.exe";throw new a(`Voice mode requires the Microsoft Visual C++ Redistributable (2015-2022). Download and install it, then try again: ${r}`,"windows-runtime-missing",{cause:n})}}function oe(s){return Array.isArray(s)?s.filter(e=>typeof e=="string"):s&&typeof s=="object"?Object.keys(s):[]}async function ie(s){let e=s.isCached,n=oe(s.info.capabilities),t=s.info,o=t.runtime?.deviceType??t.device,i=o?o.toLowerCase():void 0,r=t.sizeInBytes??(typeof t.fileSizeMb=="number"?Math.round(t.fileSizeMb*1024*1024):void 0);return{id:s.id,alias:s.alias,name:s.info.name??s.id,cached:e,capabilities:n,sizeBytes:r,device:i}}function D(s,e,n,t){let o,i=new Promise((r,c)=>{o=setTimeout(()=>c(new a(t,n)),e)});return Promise.race([s,i]).finally(()=>{o&&clearTimeout(o)})}if(!v)throw new Error("voice-foundry.worker.js must be loaded as a worker thread.");I(v);var N=re;if(!N?.nativeLocation)throw new Error("voice-foundry.worker.ts requires workerData.nativeLocation.");var B=v,M=new h({nativeLocation:N.nativeLocation});M.on("modelDownloadProgress",s=>V("modelDownloadProgress",s));M.on("sessionPreview",s=>V("sessionPreview",s));k(B,M);function V(s,e){let n={kind:"event",event:s,payload:e};B.postMessage(n)} |
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
| import __module from "module"; | ||
| import __path from "path"; | ||
| import __fs from "fs"; | ||
| const __rootRequire = __module.createRequire(import.meta.url); | ||
| const __appPath = __fs.realpathSync(import.meta.dirname); | ||
| const __sharpEntrypoint = __path.join(__appPath, "sharp", "index.js"); | ||
| const __clipboardEntrypoint = __path.join(__appPath, "clipboard", "index.js"); | ||
| const __foundryEntrypoint = __path.join(__appPath, "foundry-local-sdk", "index.js"); | ||
| const __pvRecorderEntrypoint = __path.join(__appPath, "pvrecorder", "index.js"); | ||
| const __sharpRequire = __fs.existsSync(__sharpEntrypoint) | ||
| ? __module.createRequire(__sharpEntrypoint) | ||
| : __rootRequire; | ||
| const __clipboardRequire = __fs.existsSync(__clipboardEntrypoint) | ||
| ? __module.createRequire(__clipboardEntrypoint) | ||
| : __rootRequire; | ||
| const __foundryRequire = __fs.existsSync(__foundryEntrypoint) | ||
| ? __module.createRequire(__foundryEntrypoint) | ||
| : __rootRequire; | ||
| const __pvRecorderRequire = __fs.existsSync(__pvRecorderEntrypoint) | ||
| ? __module.createRequire(__pvRecorderEntrypoint) | ||
| : __rootRequire; | ||
| const __isVendoredNativeModule = (module) => | ||
| typeof module === "string" && | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/") || module === "foundry-local-sdk" || module === "@picovoice/pvrecorder-node"); | ||
| const require = (module) => { | ||
| let req = __rootRequire; | ||
| if (typeof module === "string" && module.startsWith("@img/")) { | ||
| req = __sharpRequire; | ||
| } | ||
| if (typeof module === "string" && module.startsWith("@teddyzhu/")) { | ||
| req = __clipboardRequire; | ||
| } | ||
| if (module === "foundry-local-sdk") { | ||
| req = __foundryRequire; | ||
| } | ||
| if (module === "@picovoice/pvrecorder-node") { | ||
| req = __pvRecorderRequire; | ||
| } | ||
| if (typeof module === "string" && (__module.isBuiltin(module) || __isVendoredNativeModule(module))) { | ||
| return req(module); | ||
| } | ||
| const modulePath = __fs.realpathSync(req.resolve(module)); | ||
| const relativePath = __path.relative(__appPath, modulePath); | ||
| if (relativePath.startsWith("..")) { | ||
| throw new Error("Requiring module outside of application is a security concern; module: " + modulePath + ", app: " + __appPath); | ||
| } | ||
| return req(module); | ||
| };import __url from "url"; | ||
| const __filename = __url.fileURLToPath(import.meta.url); | ||
| const __dirname = __path.dirname(__filename); | ||
| import{parentPort as $,workerData as ee}from"node:worker_threads";import{createRequire as W}from"node:module";import{existsSync as H}from"node:fs";import*as o from"node:fs/promises";import*as a from"node:path";import{createHash as U}from"node:crypto";import{join as u,basename as oe}from"node:path";import{homedir as g}from"node:os";function j(){return process.env.XDG_CACHE_HOME||u(g(),".cache")}function k(){if(process.platform==="darwin")return u(g(),"Library","Caches","copilot");if(process.platform==="win32"){let e=process.env.LOCALAPPDATA||u(g(),".cache");return u(e,"copilot")}return u(j(),"copilot")}function A(e){if(e.includes("<!DOCTYPE")||e.includes("<html")){let r=Math.min(e.indexOf("<!DOCTYPE")!==-1?e.indexOf("<!DOCTYPE"):1/0,e.indexOf("<html")!==-1?e.indexOf("<html"):1/0),t=e.substring(0,r).trim();return t?`${t} [HTML error page omitted]`:"[HTML error page omitted]"}return e}function m(e){let r;if(e instanceof Error)r=String(e);else if(typeof e=="object"&&e!==null)try{r=JSON.stringify(e)??"[object]"}catch{return"[object with circular reference]"}else r=String(e);return A(r)}var J=1,b=".complete";var h={"win32-x64":"win-x64","win32-arm64":"win-arm64","linux-x64":"linux-x64","darwin-arm64":"osx-arm64"};function S(){return typeof __foundryRequire<"u"&&__foundryRequire||W(import.meta.url)}var d;function I(){if(d)return d;try{let e=S()("foundry-local-sdk/script/install-utils.cjs");if(typeof e.NUGET_FEED!="string"||typeof e.ORT_NIGHTLY_FEED!="string"||typeof e.runInstall!="function")throw new Error(`Expected exports {NUGET_FEED: string, ORT_NIGHTLY_FEED: string, runInstall: function}, got: ${JSON.stringify(Object.fromEntries(Object.entries(e).map(([r,t])=>[r,typeof t])))}`);return d=e,d}catch(e){throw new Error(`Failed to load foundry-local-sdk/script/install-utils.cjs: ${m(e)}. The upstream foundry-local-sdk installer may have changed shape \u2014 re-run the audit checklist in src/cli/voice/foundry/installer/nativeLoader.ts and update accordingly.`)}}var f;function G(){if(f)return f;try{let e=S()("foundry-local-sdk/deps_versions.json");if(typeof e["foundry-local-core"]?.nuget!="string"||typeof e.onnxruntime?.version!="string"||typeof e["onnxruntime-genai"]?.version!="string")throw new Error('deps_versions.json is missing one of the expected version keys: ["foundry-local-core"].nuget, .onnxruntime.version, ["onnxruntime-genai"].version');return f=e,f}catch(e){throw new Error(`Failed to load foundry-local-sdk/deps_versions.json: ${m(e)}. The upstream foundry-local-sdk installer may have changed shape \u2014 re-run the audit checklist in src/cli/voice/foundry/installer/nativeLoader.ts and update accordingly.`)}}function O(e=process.platform){let r=I(),t=G();return[{name:"Microsoft.AI.Foundry.Local.Core",version:t["foundry-local-core"].nuget,feed:r.ORT_NIGHTLY_FEED},{name:e==="linux"?"Microsoft.ML.OnnxRuntime.Gpu.Linux":"Microsoft.ML.OnnxRuntime.Foundry",version:t.onnxruntime.version,feed:r.NUGET_FEED},{name:"Microsoft.ML.OnnxRuntimeGenAI.Foundry",version:t["onnxruntime-genai"].version,feed:r.NUGET_FEED}]}function _(e){return e==="win32"?".dll":e==="darwin"?".dylib":".so"}function V(e,r){return a.join(e,`Microsoft.AI.Foundry.Local.Core${_(r)}`)}function q(e){let r=_(e),t=e==="win32"?"":"lib";return[`Microsoft.AI.Foundry.Local.Core${r}`,`${t}onnxruntime${r}`,`${t}onnxruntime-genai${r}`]}function Y(e,r=process.platform,t=process.arch){let n=h[`${r}-${t}`];if(!n)throw new Error(`Voice mode not supported on ${r}-${t}`);let i=e??process.env.COPILOT_CACHE_HOME??k(),s=O(r),c=U("sha256").update(JSON.stringify({schema:J,artifacts:s})).digest("hex").slice(0,12);return a.join(i,"foundry",c,n)}async function C(e={}){let r=e.platform??process.platform,t=e.arch??process.arch,n=`${r}-${t}`;if(!h[n])throw new Error(`Voice mode is not supported on ${n}. Supported platforms: ${Object.keys(h).join(", ")}.`);let s=Y(e.cacheRoot,r,t),c=V(s,r),l=q(r);return await N(s,l)||(e.onDownloadStart?.(),await z(s,r,l,e.runInstall)),P(c,s,r,e.existsSyncImpl)}async function N(e,r){return await y(a.join(e,b))?(await Promise.all(r.map(n=>y(a.join(e,n))))).every(Boolean):!1}function P(e,r,t,n=K){if(t!=="win32")return{corePath:e,needsBootstrap:!1};let i=a.join(r,"Microsoft.WindowsAppRuntime.Bootstrap.dll");return{corePath:e,needsBootstrap:n(i)}}function K(e){try{return H(e)}catch{return!1}}async function y(e){try{return await o.access(e),!0}catch{return!1}}async function z(e,r,t,n){let i=a.dirname(e);await o.mkdir(i,{recursive:!0});let s=a.join(i,`.tmp-${a.basename(e)}-${process.pid}-${Date.now()}`);await o.mkdir(s,{recursive:!0});try{let c=n??I().runInstall,l=O(r);await B(()=>c(l,{binDir:s}));for(let L of t)if(!await y(a.join(s,L)))throw new Error(`Foundry runtime download finished but required file is missing: ${L}. RID for ${r} may not be supported by the published packages.`);await o.writeFile(a.join(s,b),""),await Q(s,e,t)}catch(c){throw await o.rm(s,{recursive:!0,force:!0}).catch(()=>{}),c}}async function Q(e,r,t){try{await o.rename(e,r)}catch(n){let i=n.code;if(i==="ENOTEMPTY"||i==="EEXIST"||i==="EPERM"){if(await N(r,t)){await o.rm(e,{recursive:!0,force:!0}).catch(()=>{});return}await o.rm(r,{recursive:!0,force:!0}),await o.rename(e,r);return}throw n}}async function B(e){let r=process.stdout.write.bind(process.stdout),t=process.stderr.write.bind(process.stderr);process.stdout.write=(()=>!0),process.stderr.write=(()=>!0);try{return await e()}finally{process.stdout.write=r,process.stderr.write=t}}var E=class extends Error{constructor(t,n,i){super(t,i);this.code=n;this.name="VoiceBackendError"}},X=16;function T(e){return F(e,new WeakSet,0)}function F(e,r,t){if(t>=X)return{name:"Error",message:"<cause chain truncated>"};if(typeof e=="object"&&e!==null){if(r.has(e))return{name:"Error",message:"<cyclic cause>"};r.add(e)}let n;if(e instanceof E)n={name:e.name,message:e.message,stack:e.stack,code:e.code};else if(e instanceof Error)n={name:e.name,message:e.message,stack:e.stack};else return{name:"Error",message:String(e)};return e instanceof Error&&e.cause!==void 0&&(n.cause=F(e.cause,r,t+1)),n}function M(e){return e instanceof Error?e:new Error(String(e))}var w=class{initialQueue=[];initialQueueResolvers=Promise.withResolvers();logWriter=null;writePromise=this.initialQueueResolvers.promise;setLogWriter(r){this.logWriter=r;for(let t of this.initialQueue)this.writePromise=this.logWriter.writeLog(t.method,t.message);this.initialQueue=[],this.initialQueueResolvers.resolve()}async flush(){await this.writePromise}async dispose(){await this.flush()}outputPath(){return this.logWriter?.outputPath()}logToLevel(r,t){this.logWriter?this.writePromise=this.logWriter.writeLog(r,t):this.initialQueue.push({method:r,message:t})}info(r){this.logToLevel("info",r)}debug(r){this.logToLevel("debug",r)}warning(r){this.logToLevel("warning",r)}error(r){this.logToLevel("error",r instanceof Error?r.message:r)}log(r){this.error(r)}isDebug(){return!1}shouldLog(r){return!0}notice(r){this.info(r instanceof Error?r.message:r)}startGroup(r,t){this.info(`--- Start of group: ${r} ---`)}endGroup(r){this.info("--- End of group ---")}},R=new w;var v=16*1024,x=class{constructor(r){this.port=r}writeLog(r,t){let n={kind:"log",level:r,message:Z(t)};try{this.port.postMessage(n)}catch{}return Promise.resolve()}outputPath(){return"<voice-worker>"}};function D(e,r=R){r.setLogWriter(new x(e))}function Z(e){return e.length<=v?e:`${e.slice(0,v)}\u2026 [truncated, ${e.length-v} more chars]`}if(!$)throw new Error("voice-installer.worker.js must be loaded as a worker thread.");var p=$;D(p);var re=ee??{};async function te(){try{let r={kind:"ok",location:await C({cacheRoot:re.cacheRoot,onDownloadStart:()=>{let t={kind:"download-started"};p.postMessage(t)}})};p.postMessage(r)}catch(e){let r={kind:"error",error:T(M(e))};p.postMessage(r)}finally{setImmediate(()=>process.exit(0))}}te().catch(()=>{process.exit(1)}); |
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
| import __module from "module"; | ||
| import __path from "path"; | ||
| import __fs from "fs"; | ||
| const __rootRequire = __module.createRequire(import.meta.url); | ||
| const __appPath = __fs.realpathSync(import.meta.dirname); | ||
| const __sharpEntrypoint = __path.join(__appPath, "sharp", "index.js"); | ||
| const __clipboardEntrypoint = __path.join(__appPath, "clipboard", "index.js"); | ||
| const __foundryEntrypoint = __path.join(__appPath, "foundry-local-sdk", "index.js"); | ||
| const __pvRecorderEntrypoint = __path.join(__appPath, "pvrecorder", "index.js"); | ||
| const __sharpRequire = __fs.existsSync(__sharpEntrypoint) | ||
| ? __module.createRequire(__sharpEntrypoint) | ||
| : __rootRequire; | ||
| const __clipboardRequire = __fs.existsSync(__clipboardEntrypoint) | ||
| ? __module.createRequire(__clipboardEntrypoint) | ||
| : __rootRequire; | ||
| const __foundryRequire = __fs.existsSync(__foundryEntrypoint) | ||
| ? __module.createRequire(__foundryEntrypoint) | ||
| : __rootRequire; | ||
| const __pvRecorderRequire = __fs.existsSync(__pvRecorderEntrypoint) | ||
| ? __module.createRequire(__pvRecorderEntrypoint) | ||
| : __rootRequire; | ||
| const __isVendoredNativeModule = (module) => | ||
| typeof module === "string" && | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/") || module === "foundry-local-sdk" || module === "@picovoice/pvrecorder-node"); | ||
| const require = (module) => { | ||
| let req = __rootRequire; | ||
| if (typeof module === "string" && module.startsWith("@img/")) { | ||
| req = __sharpRequire; | ||
| } | ||
| if (typeof module === "string" && module.startsWith("@teddyzhu/")) { | ||
| req = __clipboardRequire; | ||
| } | ||
| if (module === "foundry-local-sdk") { | ||
| req = __foundryRequire; | ||
| } | ||
| if (module === "@picovoice/pvrecorder-node") { | ||
| req = __pvRecorderRequire; | ||
| } | ||
| if (typeof module === "string" && (__module.isBuiltin(module) || __isVendoredNativeModule(module))) { | ||
| return req(module); | ||
| } | ||
| const modulePath = __fs.realpathSync(req.resolve(module)); | ||
| const relativePath = __path.relative(__appPath, modulePath); | ||
| if (relativePath.startsWith("..")) { | ||
| throw new Error("Requiring module outside of application is a security concern; module: " + modulePath + ", app: " + __appPath); | ||
| } | ||
| return req(module); | ||
| };import __url from "url"; | ||
| const __filename = __url.fileURLToPath(import.meta.url); | ||
| const __dirname = __path.dirname(__filename); | ||
| var L=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});import{parentPort as m}from"node:worker_threads";var a=class extends Error{constructor(r,o,n){super(r,n);this.code=o;this.name="VoiceBackendError"}},x=16;function d(t){return E(t,new WeakSet,0)}function E(t,e,r){if(r>=x)return{name:"Error",message:"<cause chain truncated>"};if(typeof t=="object"&&t!==null){if(e.has(t))return{name:"Error",message:"<cyclic cause>"};e.add(t)}let o;if(t instanceof a)o={name:t.name,message:t.message,stack:t.stack,code:t.code};else if(t instanceof Error)o={name:t.name,message:t.message,stack:t.stack};else return{name:"Error",message:String(t)};return t instanceof Error&&t.cause!==void 0&&(o.cause=E(t.cause,e,r+1)),o}function w(t){return t instanceof Error?t:new Error(String(t))}var p=class{initialQueue=[];initialQueueResolvers=Promise.withResolvers();logWriter=null;writePromise=this.initialQueueResolvers.promise;setLogWriter(e){this.logWriter=e;for(let r of this.initialQueue)this.writePromise=this.logWriter.writeLog(r.method,r.message);this.initialQueue=[],this.initialQueueResolvers.resolve()}async flush(){await this.writePromise}async dispose(){await this.flush()}outputPath(){return this.logWriter?.outputPath()}logToLevel(e,r){this.logWriter?this.writePromise=this.logWriter.writeLog(e,r):this.initialQueue.push({method:e,message:r})}info(e){this.logToLevel("info",e)}debug(e){this.logToLevel("debug",e)}warning(e){this.logToLevel("warning",e)}error(e){this.logToLevel("error",e instanceof Error?e.message:e)}log(e){this.error(e)}isDebug(){return!1}shouldLog(e){return!0}notice(e){this.info(e instanceof Error?e.message:e)}startGroup(e,r){this.info(`--- Start of group: ${e} ---`)}endGroup(e){this.info("--- End of group ---")}},k=new p;var g=16*1024,f=class{constructor(e){this.port=e}writeLog(e,r){let o={kind:"log",level:e,message:T(r)};try{this.port.postMessage(o)}catch{}return Promise.resolve()}outputPath(){return"<voice-worker>"}};function b(t,e=k){e.setLogWriter(new f(t))}function T(t){return t.length<=g?t:`${t.slice(0,g)}\u2026 [truncated, ${t.length-g} more chars]`}function P(t,e){t.on("message",r=>{if(r==null||typeof r!="object")return;let o=r;o.kind==="request"&&R(t,e,o).catch(()=>{})})}async function R(t,e,r){try{let o=r.method,n=r.params,s=await e.call(o,n),i={kind:"response",id:r.id,ok:!0,result:s};t.postMessage(i)}catch(o){let n={kind:"response",id:r.id,ok:!1,error:d(o)};t.postMessage(n)}}function S(t){if(t.includes("<!DOCTYPE")||t.includes("<html")){let e=Math.min(t.indexOf("<!DOCTYPE")!==-1?t.indexOf("<!DOCTYPE"):1/0,t.indexOf("<html")!==-1?t.indexOf("<html"):1/0),r=t.substring(0,e).trim();return r?`${r} [HTML error page omitted]`:"[HTML error page omitted]"}return t}function h(t){let e;if(t instanceof Error)e=String(t);else if(typeof t=="object"&&t!==null)try{e=JSON.stringify(t)??"[object]"}catch{return"[object with circular reference]"}else e=String(t);return S(e)}var u=class{listeners=new Map;on(e,r){let o=this.listeners.get(e);o||(o=new Set,this.listeners.set(e,o));let n=r,s=o;return s.add(n),()=>{s.delete(n)}}emit(e,r){let o=this.listeners.get(e);if(!o)return;let n=[...o];for(let s of n)try{s(r)}catch{}}clear(){this.listeners.clear()}};var O=1600,C=15,l=class{pvRecorderLoader;state={tag:"idle"};shutdownPromise;events=new u;handlers={start:e=>this.handleStart(e),stop:()=>this.handleStop(),getState:()=>this.handleGetState(),shutdown:e=>this.handleShutdown(e)};constructor(e={}){this.pvRecorderLoader=e.pvRecorderLoader??(async()=>L("@picovoice/pvrecorder-node"))}call(e,r,o){if(this.shutdownPromise&&e!=="shutdown")return Promise.reject(this.disposedError());let n=this.handlers[e];return n(r)}on(e,r){return this.events.on(e,r)}onFatalError(e){return()=>{}}shutdown(e){return this.call("shutdown",e)}get inputDeviceId(){let e=this.state;switch(e.tag){case"idle":case"stopping":return;case"starting":case"active":return e.deviceId;default:return e}}handleStart(e={}){let r=e.inputDeviceId??-1,o=this.state;switch(o.tag){case"idle":return this.beginStart(r);case"starting":return o.deviceId!==r?Promise.reject(new a(`Microphone is starting on device ${o.deviceId}; cannot start device ${r}.`,"device-busy")):o.startTask;case"active":return o.deviceId!==r?Promise.reject(new a(`Microphone is already open on device ${o.deviceId}; cannot start device ${r}.`,"device-busy")):Promise.resolve();case"stopping":{let n=o.teardown,s={stopped:!1},i=(async()=>{await n.catch(()=>{}),await this.runStart(r,s)})();return this.state={tag:"starting",deviceId:r,cancel:s,startTask:i},i}default:return o}}beginStart(e){let r={stopped:!1},o=this.runStart(e,r);return this.state={tag:"starting",deviceId:e,cancel:r,startTask:o},o}async runStart(e,r){try{let o;try{o=await this.pvRecorderLoader()}catch(i){throw new a(`Voice mode microphone backend (@picovoice/pvrecorder-node) is not available: ${h(i)}. Voice mode may not be supported on this platform, or the install is incomplete \u2014 try reinstalling the CLI.`,"mic-unavailable",{cause:i})}let n;try{n=new o.PvRecorder(O,e,C),n.start()}catch(i){if(n!==void 0){let y=n;c(()=>y.stop()),c(()=>y.release())}throw new a(`Failed to open microphone: ${h(i)}.`,"mic-unavailable",{cause:i})}if(this.shutdownPromise||r.stopped)throw c(()=>n.stop()),c(()=>n.release()),this.shutdownPromise?this.disposedError():new a("Microphone start was cancelled.","cancelled");let s=this.runReadLoop(n,r);this.state={tag:"active",deviceId:e,recorder:n,cancel:r,loop:s}}catch(o){throw this.state.tag==="starting"&&this.state.cancel===r&&(this.state={tag:"idle"}),o}}handleStop(){let e=this.state;switch(e.tag){case"idle":return Promise.resolve();case"starting":{e.cancel.stopped=!0;let r=e.startTask.then(()=>{},()=>{});return this.state={tag:"stopping",teardown:r},r.then(()=>{this.state.tag==="stopping"&&this.state.teardown===r&&(this.state={tag:"idle"})})}case"active":{e.cancel.stopped=!0,c(()=>e.recorder.stop());let r=e.recorder,o=e.loop,n=(async()=>{await o.catch(()=>{}),c(()=>r.release())})();return this.state={tag:"stopping",teardown:n},n.then(()=>{this.state.tag==="stopping"&&this.state.teardown===n&&(this.state={tag:"idle"})})}case"stopping":return e.teardown;default:return e}}handleGetState(){let e=this.state;switch(e.tag){case"idle":case"starting":case"stopping":return Promise.resolve({open:!1});case"active":return Promise.resolve({open:!0});default:return e}}handleShutdown(e){return this.shutdownPromise?this.shutdownPromise:(this.shutdownPromise=(async()=>{await this.handleStop(),this.events.clear()})(),this.shutdownPromise)}disposedError(){return new a("Mic backend has been shut down.","disposed")}async runReadLoop(e,r){for(;!r.stopped;){let o;try{o=await e.read()}catch(s){if(r.stopped)return;r.stopped=!0,this.state.tag==="active"&&this.state.cancel===r&&(this.state={tag:"idle"}),c(()=>e.stop()),c(()=>e.release());let i=w(s);this.events.emit("error",{error:i});return}if(r.stopped)return;let n=Buffer.from(o.buffer.slice(o.byteOffset,o.byteOffset+o.byteLength));this.events.emit("pcm",n)}}};function c(t){try{t()}catch{}}if(!m)throw new Error("voice-mic.worker.js must be loaded as a worker thread.");b(m);var v=m,M=new l;M.on("pcm",t=>{let e={buffer:t.buffer,byteOffset:t.byteOffset,byteLength:t.byteLength},r={kind:"event",event:"pcm",payload:e};v.postMessage(r,[e.buffer])});M.on("error",t=>{let r={kind:"event",event:"error",payload:{error:d(t.error)}};v.postMessage(r)});P(v,M); |
@@ -12,2 +12,4 @@ | ||
| const __clipboardEntrypoint = __path.join(__appPath, "clipboard", "index.js"); | ||
| const __foundryEntrypoint = __path.join(__appPath, "foundry-local-sdk", "index.js"); | ||
| const __pvRecorderEntrypoint = __path.join(__appPath, "pvrecorder", "index.js"); | ||
| const __sharpRequire = __fs.existsSync(__sharpEntrypoint) | ||
@@ -19,5 +21,11 @@ ? __module.createRequire(__sharpEntrypoint) | ||
| : __rootRequire; | ||
| const __foundryRequire = __fs.existsSync(__foundryEntrypoint) | ||
| ? __module.createRequire(__foundryEntrypoint) | ||
| : __rootRequire; | ||
| const __pvRecorderRequire = __fs.existsSync(__pvRecorderEntrypoint) | ||
| ? __module.createRequire(__pvRecorderEntrypoint) | ||
| : __rootRequire; | ||
| const __isVendoredNativeModule = (module) => | ||
| typeof module === "string" && | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/")); | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/") || module === "foundry-local-sdk" || module === "@picovoice/pvrecorder-node"); | ||
| const require = (module) => { | ||
@@ -31,2 +39,8 @@ let req = __rootRequire; | ||
| } | ||
| if (module === "foundry-local-sdk") { | ||
| req = __foundryRequire; | ||
| } | ||
| if (module === "@picovoice/pvrecorder-node") { | ||
| req = __pvRecorderRequire; | ||
| } | ||
@@ -33,0 +47,0 @@ if (typeof module === "string" && (__module.isBuiltin(module) || __isVendoredNativeModule(module))) { |
+13
-8
| { | ||
| "name": "@github/copilot", | ||
| "description": "GitHub Copilot CLI brings the power of Copilot coding agent directly to your terminal.", | ||
| "version": "1.0.41-1", | ||
| "version": "1.0.41", | ||
| "license": "SEE LICENSE IN LICENSE.md", | ||
@@ -55,2 +55,4 @@ "type": "module", | ||
| "clipboard/**/*", | ||
| "foundry-local-sdk/**/*", | ||
| "pvrecorder/**/*", | ||
| "worker/**/*", | ||
@@ -63,15 +65,18 @@ "ripgrep/**/*", | ||
| "copilot-sdk/**/*", | ||
| "voice-foundry.worker.js", | ||
| "voice-installer.worker.js", | ||
| "voice-mic.worker.js", | ||
| "conpty_console_list_agent.js" | ||
| ], | ||
| "buildMetadata": { | ||
| "gitCommit": "7bdeea7" | ||
| "gitCommit": "d5264c4" | ||
| }, | ||
| "optionalDependencies": { | ||
| "@github/copilot-linux-x64": "1.0.41-1", | ||
| "@github/copilot-linux-arm64": "1.0.41-1", | ||
| "@github/copilot-darwin-x64": "1.0.41-1", | ||
| "@github/copilot-darwin-arm64": "1.0.41-1", | ||
| "@github/copilot-win32-x64": "1.0.41-1", | ||
| "@github/copilot-win32-arm64": "1.0.41-1" | ||
| "@github/copilot-linux-x64": "1.0.41", | ||
| "@github/copilot-linux-arm64": "1.0.41", | ||
| "@github/copilot-darwin-x64": "1.0.41", | ||
| "@github/copilot-darwin-arm64": "1.0.41", | ||
| "@github/copilot-win32-x64": "1.0.41", | ||
| "@github/copilot-win32-arm64": "1.0.41" | ||
| } | ||
| } |
@@ -12,2 +12,4 @@ | ||
| const __clipboardEntrypoint = __path.join(__appPath, "clipboard", "index.js"); | ||
| const __foundryEntrypoint = __path.join(__appPath, "foundry-local-sdk", "index.js"); | ||
| const __pvRecorderEntrypoint = __path.join(__appPath, "pvrecorder", "index.js"); | ||
| const __sharpRequire = __fs.existsSync(__sharpEntrypoint) | ||
@@ -19,5 +21,11 @@ ? __module.createRequire(__sharpEntrypoint) | ||
| : __rootRequire; | ||
| const __foundryRequire = __fs.existsSync(__foundryEntrypoint) | ||
| ? __module.createRequire(__foundryEntrypoint) | ||
| : __rootRequire; | ||
| const __pvRecorderRequire = __fs.existsSync(__pvRecorderEntrypoint) | ||
| ? __module.createRequire(__pvRecorderEntrypoint) | ||
| : __rootRequire; | ||
| const __isVendoredNativeModule = (module) => | ||
| typeof module === "string" && | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/")); | ||
| (module.startsWith("@img/") || module.startsWith("@teddyzhu/") || module === "foundry-local-sdk" || module === "@picovoice/pvrecorder-node"); | ||
| const require = (module) => { | ||
@@ -31,2 +39,8 @@ let req = __rootRequire; | ||
| } | ||
| if (module === "foundry-local-sdk") { | ||
| req = __foundryRequire; | ||
| } | ||
| if (module === "@picovoice/pvrecorder-node") { | ||
| req = __pvRecorderRequire; | ||
| } | ||
@@ -33,0 +47,0 @@ if (typeof module === "string" && (__module.isBuiltin(module) || __isVendoredNativeModule(module))) { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Install scripts
Supply chain riskInstall scripts are run when the package is installed or built. Malicious packages often use scripts that run automatically to execute payloads or fetch additional code.
Found 2 instances in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 21 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 3 instances in 1 package
Mixed license
LicensePackage contains multiple licenses.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 20 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Mixed license
LicensePackage contains multiple licenses.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
135225415
3.57%235
45.96%128498
5.4%8
33.33%2
Infinity%116
26.09%77
35.09%